diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index 360f96b3c..77e70e931 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -122,7 +122,6 @@ - [getJsxConfigFromContext](functions/getJsxConfigFromContext.md) - [getJsxElementType](functions/getJsxElementType.md) - [getPhaseKindOfFunction](functions/getPhaseKindOfFunction.md) -- [hasJsxAttribute](functions/hasJsxAttribute.md) - [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md) - [isAssignmentToThisState](functions/isAssignmentToThisState.md) - [isChildrenOfCreateElement](functions/isChildrenOfCreateElement.md) diff --git a/packages/core/docs/functions/getJsxAttribute.md b/packages/core/docs/functions/getJsxAttribute.md index 16d06d565..1a8981c75 100644 --- a/packages/core/docs/functions/getJsxAttribute.md +++ b/packages/core/docs/functions/getJsxAttribute.md @@ -6,7 +6,9 @@ # Function: getJsxAttribute() -> **getJsxAttribute**(`context`, `attributes`, `initialScope?`): (`name`) => `undefined` \| `TSESTreeJSXAttributeLike` +> **getJsxAttribute**(`context`, `node`, `initialScope?`): (`name`) => `undefined` \| `JSXAttribute` \| `JSXSpreadAttribute` + +Get a function to find JSX attributes by name, considering direct attributes and spread attributes. ## Parameters @@ -14,17 +16,25 @@ `RuleContext` -### attributes +The ESLint rule context + +### node + +`JSXElement` -`TSESTreeJSXAttributeLike`[] +The JSX element node ### initialScope? `Scope` +Optional initial scope for variable resolution + ## Returns -> (`name`): `undefined` \| `TSESTreeJSXAttributeLike` +A function that takes an attribute name and returns the corresponding JSX attribute node or undefined + +> (`name`): `undefined` \| `JSXAttribute` \| `JSXSpreadAttribute` ### Parameters @@ -34,4 +44,4 @@ ### Returns -`undefined` \| `TSESTreeJSXAttributeLike` +`undefined` \| `JSXAttribute` \| `JSXSpreadAttribute` diff --git a/packages/core/docs/functions/hasJsxAttribute.md b/packages/core/docs/functions/hasJsxAttribute.md deleted file mode 100644 index 19aa03647..000000000 --- a/packages/core/docs/functions/hasJsxAttribute.md +++ /dev/null @@ -1,43 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / hasJsxAttribute - -# Function: hasJsxAttribute() - -> **hasJsxAttribute**(`context`, `name`, `attributes`, `initialScope?`): `boolean` - -Checks if a JSX element has a specific attribute - -## Parameters - -### context - -`RuleContext` - -ESLint rule context - -### name - -`string` - -Name of the attribute to check for - -### attributes - -(`JSXAttribute` \| `JSXSpreadAttribute`)[] - -List of JSX attributes from opening element - -### initialScope? - -`Scope` - -Optional scope for resolving variables in spread attributes - -## Returns - -`boolean` - -boolean indicating whether the attribute exists diff --git a/packages/core/src/jsx/jsx-attribute.ts b/packages/core/src/jsx/jsx-attribute.ts index 9c8586570..938e7da1a 100644 --- a/packages/core/src/jsx/jsx-attribute.ts +++ b/packages/core/src/jsx/jsx-attribute.ts @@ -1,58 +1,47 @@ -import type * as AST from "@eslint-react/ast"; import type { RuleContext } from "@eslint-react/kit"; import { findProperty, findVariable, getVariableDefinitionNode } from "@eslint-react/var"; -import type { Scope } from "@typescript-eslint/scope-manager"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { Scope } from "@typescript-eslint/scope-manager"; import { getJsxAttributeName } from "./jsx-attribute-name"; -export function getJsxAttribute( - context: RuleContext, - attributes: AST.TSESTreeJSXAttributeLike[], - initialScope?: Scope, -) { +/** + * Get a function to find JSX attributes by name, considering direct attributes and spread attributes. + * @param context The ESLint rule context + * @param node The JSX element node + * @param initialScope Optional initial scope for variable resolution + * @returns A function that takes an attribute name and returns the corresponding JSX attribute node or undefined + */ +export function getJsxAttribute(context: RuleContext, node: TSESTree.JSXElement, initialScope?: Scope) { + const scope = initialScope ?? context.sourceCode.getScope(node); + const attributes = node.openingElement.attributes; + /** + * Find a JSX attribute by name, considering both direct attributes and spread attributes. + * @param name The name of the attribute to find + * @returns The JSX attribute node if found, otherwise undefined + */ return (name: string) => { return attributes.findLast((attr) => { // Case 1: Direct JSX attribute (e.g., className="value") if (attr.type === T.JSXAttribute) { return getJsxAttributeName(context, attr) === name; } - // For spread attributes, we need a scope to resolve variables - if (initialScope == null) return false; switch (attr.argument.type) { // Case 2: Spread from variable (e.g., {...props}) case T.Identifier: { - const variable = findVariable(attr.argument.name, initialScope); + const variable = findVariable(attr.argument.name, scope); const variableNode = getVariableDefinitionNode(variable, 0); if (variableNode?.type === T.ObjectExpression) { - return findProperty(name, variableNode.properties, initialScope) != null; + return findProperty(name, variableNode.properties, scope) != null; } return false; } // Case 3: Spread from object literal (e.g., {{...{prop: value}}}) case T.ObjectExpression: - return findProperty(name, attr.argument.properties, initialScope) != null; + return findProperty(name, attr.argument.properties, scope) != null; } return false; }); }; } - -/** - * Checks if a JSX element has a specific attribute - * - * @param context - ESLint rule context - * @param name - Name of the attribute to check for - * @param attributes - List of JSX attributes from opening element - * @param initialScope - Optional scope for resolving variables in spread attributes - * @returns boolean indicating whether the attribute exists - */ -export function hasJsxAttribute( - context: RuleContext, - name: string, - attributes: TSESTree.JSXOpeningElement["attributes"], - initialScope?: Scope, -) { - return getJsxAttribute(context, attributes, initialScope)(name) != null; -} diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts index 37fa425b5..6c9605f10 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts @@ -55,17 +55,13 @@ export function create(context: RuleContext): RuleListener { return { JSXElement(node) { - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); + const findJsxAttribute = getJsxAttribute(context, node); if (findJsxAttribute(DSIH) == null) return; - const children = findJsxAttribute("children") ?? node.children.find(isSignificantChildren); - if (children == null) return; + const childrenPropOrNode = findJsxAttribute("children") ?? node.children.find(isSignificantChildren); + if (childrenPropOrNode == null) return; context.report({ messageId: "noDangerouslySetInnerhtmlWithChildren", - node: children, + node: childrenPropOrNode, }); }, }; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts index 4f5ea2208..1cb515387 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts @@ -35,16 +35,11 @@ export function create(context: RuleContext): RuleListener { if (!context.sourceCode.text.includes(DSIH)) return {}; return { JSXElement(node) { - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); - const attr = findJsxAttribute(DSIH); - if (attr == null) return; + const dsihProp = getJsxAttribute(context, node)(DSIH); + if (dsihProp == null) return; context.report({ messageId: "noDangerouslySetInnerhtml", - node: attr, + node: dsihProp, }); }, }; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts index c7e93172f..6e33ac2d6 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts @@ -45,13 +45,7 @@ export function create(context: RuleContext): RuleListener { return; } - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); - - if (findJsxAttribute("type") != null) { + if (getJsxAttribute(context, node)("type") != null) { return; } diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts index ed6292fc1..93b0269db 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts @@ -19,14 +19,14 @@ export default createRule<[], MessageID>({ meta: { type: "problem", docs: { - description: "Enforces explicit `sandbox` attribute for `iframe` elements.", + description: "Enforces explicit `sandbox` prop for `iframe` elements.", [Symbol.for("rule_features")]: RULE_FEATURES, }, fixable: "code", hasSuggestions: true, messages: { - addIframeSandbox: "Add 'sandbox' attribute with value '{{value}}'.", - noMissingIframeSandbox: "Add missing 'sandbox' attribute on 'iframe' component.", + addIframeSandbox: "Add 'sandbox' prop with value '{{value}}'.", + noMissingIframeSandbox: "Add missing 'sandbox' prop on 'iframe' component.", }, schema: [], }, @@ -44,17 +44,11 @@ export function create(context: RuleContext): RuleListener { // If the element is not an iframe, we don't need to do anything. if (domElementType !== "iframe") return; - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); + // Find the 'sandbox' prop on the iframe element. + const sandboxProp = getJsxAttribute(context, node)("sandbox"); - // Find the 'sandbox' attribute on the iframe element. - const sandboxAttr = findJsxAttribute("sandbox"); - - // If the 'sandbox' attribute is missing, report an error. - if (sandboxAttr == null) { + // If the 'sandbox' prop is missing, report an error. + if (sandboxProp == null) { context.report({ messageId: "noMissingIframeSandbox", node: node.openingElement, @@ -71,14 +65,14 @@ export function create(context: RuleContext): RuleListener { } // Resolve the value of the 'sandbox' attribute. - const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr); - // If the value is a static string, the attribute is correctly used. + const sandboxValue = resolveJsxAttributeValue(context, sandboxProp); + // If the value is a static string, the prop is correctly used. if (typeof sandboxValue.toStatic("sandbox") === "string") return; // If the value is not a static string (e.g., a variable), report an error. context.report({ messageId: "noMissingIframeSandbox", - node: sandboxValue.node ?? sandboxAttr, + node: sandboxValue.node ?? sandboxProp, suggest: [ { messageId: "addIframeSandbox", @@ -86,8 +80,8 @@ export function create(context: RuleContext): RuleListener { fix(fixer) { // Do not try to fix spread attributes. if (sandboxValue.kind.startsWith("spread")) return null; - // Suggest replacing the attribute with a valid one. - return fixer.replaceText(sandboxAttr, `sandbox=""`); + // Suggest replacing the prop with a valid one. + return fixer.replaceText(sandboxProp, `sandbox=""`); }, }, ], diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-string-style-prop.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-string-style-prop.ts index dc6e01e5f..2a953857f 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-string-style-prop.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-string-style-prop.ts @@ -36,20 +36,14 @@ export function create(context: RuleContext): RuleListener { return; } - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); - - // Find the 'style' attribute on the element. - const styleAttr = findJsxAttribute("style"); - if (styleAttr == null) { + // Find the 'style' prop on the element. + const styleProp = getJsxAttribute(context, node)("style"); + if (styleProp == null) { return; } - // Resolve the static value of the 'style' attribute. - const styleValue = resolveJsxAttributeValue(context, styleAttr); + // Resolve the static value of the 'style' prop. + const styleValue = resolveJsxAttributeValue(context, styleProp); const staticValue = styleValue.toStatic(); // If the resolved value is a string, report an error. @@ -57,7 +51,7 @@ export function create(context: RuleContext): RuleListener { if (typeof staticValue === "string") { context.report({ messageId: "noStringStyleProp", - node: styleValue.node ?? styleAttr, + node: styleValue.node ?? styleProp, }); } }, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts index 32e82ced3..712e96d86 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts @@ -50,23 +50,18 @@ export function create(context: RuleContext): RuleListener { if (resolver.resolve(node).domElementType !== "iframe") { return; } - const findJsxAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); - const sandboxAttr = findJsxAttribute("sandbox"); - if (sandboxAttr == null) { + const sandboxProp = getJsxAttribute(context, node)("sandbox"); + if (sandboxProp == null) { return; } - const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr); + const sandboxValue = resolveJsxAttributeValue(context, sandboxProp); const sandboxValueStatic = sandboxValue.toStatic("sandbox"); if (isUnsafeSandboxCombination(sandboxValueStatic)) { context.report({ messageId: "noUnsafeIframeSandbox", - node: sandboxValue.node ?? sandboxAttr, + node: sandboxValue.node ?? sandboxProp, }); } }, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts index 11f1afb4e..149915675 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts @@ -29,9 +29,9 @@ function isExternalLinkLike(value: unknown): boolean { } /** - * Checks if a rel attribute value contains the necessary security attributes. + * Checks if a rel prop value contains the necessary security attributes. * At minimum, it should contain "noreferrer". - * @param value - The rel attribute value to check + * @param value - The rel prop value to check * @returns Whether the rel value is considered secure */ function isSafeRel(value: unknown): boolean { @@ -71,31 +71,27 @@ export function create(context: RuleContext): RuleListener { if (domElementType !== "a") return; // Get access to the component attributes - const findAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); + const findAttribute = getJsxAttribute(context, node); // Check if target="_blank" is present - const targetAttribute = findAttribute("target"); - if (targetAttribute == null) return; + const targetProp = findAttribute("target"); + if (targetProp == null) return; - const targetAttributeValue = resolveJsxAttributeValue(context, targetAttribute).toStatic("target"); - if (targetAttributeValue !== "_blank") return; + const targetValue = resolveJsxAttributeValue(context, targetProp).toStatic("target"); + if (targetValue !== "_blank") return; // Check if href points to an external resource - const hrefAttribute = findAttribute("href"); - if (hrefAttribute == null) return; + const hrefProp = findAttribute("href"); + if (hrefProp == null) return; - const hrefAttributeValue = resolveJsxAttributeValue(context, hrefAttribute).toStatic("href"); - if (!isExternalLinkLike(hrefAttributeValue)) return; + const hrefValue = resolveJsxAttributeValue(context, hrefProp).toStatic("href"); + if (!isExternalLinkLike(hrefValue)) return; - // Check if rel attribute exists and is secure - const relAttribute = findAttribute("rel"); + // Check if rel prop exists and is secure + const relProp = findAttribute("rel"); - // No rel attribute case - suggest adding one - if (relAttribute == null) { + // No rel prop case - suggest adding one + if (relProp == null) { context.report({ messageId: "noUnsafeTargetBlank", node: node.openingElement, @@ -112,18 +108,18 @@ export function create(context: RuleContext): RuleListener { return; } - // Check if existing rel attribute is secure - const relAttributeValue = resolveJsxAttributeValue(context, relAttribute).toStatic("rel"); - if (isSafeRel(relAttributeValue)) return; + // Check if existing rel prop is secure + const relValue = resolveJsxAttributeValue(context, relProp).toStatic("rel"); + if (isSafeRel(relValue)) return; - // Existing rel attribute is not secure - suggest replacing it + // Existing rel prop is not secure - suggest replacing it context.report({ messageId: "noUnsafeTargetBlank", - node: relAttribute, + node: relProp, suggest: [{ messageId: "addRelNoreferrerNoopener", fix(fixer) { - return fixer.replaceText(relAttribute, `rel="noreferrer noopener"`); + return fixer.replaceText(relProp, `rel="noreferrer noopener"`); }, }], }); diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts index 155d6fbed..a56a6a029 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts @@ -1,4 +1,4 @@ -import { hasJsxAttribute } from "@eslint-react/core"; +import { getJsxAttribute } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; @@ -57,11 +57,10 @@ export function create(context: RuleContext): RuleListener { return; } - const { attributes } = node.openingElement; - const initialScope = context.sourceCode.getScope(node); + const findJsxAttribute = getJsxAttribute(context, node); - const hasChildrenProp = hasJsxAttribute(context, "children", attributes, initialScope); - const hasDangerouslySetInnerHTML = hasJsxAttribute(context, "dangerouslySetInnerHTML", attributes, initialScope); + const hasChildrenProp = findJsxAttribute("children") != null; + const hasDangerouslySetInnerHTML = findJsxAttribute("dangerouslySetInnerHTML") != null; if (node.children.length > 0 || hasChildrenProp || hasDangerouslySetInnerHTML) { context.report({ diff --git a/packages/plugins/eslint-plugin-react-dom/src/utils/create-jsx-element-resolver.ts b/packages/plugins/eslint-plugin-react-dom/src/utils/create-jsx-element-resolver.ts index 7d6fc2680..8c4f803cd 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/utils/create-jsx-element-resolver.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/utils/create-jsx-element-resolver.ts @@ -42,8 +42,7 @@ export function createJsxElementResolver(context: RuleContext) { } // Look for the polymorphic prop (e.g., 'as', 'component') in the element's attributes - const findAttribute = getJsxAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const polymorphicProp = findAttribute(polymorphicPropName); + const polymorphicProp = getJsxAttribute(context, node)(polymorphicPropName); // If the polymorphic prop exists, try to determine its static value if (polymorphicProp != null) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts index 9440aec6f..4686167b9 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts @@ -31,12 +31,7 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { return { JSXElement(node) { - const findAttribute = getJsxAttribute( - context, - node.openingElement.attributes, - context.sourceCode.getScope(node), - ); - const childrenProp = findAttribute("children"); + const childrenProp = getJsxAttribute(context, node)("children"); if (childrenProp != null) { context.report({ messageId: "noChildrenProp", diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts index 9933aa83e..f44adefbb 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts @@ -36,8 +36,7 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { return { JSXOpeningElement(node: TSESTree.JSXOpeningElement) { - const initialScope = context.sourceCode.getScope(node); - const keyProp = getJsxAttribute(context, node.attributes, initialScope)("key"); + const keyProp = getJsxAttribute(context, node.parent)("key"); const isKeyPropOnElement = node.attributes .some((n) => n.type === T.JSXAttribute diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts index 249790a83..de62a946b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { hasJsxAttribute, isChildrenToArrayCall } from "@eslint-react/core"; +import { getJsxAttribute, isChildrenToArrayCall } from "@eslint-react/core"; import { type RuleContext, type RuleFeature, report } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -40,8 +40,7 @@ export function create(context: RuleContext): RuleListener { function checkIteratorElement(node: TSESTree.Node): null | ReportDescriptor { switch (node.type) { case T.JSXElement: { - const initialScope = context.sourceCode.getScope(node); - if (!hasJsxAttribute(context, "key", node.openingElement.attributes, initialScope)) { + if (getJsxAttribute(context, node)("key") == null) { return { messageId: "missingKey", node, @@ -101,7 +100,7 @@ export function create(context: RuleContext): RuleListener { } const initialScope = context.sourceCode.getScope(node); for (const element of elements) { - if (!hasJsxAttribute(context, "key", element.openingElement.attributes, initialScope)) { + if (getJsxAttribute(context, element, initialScope)("key") == null) { context.report({ messageId: "missingKey", node: element, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts index 530df65b0..061362f9b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-param */ import * as AST from "@eslint-react/ast"; -import { hasJsxAttribute, isJsxFragmentElement, isJsxHostElement, isJsxText } from "@eslint-react/core"; +import { getJsxAttribute, isJsxFragmentElement, isJsxHostElement, isJsxText } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { TSESTree } from "@typescript-eslint/utils"; @@ -109,10 +109,8 @@ function checkNode( node: TSESTree.JSXElement | TSESTree.JSXFragment, allowExpressions: boolean, ) { - const initialScope = context.sourceCode.getScope(node); - // Skip if the fragment has a key prop (indicates it's needed for lists) - if (node.type === T.JSXElement && hasJsxAttribute(context, "key", node.openingElement.attributes, initialScope)) { + if (node.type === T.JSXElement && getJsxAttribute(context, node)("key") != null) { return; }