Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inline methods of jsx-ast-utils to simplify dependency tree #114

Merged
merged 2 commits into from
Dec 30, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -38,7 +38,6 @@
"dependencies": {
"@typescript-eslint/utils": "^6.4.0",
"is-html": "^2.0.0",
"jsx-ast-utils": "^3.3.3",
"kebab-case": "^1.0.2",
"known-css-properties": "^0.24.0",
"style-to-object": "^0.3.0"
60 changes: 49 additions & 11 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/deps.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
declare module "jsx-ast-utils";
declare module "kebab-case";
25 changes: 4 additions & 21 deletions src/rules/jsx-no-duplicate-props.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { jsxGetAllProps } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;

@@ -58,27 +59,9 @@ export default createRule<Options, MessageIds>({
props.add(name);
};

node.attributes.forEach((decl) => {
if (decl.type === "JSXSpreadAttribute") {
if (decl.argument.type === "ObjectExpression") {
for (const prop of decl.argument.properties) {
if (prop.type === "Property") {
if (prop.key.type === "Identifier") {
checkPropName(prop.key.name, prop.key);
} else if (prop.key.type === "Literal") {
checkPropName(String(prop.key.value), prop.key);
}
}
}
}
} else {
const name =
decl.name.type === "JSXNamespacedName"
? `${decl.name.namespace.name}:${decl.name.name.name}`
: decl.name.name;
checkPropName(name, decl.name);
}
});
for (const [name, propNode] of jsxGetAllProps(node.attributes)) {
checkPropName(name, propNode);
}

const hasChildrenProp = props.has("children");
const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).children.length > 0;
6 changes: 3 additions & 3 deletions src/rules/no-innerhtml.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import { propName } from "jsx-ast-utils";
import isHtml from "is-html";
import { jsxPropName } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStringIfConstant } = ASTUtils;
@@ -48,7 +48,7 @@ export default createRule<Options, MessageIds>({
const allowStatic = Boolean(context.options[0]?.allowStatic ?? true);
return {
JSXAttribute(node) {
if (propName(node) === "dangerouslySetInnerHTML") {
if (jsxPropName(node) === "dangerouslySetInnerHTML") {
if (
node.value?.type === "JSXExpressionContainer" &&
node.value.expression.type === "ObjectExpression" &&
@@ -85,7 +85,7 @@ export default createRule<Options, MessageIds>({
});
}
return;
} else if (propName(node) !== "innerHTML") {
} else if (jsxPropName(node) !== "innerHTML") {
return;
}

9 changes: 4 additions & 5 deletions src/rules/no-react-specific-props.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TSESLint, ESLintUtils } from "@typescript-eslint/utils";
import { getProp, hasProp } from "jsx-ast-utils";
import { isDOMElementName } from "../utils";
import { isDOMElementName, jsxGetProp, jsxHasProp } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;

@@ -29,10 +28,10 @@ export default createRule({
return {
JSXOpeningElement(node) {
for (const { from, to } of reactSpecificProps) {
const classNameAttribute = getProp(node.attributes, from);
const classNameAttribute = jsxGetProp(node.attributes, from);
if (classNameAttribute) {
// only auto-fix if there is no class prop defined
const fix = !hasProp(node.attributes, to, { ignoreCase: false })
const fix = !jsxHasProp(node.attributes, to)
? (fixer: TSESLint.RuleFixer) => fixer.replaceText(classNameAttribute.name, to)
: undefined;

@@ -45,7 +44,7 @@ export default createRule({
}
}
if (node.name.type === "JSXIdentifier" && isDOMElementName(node.name.name)) {
const keyProp = getProp(node.attributes, "key");
const keyProp = jsxGetProp(node.attributes, "key");
if (keyProp) {
// no DOM element has a 'key' prop, so we can assert that this is a holdover from React.
context.report({
11 changes: 6 additions & 5 deletions src/rules/prefer-classlist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ESLintUtils, TSESTree as T } from "@typescript-eslint/utils";
import { hasProp, propName } from "jsx-ast-utils";
import { jsxHasProp, jsxPropName } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;

@@ -45,10 +45,11 @@ export default createRule<Options, MessageIds>({
return {
JSXAttribute(node) {
if (
["class", "className"].indexOf(propName(node)) === -1 ||
hasProp((node.parent as T.JSXOpeningElement | undefined)?.attributes, "classlist", {
ignoreCase: false,
})
["class", "className"].indexOf(jsxPropName(node)) === -1 ||
jsxHasProp(
(node.parent as T.JSXOpeningElement | undefined)?.attributes ?? [],
"classlist"
)
) {
return;
}
4 changes: 2 additions & 2 deletions src/rules/style-prop.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import kebabCase from "kebab-case";
import { all as allCssProperties } from "known-css-properties";
import parse from "style-to-object";
import { propName } from "jsx-ast-utils";
import { jsxPropName } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getPropertyName, getStaticValue } = ASTUtils;
@@ -62,7 +62,7 @@ export default createRule<Options, MessageIds>({

return {
JSXAttribute(node) {
if (styleProps.indexOf(propName(node)) === -1) {
if (styleProps.indexOf(jsxPropName(node)) === -1) {
return;
}
const style =
44 changes: 44 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -220,3 +220,47 @@ export function removeSpecifier(
}
return fixer.remove(specifier);
}

export function jsxPropName(prop: T.JSXAttribute) {
if (prop.name.type === "JSXNamespacedName") {
return `${prop.name.namespace.name}:${prop.name.name.name}`;
}

return prop.name.name;
}

type Props = T.JSXOpeningElement["attributes"];

/** Iterate through both attributes and spread object props, yielding the name and the node. */
export function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {
for (const attr of props) {
if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
for (const property of attr.argument.properties) {
if (property.type === "Property") {
if (property.key.type === "Identifier") {
yield [property.key.name, property.key];
} else if (property.key.type === "Literal") {
yield [String(property.key.value), property.key];
}
}
}
} else if (attr.type === "JSXAttribute") {
yield [jsxPropName(attr), attr.name];
}
}
}

/** Returns whether an element has a prop, checking spread object props. */
export function jsxHasProp(props: Props, prop: string) {
for (const [p] of jsxGetAllProps(props)) {
if (p === prop) return true;
}
return false;
}

/** Get a JSXAttribute, excluding spread props. */
export function jsxGetProp(props: Props, prop: string) {
return props.find(
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
) as T.JSXAttribute | undefined;
}