Skip to content

Commit 10be4b3

Browse files
committed
Port jsx-ast-util's checking for spread object props, simplify jsx-no-duplicate-props.
1 parent 51b3710 commit 10be4b3

File tree

2 files changed

+33
-30
lines changed

2 files changed

+33
-30
lines changed

src/rules/jsx-no-duplicate-props.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
2+
import { jsxGetAllProps } from "../utils";
23

34
const createRule = ESLintUtils.RuleCreator.withoutDocs;
45

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

61-
node.attributes.forEach((decl) => {
62-
if (decl.type === "JSXSpreadAttribute") {
63-
if (decl.argument.type === "ObjectExpression") {
64-
for (const prop of decl.argument.properties) {
65-
if (prop.type === "Property") {
66-
if (prop.key.type === "Identifier") {
67-
checkPropName(prop.key.name, prop.key);
68-
} else if (prop.key.type === "Literal") {
69-
checkPropName(String(prop.key.value), prop.key);
70-
}
71-
}
72-
}
73-
}
74-
} else {
75-
const name =
76-
decl.name.type === "JSXNamespacedName"
77-
? `${decl.name.namespace.name}:${decl.name.name.name}`
78-
: decl.name.name;
79-
checkPropName(name, decl.name);
80-
}
81-
});
62+
for (const [name, propNode] of jsxGetAllProps(node.attributes)) {
63+
checkPropName(name, propNode);
64+
}
8265

8366
const hasChildrenProp = props.has("children");
8467
const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).children.length > 0;

src/utils.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,24 +222,44 @@ export function removeSpecifier(
222222
}
223223

224224
export function jsxPropName(prop: T.JSXAttribute) {
225-
if (!prop.type || prop.type !== "JSXAttribute") {
226-
throw new Error("The prop must be a JSXAttribute collected by the AST parser.");
227-
}
228-
229225
if (prop.name.type === "JSXNamespacedName") {
230226
return `${prop.name.namespace.name}:${prop.name.name.name}`;
231227
}
232228

233229
return prop.name.name;
234230
}
235231

236-
export function jsxHasProp(props: (T.JSXAttribute | T.JSXSpreadAttribute)[], prop: string) {
237-
return props.some(
238-
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
239-
);
232+
type Props = T.JSXOpeningElement["attributes"];
233+
234+
/** Iterate through both attributes and spread object props, yielding the name and the node. */
235+
export function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {
236+
for (const attr of props) {
237+
if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
238+
for (const property of attr.argument.properties) {
239+
if (property.type === "Property") {
240+
if (property.key.type === "Identifier") {
241+
yield [property.key.name, property.key];
242+
} else if (property.key.type === "Literal") {
243+
yield [String(property.key.value), property.key];
244+
}
245+
}
246+
}
247+
} else if (attr.type === "JSXAttribute") {
248+
yield [jsxPropName(attr), attr.name];
249+
}
250+
}
251+
}
252+
253+
/** Returns whether an element has a prop, checking spread object props. */
254+
export function jsxHasProp(props: Props, prop: string) {
255+
for (const [p] of jsxGetAllProps(props)) {
256+
if (p === prop) return true;
257+
}
258+
return false;
240259
}
241260

242-
export function jsxGetProp(props: (T.JSXAttribute | T.JSXSpreadAttribute)[], prop: string) {
261+
/** Get a JSXAttribute, excluding spread props. */
262+
export function jsxGetProp(props: Props, prop: string) {
243263
return props.find(
244264
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
245265
) as T.JSXAttribute | undefined;

0 commit comments

Comments
 (0)