diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index e13fdf7881..360f96b3cc 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -23,7 +23,6 @@ ## Type Aliases -- [AttributeValue](type-aliases/AttributeValue.md) - [Component](type-aliases/Component.md) - [ComponentDetectionHint](type-aliases/ComponentDetectionHint.md) - [ComponentEffectPhaseKind](type-aliases/ComponentEffectPhaseKind.md) @@ -33,7 +32,8 @@ - [ComponentPhaseKind](type-aliases/ComponentPhaseKind.md) - [ComponentStateKind](type-aliases/ComponentStateKind.md) - [EffectKind](type-aliases/EffectKind.md) -- [JSXDetectionHint](type-aliases/JSXDetectionHint.md) +- [JsxAttributeValue](type-aliases/JsxAttributeValue.md) +- [JsxDetectionHint](type-aliases/JsxDetectionHint.md) ## Variables @@ -106,25 +106,23 @@ - [isUseStateCall](variables/isUseStateCall.md) - [isUseSyncExternalStoreCall](variables/isUseSyncExternalStoreCall.md) - [isUseTransitionCall](variables/isUseTransitionCall.md) -- [JSXDetectionHint](variables/JSXDetectionHint.md) +- [JsxDetectionHint](variables/JsxDetectionHint.md) - [JsxEmit](variables/JsxEmit.md) - [REACT\_BUILTIN\_HOOK\_NAMES](variables/REACT_BUILTIN_HOOK_NAMES.md) ## Functions -- [findParentAttribute](functions/findParentAttribute.md) -- [getAttribute](functions/getAttribute.md) -- [getAttributeName](functions/getAttributeName.md) +- [findParentJsxAttribute](functions/findParentJsxAttribute.md) - [getComponentFlagFromInitPath](functions/getComponentFlagFromInitPath.md) - [getComponentNameFromId](functions/getComponentNameFromId.md) -- [getElementType](functions/getElementType.md) - [getFunctionComponentId](functions/getFunctionComponentId.md) +- [getJsxAttribute](functions/getJsxAttribute.md) +- [getJsxAttributeName](functions/getJsxAttributeName.md) - [getJsxConfigFromAnnotation](functions/getJsxConfigFromAnnotation.md) - [getJsxConfigFromContext](functions/getJsxConfigFromContext.md) +- [getJsxElementType](functions/getJsxElementType.md) - [getPhaseKindOfFunction](functions/getPhaseKindOfFunction.md) -- [hasAnyAttribute](functions/hasAnyAttribute.md) -- [hasAttribute](functions/hasAttribute.md) -- [hasEveryAttribute](functions/hasEveryAttribute.md) +- [hasJsxAttribute](functions/hasJsxAttribute.md) - [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md) - [isAssignmentToThisState](functions/isAssignmentToThisState.md) - [isChildrenOfCreateElement](functions/isChildrenOfCreateElement.md) @@ -135,13 +133,13 @@ - [isComponentWrapperCall](functions/isComponentWrapperCall.md) - [isComponentWrapperCallLoose](functions/isComponentWrapperCallLoose.md) - [isDeclaredInRenderPropLoose](functions/isDeclaredInRenderPropLoose.md) -- [isFragmentElement](functions/isFragmentElement.md) - [isFunctionOfComponentDidMount](functions/isFunctionOfComponentDidMount.md) - [isFunctionOfComponentWillUnmount](functions/isFunctionOfComponentWillUnmount.md) - [isFunctionOfRenderMethod](functions/isFunctionOfRenderMethod.md) - [isFunctionOfUseEffectCleanup](functions/isFunctionOfUseEffectCleanup.md) - [isFunctionOfUseEffectSetup](functions/isFunctionOfUseEffectSetup.md) -- [isHostElement](functions/isHostElement.md) +- [isJsxFragmentElement](functions/isJsxFragmentElement.md) +- [isJsxHostElement](functions/isJsxHostElement.md) - [isJsxLike](functions/isJsxLike.md) - [isJsxText](functions/isJsxText.md) - [isPureComponent](functions/isPureComponent.md) @@ -158,7 +156,7 @@ - [isRenderPropLoose](functions/isRenderPropLoose.md) - [isThisSetState](functions/isThisSetState.md) - [isUseEffectLikeCall](functions/isUseEffectLikeCall.md) -- [resolveAttributeValue](functions/resolveAttributeValue.md) +- [resolveJsxAttributeValue](functions/resolveJsxAttributeValue.md) - [stringifyJsx](functions/stringifyJsx.md) - [useComponentCollector](functions/useComponentCollector.md) - [useComponentCollectorLegacy](functions/useComponentCollectorLegacy.md) diff --git a/packages/core/docs/functions/findParentAttribute.md b/packages/core/docs/functions/findParentJsxAttribute.md similarity index 73% rename from packages/core/docs/functions/findParentAttribute.md rename to packages/core/docs/functions/findParentJsxAttribute.md index 6d1d5b1092..8702520387 100644 --- a/packages/core/docs/functions/findParentAttribute.md +++ b/packages/core/docs/functions/findParentJsxAttribute.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / findParentAttribute +[@eslint-react/core](../README.md) / findParentJsxAttribute -# Function: findParentAttribute() +# Function: findParentJsxAttribute() -> **findParentAttribute**(`node`, `test`): `undefined` \| `JSXAttribute` +> **findParentJsxAttribute**(`node`, `test`): `undefined` \| `JSXAttribute` Traverses up the AST to find a parent JSX attribute node that matches a given test diff --git a/packages/core/docs/functions/getAttribute.md b/packages/core/docs/functions/getJsxAttribute.md similarity index 61% rename from packages/core/docs/functions/getAttribute.md rename to packages/core/docs/functions/getJsxAttribute.md index 3dd8b08f79..16d06d5653 100644 --- a/packages/core/docs/functions/getAttribute.md +++ b/packages/core/docs/functions/getJsxAttribute.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / getAttribute +[@eslint-react/core](../README.md) / getJsxAttribute -# Function: getAttribute() +# Function: getJsxAttribute() -> **getAttribute**(`context`, `attributes`, `initialScope?`): (`name`) => `undefined` \| `TSESTreeJSXAttributeLike` +> **getJsxAttribute**(`context`, `attributes`, `initialScope?`): (`name`) => `undefined` \| `TSESTreeJSXAttributeLike` ## Parameters diff --git a/packages/core/docs/functions/getAttributeName.md b/packages/core/docs/functions/getJsxAttributeName.md similarity index 63% rename from packages/core/docs/functions/getAttributeName.md rename to packages/core/docs/functions/getJsxAttributeName.md index 827672a1a2..5b576739e6 100644 --- a/packages/core/docs/functions/getAttributeName.md +++ b/packages/core/docs/functions/getJsxAttributeName.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / getAttributeName +[@eslint-react/core](../README.md) / getJsxAttributeName -# Function: getAttributeName() +# Function: getJsxAttributeName() -> **getAttributeName**(`context`, `node`): `string` +> **getJsxAttributeName**(`context`, `node`): `string` Get the stringified name of a JSX attribute diff --git a/packages/core/docs/functions/getElementType.md b/packages/core/docs/functions/getJsxElementType.md similarity index 75% rename from packages/core/docs/functions/getElementType.md rename to packages/core/docs/functions/getJsxElementType.md index 1d0076eaeb..bf8c26c896 100644 --- a/packages/core/docs/functions/getElementType.md +++ b/packages/core/docs/functions/getJsxElementType.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / getElementType +[@eslint-react/core](../README.md) / getJsxElementType -# Function: getElementType() +# Function: getJsxElementType() -> **getElementType**(`context`, `node`): `string` +> **getJsxElementType**(`context`, `node`): `string` Extracts the element type name from a JSX element or fragment For JSX elements, returns the stringified name (e.g., "div", "Button", "React.Fragment") diff --git a/packages/core/docs/functions/hasAnyAttribute.md b/packages/core/docs/functions/hasAnyAttribute.md deleted file mode 100644 index 33abe8b33e..0000000000 --- a/packages/core/docs/functions/hasAnyAttribute.md +++ /dev/null @@ -1,43 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / hasAnyAttribute - -# Function: hasAnyAttribute() - -> **hasAnyAttribute**(`context`, `names`, `attributes`, `initialScope?`): `boolean` - -Checks if a JSX element has at least one of the specified attributes - -## Parameters - -### context - -`RuleContext` - -ESLint rule context - -### names - -`string`[] - -Array of attribute names 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 any of the attributes exist diff --git a/packages/core/docs/functions/hasEveryAttribute.md b/packages/core/docs/functions/hasEveryAttribute.md deleted file mode 100644 index aee0af6e86..0000000000 --- a/packages/core/docs/functions/hasEveryAttribute.md +++ /dev/null @@ -1,43 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / hasEveryAttribute - -# Function: hasEveryAttribute() - -> **hasEveryAttribute**(`context`, `names`, `attributes`, `initialScope?`): `boolean` - -Checks if a JSX element has all of the specified attributes - -## Parameters - -### context - -`RuleContext` - -ESLint rule context - -### names - -`string`[] - -Array of attribute names 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 all of the attributes exist diff --git a/packages/core/docs/functions/hasAttribute.md b/packages/core/docs/functions/hasJsxAttribute.md similarity index 74% rename from packages/core/docs/functions/hasAttribute.md rename to packages/core/docs/functions/hasJsxAttribute.md index 799d265d62..19aa036477 100644 --- a/packages/core/docs/functions/hasAttribute.md +++ b/packages/core/docs/functions/hasJsxAttribute.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / hasAttribute +[@eslint-react/core](../README.md) / hasJsxAttribute -# Function: hasAttribute() +# Function: hasJsxAttribute() -> **hasAttribute**(`context`, `name`, `attributes`, `initialScope?`): `boolean` +> **hasJsxAttribute**(`context`, `name`, `attributes`, `initialScope?`): `boolean` Checks if a JSX element has a specific attribute diff --git a/packages/core/docs/functions/isFragmentElement.md b/packages/core/docs/functions/isJsxFragmentElement.md similarity index 70% rename from packages/core/docs/functions/isFragmentElement.md rename to packages/core/docs/functions/isJsxFragmentElement.md index c8d38e1347..c4d2004526 100644 --- a/packages/core/docs/functions/isFragmentElement.md +++ b/packages/core/docs/functions/isJsxFragmentElement.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / isFragmentElement +[@eslint-react/core](../README.md) / isJsxFragmentElement -# Function: isFragmentElement() +# Function: isJsxFragmentElement() -> **isFragmentElement**(`context`, `node`): `node is JSXElement` +> **isJsxFragmentElement**(`context`, `node`): `node is JSXElement` Determines if a JSX element is a React Fragment Fragments can be imported from React and used like or diff --git a/packages/core/docs/functions/isHostElement.md b/packages/core/docs/functions/isJsxHostElement.md similarity index 71% rename from packages/core/docs/functions/isHostElement.md rename to packages/core/docs/functions/isJsxHostElement.md index b1828bd26d..108e63c1ad 100644 --- a/packages/core/docs/functions/isHostElement.md +++ b/packages/core/docs/functions/isJsxHostElement.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / isHostElement +[@eslint-react/core](../README.md) / isJsxHostElement -# Function: isHostElement() +# Function: isJsxHostElement() -> **isHostElement**(`context`, `node`): `boolean` +> **isJsxHostElement**(`context`, `node`): `boolean` Determines if a JSX element is a host element Host elements in React start with lowercase letters (e.g., div, span) diff --git a/packages/core/docs/functions/resolveAttributeValue.md b/packages/core/docs/functions/resolveAttributeValue.md deleted file mode 100644 index 7dda553791..0000000000 --- a/packages/core/docs/functions/resolveAttributeValue.md +++ /dev/null @@ -1,23 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / resolveAttributeValue - -# Function: resolveAttributeValue() - -> **resolveAttributeValue**(`context`, `attribute`): \{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \} - -## Parameters - -### context - -`RuleContext` - -### attribute - -`TSESTreeJSXAttributeLike` - -## Returns - -\{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \} diff --git a/packages/core/docs/functions/resolveJsxAttributeValue.md b/packages/core/docs/functions/resolveJsxAttributeValue.md new file mode 100644 index 0000000000..e7025300a3 --- /dev/null +++ b/packages/core/docs/functions/resolveJsxAttributeValue.md @@ -0,0 +1,23 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / resolveJsxAttributeValue + +# Function: resolveJsxAttributeValue() + +> **resolveJsxAttributeValue**(`context`, `attribute`): \{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \} + +## Parameters + +### context + +`RuleContext` + +### attribute + +`TSESTreeJSXAttributeLike` + +## Returns + +\{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \} diff --git a/packages/core/docs/type-aliases/AttributeValue.md b/packages/core/docs/type-aliases/AttributeValue.md deleted file mode 100644 index 94d2b4ef49..0000000000 --- a/packages/core/docs/type-aliases/AttributeValue.md +++ /dev/null @@ -1,11 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / AttributeValue - -# Type Alias: AttributeValue - -> **AttributeValue** = \{ `kind`: `"boolean"`; `toStatic`: `true`; \} \| \{ `kind`: `"element"`; `node`: `TSESTree.JSXElement`; `toStatic`: `unknown`; \} \| \{ `kind`: `"literal"`; `node`: `TSESTree.Literal`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `TSESTree.JSXExpressionContainer`\[`"expression"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadProps"`; `node`: `TSESTree.JSXSpreadAttribute`\[`"argument"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadChild"`; `node`: `TSESTree.JSXSpreadChild`\[`"expression"`\]; `toStatic`: `unknown`; \} - -Represents possible JSX attribute value types that can be resolved diff --git a/packages/core/docs/type-aliases/JsxAttributeValue.md b/packages/core/docs/type-aliases/JsxAttributeValue.md new file mode 100644 index 0000000000..204ca1771e --- /dev/null +++ b/packages/core/docs/type-aliases/JsxAttributeValue.md @@ -0,0 +1,11 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / JsxAttributeValue + +# Type Alias: JsxAttributeValue + +> **JsxAttributeValue** = \{ `kind`: `"boolean"`; `toStatic`: `true`; \} \| \{ `kind`: `"element"`; `node`: `TSESTree.JSXElement`; `toStatic`: `unknown`; \} \| \{ `kind`: `"literal"`; `node`: `TSESTree.Literal`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `TSESTree.JSXExpressionContainer`\[`"expression"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadProps"`; `node`: `TSESTree.JSXSpreadAttribute`\[`"argument"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadChild"`; `node`: `TSESTree.JSXSpreadChild`\[`"expression"`\]; `toStatic`: `unknown`; \} + +Represents possible JSX attribute value types that can be resolved diff --git a/packages/core/docs/type-aliases/JSXDetectionHint.md b/packages/core/docs/type-aliases/JsxDetectionHint.md similarity index 55% rename from packages/core/docs/type-aliases/JSXDetectionHint.md rename to packages/core/docs/type-aliases/JsxDetectionHint.md index 400be121fc..a0e002c5e7 100644 --- a/packages/core/docs/type-aliases/JSXDetectionHint.md +++ b/packages/core/docs/type-aliases/JsxDetectionHint.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / JSXDetectionHint +[@eslint-react/core](../README.md) / JsxDetectionHint -# Type Alias: JSXDetectionHint +# Type Alias: JsxDetectionHint -> **JSXDetectionHint** = `bigint` +> **JsxDetectionHint** = `bigint` BitFlags for configuring JSX detection behavior Uses BigInt for bit operations to support many flags diff --git a/packages/core/docs/variables/JSXDetectionHint.md b/packages/core/docs/variables/JsxDetectionHint.md similarity index 89% rename from packages/core/docs/variables/JSXDetectionHint.md rename to packages/core/docs/variables/JsxDetectionHint.md index 2c6d7cb59b..0c852295f8 100644 --- a/packages/core/docs/variables/JSXDetectionHint.md +++ b/packages/core/docs/variables/JsxDetectionHint.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / JSXDetectionHint +[@eslint-react/core](../README.md) / JsxDetectionHint -# Variable: JSXDetectionHint +# Variable: JsxDetectionHint -> **JSXDetectionHint**: `object` +> **JsxDetectionHint**: `object` Flags to control JSX detection behavior: - Skip* flags: Ignore specific node types when detecting JSX diff --git a/packages/core/src/component/component-detection-hint.ts b/packages/core/src/component/component-detection-hint.ts index d36e835135..a91b171e1f 100644 --- a/packages/core/src/component/component-detection-hint.ts +++ b/packages/core/src/component/component-detection-hint.ts @@ -1,6 +1,6 @@ /* eslint-disable perfectionist/sort-objects */ -import { JSXDetectionHint } from "../jsx"; +import { JsxDetectionHint } from "../jsx"; export type ComponentDetectionHint = bigint; @@ -9,9 +9,9 @@ export type ComponentDetectionHint = bigint; */ export const ComponentDetectionHint = { /** - * 1n << 0n - 1n << 63n are reserved for JSXDetectionHint + * 1n << 0n - 1n << 63n are reserved for JsxDetectionHint */ - ...JSXDetectionHint, + ...JsxDetectionHint, /** * Skip function component created by React.memo */ diff --git a/packages/core/src/component/component-render-prop.ts b/packages/core/src/component/component-render-prop.ts index ade460b3f7..2e5302f2fa 100644 --- a/packages/core/src/component/component-render-prop.ts +++ b/packages/core/src/component/component-render-prop.ts @@ -2,7 +2,7 @@ import * as AST from "@eslint-react/ast"; import type { RuleContext } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import { JSXDetectionHint, isJsxLike } from "../jsx"; +import { JsxDetectionHint, isJsxLike } from "../jsx"; /** * Unsafe check whether given node is a render function @@ -28,10 +28,10 @@ export function isRenderFunctionLoose(context: RuleContext, node: AST.TSESTreeFu return isJsxLike( context.sourceCode, body, - JSXDetectionHint.SkipNullLiteral - | JSXDetectionHint.SkipUndefined - | JSXDetectionHint.StrictLogical - | JSXDetectionHint.StrictConditional, + JsxDetectionHint.SkipNullLiteral + | JsxDetectionHint.SkipUndefined + | JsxDetectionHint.StrictLogical + | JsxDetectionHint.StrictConditional, ); } diff --git a/packages/core/src/jsx/index.ts b/packages/core/src/jsx/index.ts index 423a7b65c7..cd56225cbd 100644 --- a/packages/core/src/jsx/index.ts +++ b/packages/core/src/jsx/index.ts @@ -5,6 +5,5 @@ export * from "./jsx-config"; export * from "./jsx-detection"; export * from "./jsx-element-is"; export * from "./jsx-element-type"; -export * from "./jsx-has"; export * from "./jsx-hierarchy"; export * from "./jsx-stringify"; diff --git a/packages/core/src/jsx/jsx-attribute-name.ts b/packages/core/src/jsx/jsx-attribute-name.ts index df757acfb7..a332de3538 100644 --- a/packages/core/src/jsx/jsx-attribute-name.ts +++ b/packages/core/src/jsx/jsx-attribute-name.ts @@ -8,6 +8,6 @@ import { stringifyJsx } from "./jsx-stringify"; * @param node The JSX attribute node * @returns The name of the attribute */ -export function getAttributeName(context: RuleContext, node: TSESTree.JSXAttribute) { +export function getJsxAttributeName(context: RuleContext, node: TSESTree.JSXAttribute) { return stringifyJsx(node.name); } diff --git a/packages/core/src/jsx/jsx-attribute-value.ts b/packages/core/src/jsx/jsx-attribute-value.ts index f67371dc3b..0f32b146c8 100644 --- a/packages/core/src/jsx/jsx-attribute-value.ts +++ b/packages/core/src/jsx/jsx-attribute-value.ts @@ -10,7 +10,7 @@ import { P, match } from "ts-pattern"; /** * Represents possible JSX attribute value types that can be resolved */ -export type AttributeValue = +export type JsxAttributeValue = | { kind: "boolean"; toStatic(): true } // Boolean attributes (e.g., disabled) | { kind: "element"; node: TSESTree.JSXElement; toStatic(): unknown } // JSX element as value (e.g., />) | { kind: "literal"; node: TSESTree.Literal; toStatic(): TSESTree.Literal["value"] } // Literal values @@ -18,7 +18,7 @@ export type AttributeValue = | { kind: "spreadProps"; node: TSESTree.JSXSpreadAttribute["argument"]; toStatic(name?: string): unknown } // Spread props (e.g., {...props}) | { kind: "spreadChild"; node: TSESTree.JSXSpreadChild["expression"]; toStatic(): unknown }; // Spread children (e.g., {...["Hello", " ", "spread", " ", "children"]}) -export function resolveAttributeValue(context: RuleContext, attribute: AST.TSESTreeJSXAttributeLike) { +export function resolveJsxAttributeValue(context: RuleContext, attribute: AST.TSESTreeJSXAttributeLike) { const initialScope = context.sourceCode.getScope(attribute); function handleJsxAttribute(node: TSESTree.JSXAttribute) { // Case 1: Boolean attribute with no value (e.g., disabled) @@ -28,7 +28,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST toStatic() { return true; }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; } switch (node.value.type) { // Case 2: Literal value (e.g., className="container") @@ -40,7 +40,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST toStatic() { return staticValue; }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; } // Case 3: Expression container (e.g., className={variable}) case T.JSXExpressionContainer: { @@ -51,7 +51,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST toStatic() { return getStaticValue(expr, initialScope)?.value; }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; } // Case 4: JSX Element as value (e.g., element=) case T.JSXElement: @@ -61,7 +61,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST toStatic() { return unit; }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; // Case 5: JSX spread children (e.g.,
{...["Hello", " ", "spread", " ", "children"]}
) case T.JSXSpreadChild: return { @@ -70,7 +70,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST toStatic() { return unit; }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; } } @@ -86,7 +86,7 @@ export function resolveAttributeValue(context: RuleContext, attribute: AST.TSEST .with({ [name]: P.select(P.any) }, identity) .otherwise(() => unit); }, - } as const satisfies AttributeValue; + } as const satisfies JsxAttributeValue; } switch (attribute.type) { case T.JSXAttribute: diff --git a/packages/core/src/jsx/jsx-attribute.ts b/packages/core/src/jsx/jsx-attribute.ts index 07b3ca0823..9c8586570d 100644 --- a/packages/core/src/jsx/jsx-attribute.ts +++ b/packages/core/src/jsx/jsx-attribute.ts @@ -2,16 +2,21 @@ 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 { getAttributeName } from "./jsx-attribute-name"; +import { getJsxAttributeName } from "./jsx-attribute-name"; -export function getAttribute(context: RuleContext, attributes: AST.TSESTreeJSXAttributeLike[], initialScope?: Scope) { +export function getJsxAttribute( + context: RuleContext, + attributes: AST.TSESTreeJSXAttributeLike[], + initialScope?: Scope, +) { return (name: string) => { return attributes.findLast((attr) => { // Case 1: Direct JSX attribute (e.g., className="value") if (attr.type === T.JSXAttribute) { - return getAttributeName(context, attr) === name; + return getJsxAttributeName(context, attr) === name; } // For spread attributes, we need a scope to resolve variables if (initialScope == null) return false; @@ -33,3 +38,21 @@ export function getAttribute(context: RuleContext, attributes: AST.TSESTreeJSXAt }); }; } + +/** + * 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/core/src/jsx/jsx-detection.ts b/packages/core/src/jsx/jsx-detection.ts index 9670e1e8ca..b4addaa4a4 100644 --- a/packages/core/src/jsx/jsx-detection.ts +++ b/packages/core/src/jsx/jsx-detection.ts @@ -24,7 +24,7 @@ import type { TSESTree } from "@typescript-eslint/utils"; * BitFlags for configuring JSX detection behavior * Uses BigInt for bit operations to support many flags */ -export type JSXDetectionHint = bigint; +export type JsxDetectionHint = bigint; /* eslint-disable perfectionist/sort-objects */ /** @@ -32,7 +32,7 @@ export type JSXDetectionHint = bigint; * - Skip* flags: Ignore specific node types when detecting JSX * - Strict* flags: Enforce stricter rules for container types */ -export const JSXDetectionHint = { +export const JsxDetectionHint = { None: 0n, SkipUndefined: 1n << 0n, // Ignore undefined values SkipNullLiteral: 1n << 1n, // Ignore null literals @@ -53,8 +53,8 @@ export const JSXDetectionHint = { * Skips undefined and boolean literals (common in React) */ export const DEFAULT_JSX_DETECTION_HINT = 0n - | JSXDetectionHint.SkipUndefined - | JSXDetectionHint.SkipBooleanLiteral; + | JsxDetectionHint.SkipUndefined + | JsxDetectionHint.SkipBooleanLiteral; /** * Checks if a node is a `JSXText` or a `Literal` node @@ -79,7 +79,7 @@ export function isJsxText(node: TSESTree.Node | null | unit): node is TSESTree.J export function isJsxLike( code: { getScope: (node: TSESTree.Node) => Scope }, node: TSESTree.Node | unit | null, - hint: JSXDetectionHint = DEFAULT_JSX_DETECTION_HINT, + hint: JsxDetectionHint = DEFAULT_JSX_DETECTION_HINT, ): boolean { if (node == null) return false; @@ -91,30 +91,30 @@ export function isJsxLike( // Handle different literal types according to hint flags switch (typeof node.value) { case "boolean": - return !(hint & JSXDetectionHint.SkipBooleanLiteral); + return !(hint & JsxDetectionHint.SkipBooleanLiteral); case "string": - return !(hint & JSXDetectionHint.SkipStringLiteral); + return !(hint & JsxDetectionHint.SkipStringLiteral); case "number": - return !(hint & JSXDetectionHint.SkipNumberLiteral); + return !(hint & JsxDetectionHint.SkipNumberLiteral); case "bigint": - return !(hint & JSXDetectionHint.SkipBigIntLiteral); + return !(hint & JsxDetectionHint.SkipBigIntLiteral); } if (node.value == null) { - return !(hint & JSXDetectionHint.SkipNullLiteral); + return !(hint & JsxDetectionHint.SkipNullLiteral); } return false; } case T.TemplateLiteral: { // Template literals are treated like string literals - return !(hint & JSXDetectionHint.SkipStringLiteral); + return !(hint & JsxDetectionHint.SkipStringLiteral); } case T.ArrayExpression: { // Empty arrays can be filtered with SkipEmptyArray if (node.elements.length === 0) { - return !(hint & JSXDetectionHint.SkipEmptyArray); + return !(hint & JsxDetectionHint.SkipEmptyArray); } // StrictArray requires all elements to be JSX - if (hint & JSXDetectionHint.StrictArray) { + if (hint & JsxDetectionHint.StrictArray) { return node.elements.every((n) => isJsxLike(code, n, hint)); } // Default: array is JSX-like if any element is JSX-like @@ -122,7 +122,7 @@ export function isJsxLike( } case T.LogicalExpression: { // StrictLogical requires both sides to be JSX - if (hint & JSXDetectionHint.StrictLogical) { + if (hint & JsxDetectionHint.StrictLogical) { return isJsxLike(code, node.left, hint) && isJsxLike(code, node.right, hint); } // Default: logical expression is JSX-like if either side is JSX-like @@ -133,9 +133,9 @@ export function isJsxLike( function leftHasJSX(node: TSESTree.ConditionalExpression) { if (Array.isArray(node.consequent)) { if (node.consequent.length === 0) { - return !(hint & JSXDetectionHint.SkipEmptyArray); + return !(hint & JsxDetectionHint.SkipEmptyArray); } - if (hint & JSXDetectionHint.StrictArray) { + if (hint & JsxDetectionHint.StrictArray) { return node.consequent.every((n: TSESTree.Expression) => isJsxLike(code, n, hint)); } return node.consequent.some((n: TSESTree.Expression) => isJsxLike(code, n, hint)); @@ -149,7 +149,7 @@ export function isJsxLike( } // StrictConditional requires both branches to contain JSX - if (hint & JSXDetectionHint.StrictConditional) { + if (hint & JsxDetectionHint.StrictConditional) { return leftHasJSX(node) && rightHasJSX(node); } // Default: conditional is JSX-like if either branch has JSX @@ -162,7 +162,7 @@ export function isJsxLike( } case T.CallExpression: { // Skip createElement calls if configured to do so - if (hint & JSXDetectionHint.SkipCreateElement) { + if (hint & JsxDetectionHint.SkipCreateElement) { return false; } // Check for React.createElement or createElement calls @@ -178,7 +178,7 @@ export function isJsxLike( const { name } = node; // Handle 'undefined' identifier according to hint if (name === "undefined") { - return !(hint & JSXDetectionHint.SkipUndefined); + return !(hint & JsxDetectionHint.SkipUndefined); } // Check if this is a JSX tag name if (AST.isJSXTagNameExpression(node)) { diff --git a/packages/core/src/jsx/jsx-element-is.ts b/packages/core/src/jsx/jsx-element-is.ts index 69c1e0e3dc..c568e76e2a 100644 --- a/packages/core/src/jsx/jsx-element-is.ts +++ b/packages/core/src/jsx/jsx-element-is.ts @@ -1,7 +1,7 @@ import type { RuleContext } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import { getElementType } from "./jsx-element-type"; +import { getJsxElementType } from "./jsx-element-type"; /** * Determines if a JSX element is a host element @@ -11,7 +11,7 @@ import { getElementType } from "./jsx-element-type"; * @param node - AST node to check * @returns boolean indicating if the element is a host element */ -export function isHostElement(context: RuleContext, node: TSESTree.Node) { +export function isJsxHostElement(context: RuleContext, node: TSESTree.Node) { return node.type === T.JSXElement && node.openingElement.name.type === T.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name); @@ -25,9 +25,9 @@ export function isHostElement(context: RuleContext, node: TSESTree.Node) { * @param node - AST node to check * @returns boolean indicating if the element is a Fragment with type narrowing */ -export function isFragmentElement(context: RuleContext, node: TSESTree.Node): node is TSESTree.JSXElement { +export function isJsxFragmentElement(context: RuleContext, node: TSESTree.Node): node is TSESTree.JSXElement { if (node.type !== T.JSXElement) return false; - return getElementType(context, node) + return getJsxElementType(context, node) .split(".") .at(-1) === "Fragment"; } diff --git a/packages/core/src/jsx/jsx-element-type.ts b/packages/core/src/jsx/jsx-element-type.ts index 5909c9a0fa..09a209914c 100644 --- a/packages/core/src/jsx/jsx-element-type.ts +++ b/packages/core/src/jsx/jsx-element-type.ts @@ -12,7 +12,7 @@ import { stringifyJsx } from "./jsx-stringify"; * @param node - JSX element or fragment node * @returns String representation of the element type */ -export function getElementType(context: RuleContext, node: TSESTree.JSXElement | TSESTree.JSXFragment) { +export function getJsxElementType(context: RuleContext, node: TSESTree.JSXElement | TSESTree.JSXFragment) { if (node.type === T.JSXFragment) { return ""; } diff --git a/packages/core/src/jsx/jsx-has.ts b/packages/core/src/jsx/jsx-has.ts deleted file mode 100644 index d26cd8940a..0000000000 --- a/packages/core/src/jsx/jsx-has.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { RuleContext } from "@eslint-react/kit"; -import type { Scope } from "@typescript-eslint/scope-manager"; -import type { TSESTree } from "@typescript-eslint/types"; -import { getAttribute } from "./jsx-attribute"; - -/** - * 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 hasAttribute( - context: RuleContext, - name: string, - attributes: TSESTree.JSXOpeningElement["attributes"], - initialScope?: Scope, -) { - return getAttribute(context, attributes, initialScope)(name) != null; -} - -/** - * Checks if a JSX element has at least one of the specified attributes - * - * @param context - ESLint rule context - * @param names - Array of attribute names 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 any of the attributes exist - */ -export function hasAnyAttribute( - context: RuleContext, - names: string[], - attributes: TSESTree.JSXOpeningElement["attributes"], - initialScope?: Scope, -) { - return names.some((n) => hasAttribute(context, n, attributes, initialScope)); -} - -/** - * Checks if a JSX element has all of the specified attributes - * - * @param context - ESLint rule context - * @param names - Array of attribute names 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 all of the attributes exist - */ -export function hasEveryAttribute( - context: RuleContext, - names: string[], - attributes: TSESTree.JSXOpeningElement["attributes"], - initialScope?: Scope, -) { - return names.every((n) => hasAttribute(context, n, attributes, initialScope)); -} diff --git a/packages/core/src/jsx/jsx-hierarchy.ts b/packages/core/src/jsx/jsx-hierarchy.ts index 14f06e4719..bb3a94938a 100644 --- a/packages/core/src/jsx/jsx-hierarchy.ts +++ b/packages/core/src/jsx/jsx-hierarchy.ts @@ -12,7 +12,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; * Defaults to always returning true (matches any attribute) * @returns The first matching JSX attribute node found when traversing upwards, or undefined */ -export function findParentAttribute( +export function findParentJsxAttribute( node: TSESTree.Node, test: (node: TSESTree.JSXAttribute) => boolean = constTrue, ): TSESTree.JSXAttribute | unit { diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts index fab4e90917..7134226a9b 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts @@ -1,9 +1,9 @@ import { JsxEmit, - getElementType, getJsxConfigFromAnnotation, getJsxConfigFromContext, - isFragmentElement, + getJsxElementType, + isJsxFragmentElement, } from "@eslint-react/core"; import { flow } from "@eslint-react/eff"; import { type RuleContext, type RuleFeature, report } from "@eslint-react/kit"; @@ -53,10 +53,10 @@ export function create(context: RuleContext): RuleListener { data: { json: stringify({ kind: match(node) - .with({ type: T.JSXElement }, (n) => isFragmentElement(context, n) ? "fragment" : "element") + .with({ type: T.JSXElement }, (n) => isJsxFragmentElement(context, n) ? "fragment" : "element") .with({ type: T.JSXFragment }, () => "fragment") .exhaustive(), - type: getElementType(context, node), + type: getJsxElementType(context, node), jsx: match(jsxConfig.jsx) .with(JsxEmit.None, () => "none") .with(JsxEmit.ReactJSX, () => "react-jsx") 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 ef7d613e51..ec578e1891 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 @@ -1,6 +1,6 @@ -import { hasAttribute, isJsxText } from "@eslint-react/core"; +import { getJsxAttribute, isJsxText } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; +import { type TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; @@ -12,7 +12,6 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[]; export type MessageID = CamelCase; -// TODO: Use the information in `settings["react-x"].additionalComponents` to add support for user-defined components that use different properties to receive HTML and set them internally. export default createRule<[], MessageID>({ meta: { type: "problem", @@ -31,46 +30,69 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); -const dangerouslySetInnerHTML = "dangerouslySetInnerHTML"; +const DSIH = "dangerouslySetInnerHTML"; + +/** + * Checks if a JSX child node is considered significant (i.e., not just whitespace for formatting). + * @param node The JSX child node to check. + * @returns `true` if the node is significant, `false` otherwise. + */ +function isSignificantChildren(node: TSESTree.JSXElement["children"][number]): boolean { + if (!isJsxText(node)) { + return true; + } + // A JSXText node is insignificant if it's purely whitespace and contains a newline, + // which is a common pattern for formatting. + const isFormattingWhitespace = node.raw.trim() === "" && node.raw.includes("\n"); + + return !isFormattingWhitespace; +} + +/** + * Checks if a JSX element has children, either through the `children` prop or as JSX children. + * @param context The rule context. + * @param node The JSX element to check. + * @returns `true` if the element has children, `false` otherwise. + */ +function hasChildren(context: RuleContext, node: TSESTree.JSXElement): boolean { + const findJsxAttribute = getJsxAttribute( + context, + node.openingElement.attributes, + context.sourceCode.getScope(node), + ); + + if (findJsxAttribute("children") != null) { + return true; + } + + return node.children.some(isSignificantChildren); +} export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes(dangerouslySetInnerHTML)) return {}; + // Fast path: skip if `dangerouslySetInnerHTML` is not present in the file + if (!context.sourceCode.text.includes(DSIH)) { + return {}; + } + return { JSXElement(node) { - const attributes = node.openingElement.attributes; - const initialScope = context.sourceCode.getScope(node); - const hasChildren = node.children.some(isSignificantChildren) - || hasAttribute(context, "children", attributes, initialScope); - if (hasChildren && hasAttribute(context, dangerouslySetInnerHTML, attributes, initialScope)) { + const findJsxAttribute = getJsxAttribute( + context, + node.openingElement.attributes, + context.sourceCode.getScope(node), + ); + + const DSIHAttr = findJsxAttribute(DSIH); + if (DSIHAttr == null) { + return; + } + + if (hasChildren(context, node)) { context.report({ messageId: "noDangerouslySetInnerhtmlWithChildren", - node, + node: DSIHAttr, }); } }, }; } - -/** - * Check if a Literal or JSXText node is whitespace - * @param node The AST node to check - * @returns boolean `true` if the node is whitespace - */ -function isWhiteSpace(node: TSESTree.JSXText | TSESTree.Literal) { - return typeof node.value === "string" && node.raw.trim() === ""; -} - -/** - * Check if a Literal or JSXText node is padding spaces - * @param node The AST node to check - * @returns boolean - */ -function isPaddingSpaces(node: TSESTree.Node) { - return isJsxText(node) - && isWhiteSpace(node) - && node.raw.includes("\n"); -} - -function isSignificantChildren(node: TSESTree.JSXElement["children"][number]) { - return node.type !== AST_NODE_TYPES.JSXText || !isPaddingSpaces(node); -} 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 e478b63462..4f5ea2208e 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 @@ -1,4 +1,4 @@ -import { getAttribute } 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"; @@ -11,7 +11,8 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[]; export type MessageID = CamelCase; -// TODO: Use the information in `settings["react-x"].additionalComponents` to add support for user-defined components that use different properties to receive HTML and set them internally. +const DSIH = "dangerouslySetInnerHTML"; + export default createRule<[], MessageID>({ meta: { type: "problem", @@ -29,18 +30,21 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); -const dangerouslySetInnerHTML = "dangerouslySetInnerHTML"; - export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes(dangerouslySetInnerHTML)) return {}; + // Fast path: skip if `dangerouslySetInnerHTML` is not present in the file + if (!context.sourceCode.text.includes(DSIH)) return {}; return { JSXElement(node) { - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const attribute = getAttributeEx(dangerouslySetInnerHTML); - if (attribute == null) return; + const findJsxAttribute = getJsxAttribute( + context, + node.openingElement.attributes, + context.sourceCode.getScope(node), + ); + const attr = findJsxAttribute(DSIH); + if (attr == null) return; context.report({ messageId: "noDangerouslySetInnerhtml", - node: attribute, + node: attr, }); }, }; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts index f37cb31a65..02e69e77b2 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts @@ -31,6 +31,7 @@ export default createRule<[], MessageID>({ const findDOMNode = "findDOMNode"; export function create(context: RuleContext): RuleListener { + // Fast path: skip if `findDOMNode` is not present in the file if (!context.sourceCode.text.includes(findDOMNode)) return {}; return { CallExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts index d6a352e876..134a6a953a 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts @@ -31,6 +31,7 @@ export default createRule<[], MessageID>({ const flushSync = "flushSync"; export function create(context: RuleContext): RuleListener { + // Fast path: skip if `flushSync` is not present in the file if (!context.sourceCode.text.includes(flushSync)) return {}; return { CallExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts index 788b468d15..014b8c7004 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts @@ -37,6 +37,7 @@ export default createRule<[], MessageID>({ const hydrate = "hydrate"; export function create(context: RuleContext): RuleListener { + // Fast path: skip if `hydrate` is not present in the file if (!context.sourceCode.text.includes(hydrate)) return {}; const settings = getSettingsFromContext(context); if (compare(settings.version, "18.0.0", "<")) return {}; 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 1a9eaba589..c7e93172fb 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 @@ -1,6 +1,6 @@ -import { getAttribute } from "@eslint-react/core"; +import { getJsxAttribute } from "@eslint-react/core"; import type { RuleContext, RuleFeature, RuleSuggest } from "@eslint-react/kit"; -import type { RuleFixer, RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; import { createJsxElementResolver, createRule } from "../utils"; @@ -38,27 +38,32 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const resolver = createJsxElementResolver(context); + return { JSXElement(node) { - const { domElementType } = resolver.resolve(node); - if (domElementType !== "button") return; - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - if (getAttributeEx("type") != null) return; + if (resolver.resolve(node).domElementType !== "button") { + return; + } + + const findJsxAttribute = getJsxAttribute( + context, + node.openingElement.attributes, + context.sourceCode.getScope(node), + ); + + if (findJsxAttribute("type") != null) { + return; + } + context.report({ messageId: "noMissingButtonType", node: node.openingElement, - suggest: getSuggest((type) => (fixer: RuleFixer) => { - return fixer.insertTextAfter(node.openingElement.name, ` type="${type}"`); - }), + suggest: BUTTON_TYPES.map((type): RuleSuggest => ({ + messageId: "addButtonType", + data: { type }, + fix: (fixer) => fixer.insertTextAfter(node.openingElement.name, ` type="${type}"`), + })), }); }, }; } - -function getSuggest(getFix: (type: string) => RuleSuggest["fix"]): RuleSuggest[] { - return BUTTON_TYPES.map((type) => ({ - messageId: "addButtonType", - data: { type }, - fix: getFix(type), - })); -} 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 eb8d5c3eb9..ed6292fc1c 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 @@ -1,4 +1,4 @@ -import { getAttribute, resolveAttributeValue } from "@eslint-react/core"; +import { getJsxAttribute, resolveJsxAttributeValue } 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"; @@ -37,13 +37,24 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const resolver = createJsxElementResolver(context); + return { JSXElement(node) { const { domElementType } = resolver.resolve(node); + // If the element is not an iframe, we don't need to do anything. if (domElementType !== "iframe") return; - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const sandboxAttribute = getAttributeEx("sandbox"); - if (sandboxAttribute == null) { + + const findJsxAttribute = getJsxAttribute( + context, + node.openingElement.attributes, + context.sourceCode.getScope(node), + ); + + // Find the 'sandbox' attribute on the iframe element. + const sandboxAttr = findJsxAttribute("sandbox"); + + // If the 'sandbox' attribute is missing, report an error. + if (sandboxAttr == null) { context.report({ messageId: "noMissingIframeSandbox", node: node.openingElement, @@ -51,24 +62,32 @@ export function create(context: RuleContext): RuleListener { messageId: "addIframeSandbox", data: { value: "" }, fix(fixer) { + // Suggest adding a 'sandbox' attribute. return fixer.insertTextAfter(node.openingElement.name, ` sandbox=""`); }, }], }); return; } - const sandboxAttributeValue = resolveAttributeValue(context, sandboxAttribute); - if (typeof sandboxAttributeValue.toStatic("sandbox") === "string") return; + + // Resolve the value of the 'sandbox' attribute. + const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr); + // If the value is a static string, the attribute 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: sandboxAttributeValue.node ?? sandboxAttribute, + node: sandboxValue.node ?? sandboxAttr, suggest: [ { messageId: "addIframeSandbox", data: { value: "" }, fix(fixer) { - if (sandboxAttributeValue.kind.startsWith("spread")) return null; - return fixer.replaceText(sandboxAttribute, `sandbox=""`); + // 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=""`); }, }, ], diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts index 459cd88a3f..b184568513 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts @@ -1,4 +1,4 @@ -import { getElementType } from "@eslint-react/core"; +import { getJsxElementType } 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"; @@ -31,7 +31,7 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { return { JSXElement(node) { - const name = getElementType(context, node); + const name = getJsxElementType(context, node); if (typeof name !== "string" || !name.includes(":")) { return; } diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts index 5652d5f586..59cc6066d4 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts @@ -35,6 +35,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `render` is not present in the file if (!context.sourceCode.text.includes("render")) return {}; const settings = getSettingsFromContext(context); if (compare(settings.version, "18.0.0", "<")) return {}; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts index 2e63d1afff..6f99e72b08 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts @@ -1,4 +1,4 @@ -import { resolveAttributeValue } from "@eslint-react/core"; +import { resolveJsxAttributeValue } from "@eslint-react/core"; import { RE_JAVASCRIPT_PROTOCOL, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -33,7 +33,7 @@ export function create(context: RuleContext): RuleListener { return { JSXAttribute(node) { if (node.name.type !== T.JSXIdentifier || node.value == null) return; - const value = resolveAttributeValue(context, node).toStatic(); + const value = resolveJsxAttributeValue(context, node).toStatic(); if (typeof value === "string" && RE_JAVASCRIPT_PROTOCOL.test(value)) { context.report({ messageId: "noScriptUrl", 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 2871ef1707..dc6e01e5f8 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 @@ -1,4 +1,4 @@ -import { getAttribute, isHostElement, resolveAttributeValue } from "@eslint-react/core"; +import { getJsxAttribute, isJsxHostElement, resolveJsxAttributeValue } 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"; @@ -31,15 +31,33 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { return { JSXElement(node) { - if (!isHostElement(context, node)) return; - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const attribute = getAttributeEx("style"); - if (attribute == null) return; - const attributeValue = resolveAttributeValue(context, attribute); - if (typeof attributeValue.toStatic() === "string") { + // This rule only applies to host elements (e.g.,
, ), not custom components. + if (!isJsxHostElement(context, node)) { + 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) { + return; + } + + // Resolve the static value of the 'style' attribute. + const styleValue = resolveJsxAttributeValue(context, styleAttr); + const staticValue = styleValue.toStatic(); + + // If the resolved value is a string, report an error. + // e.g.,
+ if (typeof staticValue === "string") { context.report({ messageId: "noStringStyleProp", - node: attributeValue.node ?? attribute, + node: styleValue.node ?? styleAttr, }); } }, 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 11e9ed1dfb..32e82ced3a 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 @@ -1,4 +1,4 @@ -import { getAttribute, resolveAttributeValue } from "@eslint-react/core"; +import { getJsxAttribute, resolveJsxAttributeValue } 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"; @@ -11,15 +11,18 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[]; export type MessageID = CamelCase; -const unsafeSandboxValues = [ - ["allow-scripts", "allow-same-origin"], -] as const; +const UNSAFE_SANDBOX_VALUES = ["allow-scripts", "allow-same-origin"] as const; -function isSafeSandbox(value: unknown): value is string { - if (typeof value !== "string") return false; - return !unsafeSandboxValues.some((values) => { - return values.every((v) => value.includes(v)); - }); +/** + * Checks if the sandbox attribute value contains an unsafe combination. + * @param value The value of the sandbox attribute. + * @returns `true` if the value is a string and contains an unsafe combination, `false` otherwise. + */ +function isUnsafeSandboxCombination(value: unknown): value is string { + if (typeof value !== "string") { + return false; + } + return UNSAFE_SANDBOX_VALUES.every((v) => value.includes(v)); } export default createRule<[], MessageID>({ @@ -41,19 +44,29 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const resolver = createJsxElementResolver(context); + return { JSXElement(node) { - const { domElementType } = resolver.resolve(node); - if (domElementType !== "iframe") return; - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const sandboxAttribute = getAttributeEx("sandbox"); - if (sandboxAttribute == null) return; - const sandboxValue = resolveAttributeValue(context, sandboxAttribute); + 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) { + return; + } + + const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr); const sandboxValueStatic = sandboxValue.toStatic("sandbox"); - if (!isSafeSandbox(sandboxValueStatic)) { + + if (isUnsafeSandboxCombination(sandboxValueStatic)) { context.report({ messageId: "noUnsafeIframeSandbox", - node: sandboxValue.node ?? sandboxAttribute, + node: sandboxValue.node ?? sandboxAttr, }); } }, 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 d7e2320596..11f1afb4e0 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 @@ -1,4 +1,4 @@ -import { getAttribute, resolveAttributeValue } from "@eslint-react/core"; +import { getJsxAttribute, resolveJsxAttributeValue } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -71,28 +71,28 @@ export function create(context: RuleContext): RuleListener { if (domElementType !== "a") return; // Get access to the component attributes - const getAttributeEx = getAttribute( + const findAttribute = getJsxAttribute( context, node.openingElement.attributes, context.sourceCode.getScope(node), ); // Check if target="_blank" is present - const targetAttribute = getAttributeEx("target"); + const targetAttribute = findAttribute("target"); if (targetAttribute == null) return; - const targetAttributeValue = resolveAttributeValue(context, targetAttribute).toStatic("target"); + const targetAttributeValue = resolveJsxAttributeValue(context, targetAttribute).toStatic("target"); if (targetAttributeValue !== "_blank") return; // Check if href points to an external resource - const hrefAttribute = getAttributeEx("href"); + const hrefAttribute = findAttribute("href"); if (hrefAttribute == null) return; - const hrefAttributeValue = resolveAttributeValue(context, hrefAttribute).toStatic("href"); + const hrefAttributeValue = resolveJsxAttributeValue(context, hrefAttribute).toStatic("href"); if (!isExternalLinkLike(hrefAttributeValue)) return; // Check if rel attribute exists and is secure - const relAttribute = getAttributeEx("rel"); + const relAttribute = findAttribute("rel"); // No rel attribute case - suggest adding one if (relAttribute == null) { @@ -113,7 +113,7 @@ export function create(context: RuleContext): RuleListener { } // Check if existing rel attribute is secure - const relAttributeValue = resolveAttributeValue(context, relAttribute).toStatic("rel"); + const relAttributeValue = resolveJsxAttributeValue(context, relAttribute).toStatic("rel"); if (isSafeRel(relAttributeValue)) return; // Existing rel attribute is not secure - suggest replacing it diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.ts index f98c903232..b321aa72bb 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.ts @@ -35,6 +35,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `useFormState` is not present in the file if (!context.sourceCode.text.includes("useFormState")) return {}; const settings = getSettingsFromContext(context); if (compare(settings.version, "19.0.0", "<")) return {}; 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 ce50de4d6c..155d6fbed3 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 { hasAttribute } from "@eslint-react/core"; +import { hasJsxAttribute } 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"; @@ -30,7 +30,6 @@ const voidElements = new Set([ "wbr", ]); -// TODO: Use the information in `settings["react-x"].additionalComponents` to add support for user-defined components that use the void element internally export default createRule<[], MessageID>({ meta: { type: "problem", @@ -50,26 +49,21 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const resolver = createJsxElementResolver(context); + return { JSXElement(node) { const { domElementType: elementName } = resolver.resolve(node); - if (elementName.length === 0 || !voidElements.has(elementName)) { + if (!voidElements.has(elementName)) { return; } - if (node.children.length > 0) { - context.report({ - messageId: "noVoidElementsWithChildren", - node, - data: { - element: elementName, - }, - }); - } + const { attributes } = node.openingElement; const initialScope = context.sourceCode.getScope(node); - const hasAttributeEx = (name: string) => hasAttribute(context, name, attributes, initialScope); - if (hasAttributeEx("children") || hasAttributeEx("dangerouslySetInnerHTML")) { - // e.g.
+ + const hasChildrenProp = hasJsxAttribute(context, "children", attributes, initialScope); + const hasDangerouslySetInnerHTML = hasJsxAttribute(context, "dangerouslySetInnerHTML", attributes, initialScope); + + if (node.children.length > 0 || hasChildrenProp || hasDangerouslySetInnerHTML) { context.report({ messageId: "noVoidElementsWithChildren", node, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts index 5c7f2f6f55..5ef2292126 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts @@ -60,9 +60,9 @@ export function create(context: RuleContext): RuleListener { `${importStringPrefix} * as ${node.local.name} from ${importSourceQuoted}${semi}`, ); } - // dprint-ignore // remove the default specifier and prepend the namespace import specifier - const specifiers = importDeclarationText.slice(importDeclarationText.indexOf("{"), importDeclarationText.indexOf("}") + 1); + const specifiers = importDeclarationText + .slice(importDeclarationText.indexOf("{"), importDeclarationText.indexOf("}") + 1); return fixer.replaceText( node.parent, [ 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 6628e068fe..7d6fc2680d 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 @@ -1,4 +1,4 @@ -import { getAttribute, getElementType, resolveAttributeValue } from "@eslint-react/core"; +import { getJsxAttribute, getJsxElementType, resolveJsxAttributeValue } from "@eslint-react/core"; import type { RuleContext } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; import type { TSESTree } from "@typescript-eslint/types"; @@ -26,7 +26,7 @@ export function createJsxElementResolver(context: RuleContext) { */ resolve(node: TSESTree.JSXElement) { // Get the element name/type (e.g., 'div', 'Button', etc.) - const elementName = getElementType(context, node); + const elementName = getJsxElementType(context, node); // Create the base result with element types const result = { @@ -42,12 +42,12 @@ export function createJsxElementResolver(context: RuleContext) { } // Look for the polymorphic prop (e.g., 'as', 'component') in the element's attributes - const getAttributeEx = getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); - const polymorphicProp = getAttributeEx(polymorphicPropName); + const findAttribute = getJsxAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node)); + const polymorphicProp = findAttribute(polymorphicPropName); // If the polymorphic prop exists, try to determine its static value if (polymorphicProp != null) { - const polymorphicPropValue = resolveAttributeValue(context, polymorphicProp); + const polymorphicPropValue = resolveJsxAttributeValue(context, polymorphicProp); const staticValue = polymorphicPropValue.toStatic(polymorphicPropName); // If we have a string value, use it as the DOM element type diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts index bde4d9a237..ffc6a13bd0 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts @@ -53,6 +53,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `useEffect` like symbols are not present in the file if (!/use\w*Effect/u.test(context.sourceCode.text)) return {}; const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = []; diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts index 9247774c7d..4ba5882930 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts @@ -30,6 +30,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `createContext` is not present in the file if (!context.sourceCode.text.includes("createContext")) return {}; return { CallExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts index 9377e9a2de..8aad4a56c2 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts @@ -173,6 +173,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `addEventListener` is not present in the file if (!context.sourceCode.text.includes("addEventListener")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts index 4b3b87c58f..02ba743b35 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts @@ -80,6 +80,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `setInterval` is not present in the file if (!context.sourceCode.text.includes("setInterval")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts index 853520e995..121acadd58 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts @@ -112,6 +112,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `ResizeObserver` is not present in the file if (!context.sourceCode.text.includes("ResizeObserver")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts index 1065f2a85f..bbc7c0ab87 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts @@ -80,6 +80,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `setTimeout` is not present in the file if (!context.sourceCode.text.includes("setTimeout")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts index cd1300d78d..6cf46d9271 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts @@ -1,4 +1,4 @@ -import { getAttributeName } from "@eslint-react/core"; +import { getJsxAttributeName } from "@eslint-react/core"; import type { unit } from "@eslint-react/eff"; import type { RuleContext, RuleFeature, RulePolicy } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; @@ -55,7 +55,7 @@ export function create(context: RuleContext): RuleListener { return { JSXAttribute(node: TSESTree.JSXAttribute) { const { value } = node; - const propName = getAttributeName(context, node); + const propName = getJsxAttributeName(context, node); switch (true) { case policy === 1 diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts index 0870e51f9f..b7361355f8 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts @@ -1,4 +1,4 @@ -import { getJsxConfigFromAnnotation, getJsxConfigFromContext, isFragmentElement } from "@eslint-react/core"; +import { getJsxConfigFromAnnotation, getJsxConfigFromContext, isJsxFragmentElement } from "@eslint-react/core"; import type { unit } from "@eslint-react/eff"; import { type RuleContext, type RuleFeature, type RulePolicy } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; @@ -62,7 +62,7 @@ export function create(context: RuleContext): RuleListener { return match(policy) .with(1, () => ({ JSXElement(node: TSESTree.JSXElement) { - if (!isFragmentElement(context, node)) return; + if (!isJsxFragmentElement(context, node)) return; const hasAttributes = node.openingElement.attributes.length > 0; if (hasAttributes) return; context.report({ diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts index d7822407fd..872ab286a5 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts @@ -49,6 +49,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `setState` is not present in the file if (!context.sourceCode.text.includes("setState")) { return {}; } 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 3bea6e5bbb..9440aec6fd 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 @@ -1,4 +1,4 @@ -import { getAttribute } 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"; @@ -31,12 +31,12 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { return { JSXElement(node) { - const getAttributeEx = getAttribute( + const findAttribute = getJsxAttribute( context, node.openingElement.attributes, context.sourceCode.getScope(node), ); - const childrenProp = getAttributeEx("children"); + const childrenProp = findAttribute("children"); if (childrenProp != null) { context.report({ messageId: "noChildrenProp", diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-class-component.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-class-component.ts index bf8fca0c56..e980c9fafa 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-class-component.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-class-component.ts @@ -29,6 +29,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `Component` is not present in the file if (!context.sourceCode.text.includes("Component")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); return { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-mount.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-mount.ts index d5302c412d..2abdcbe952 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-mount.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-mount.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentWillMount` is not present in the file if (!context.sourceCode.text.includes("componentWillMount")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-receive-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-receive-props.ts index 92e2c36d90..9c56d72497 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-receive-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-receive-props.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentWillReceiveProps` is not present in the file if (!context.sourceCode.text.includes("componentWillReceiveProps")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); return { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-update.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-update.ts index 12c8b0a38f..a403ef3979 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-update.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-component-will-update.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentWillUpdate` is not present in the file if (!context.sourceCode.text.includes("componentWillUpdate")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts index 35da096c54..14879b27bc 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts @@ -1,4 +1,4 @@ -import { getElementType, isComponentNameLoose } from "@eslint-react/core"; +import { getJsxElementType, isComponentNameLoose } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -34,12 +34,13 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `Provider` is not present in the file if (!context.sourceCode.text.includes("Provider")) return {}; const { version } = getSettingsFromContext(context); if (compare(version, "19.0.0", "<")) return {}; return { JSXElement(node) { - const fullName = getElementType(context, node); + const fullName = getJsxElementType(context, node); const parts = fullName.split("."); const selfName = parts.pop(); const contextFullName = parts.join("."); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts index 260370798c..98f61d0e7f 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `defaultProps` is not present in the file if (!context.sourceCode.text.includes("defaultProps")) return {}; return { AssignmentExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts index e66ff14144..e78425a800 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts @@ -38,6 +38,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `forwardRef` is not present in the file if (!context.sourceCode.text.includes("forwardRef")) { return {}; } 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 05e1e4e5b5..9933aa83e1 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 @@ -1,4 +1,4 @@ -import { getAttribute } from "@eslint-react/core"; +import { getJsxAttribute } from "@eslint-react/core"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -37,7 +37,7 @@ export function create(context: RuleContext): RuleListener { return { JSXOpeningElement(node: TSESTree.JSXOpeningElement) { const initialScope = context.sourceCode.getScope(node); - const keyProp = getAttribute(context, node.attributes, initialScope)("key"); + const keyProp = getJsxAttribute(context, node.attributes, initialScope)("key"); const isKeyPropOnElement = node.attributes .some((n) => n.type === T.JSXAttribute diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts index fc4c4d5d58..9a3626e1ce 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts @@ -43,6 +43,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `&&` is not present in the file if (!context.sourceCode.text.includes("&&")) return {}; const { version } = getSettingsFromContext(context); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts index 616e99f360..e0b810782e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts @@ -30,6 +30,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `memo` or `forwardRef` is not present in the file if (!context.sourceCode.text.includes("memo") && !context.sourceCode.text.includes("forwardRef")) return {}; const { ctx, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts index 3c28953dc3..413a0c3449 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts @@ -35,6 +35,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `createContext` is not present in the file if (!context.sourceCode.text.includes("createContext")) return {}; // `React.createContext` calls const createCalls: TSESTree.CallExpression[] = []; 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 4a64847b49..249790a838 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 { hasAttribute, isChildrenToArrayCall } from "@eslint-react/core"; +import { hasJsxAttribute, 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"; @@ -41,7 +41,7 @@ export function create(context: RuleContext): RuleListener { switch (node.type) { case T.JSXElement: { const initialScope = context.sourceCode.getScope(node); - if (!hasAttribute(context, "key", node.openingElement.attributes, initialScope)) { + if (!hasJsxAttribute(context, "key", node.openingElement.attributes, initialScope)) { return { messageId: "missingKey", node, @@ -101,7 +101,7 @@ export function create(context: RuleContext): RuleListener { } const initialScope = context.sourceCode.getScope(node); for (const element of elements) { - if (!hasAttribute(context, "key", element.openingElement.attributes, initialScope)) { + if (!hasJsxAttribute(context, "key", element.openingElement.attributes, initialScope)) { context.report({ messageId: "missingKey", node: element, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts index c9c0c9f57e..6a143850cf 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts @@ -40,6 +40,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `captureOwnerStack` is not present in the file if (!context.sourceCode.text.includes("captureOwnerStack")) return {}; const { importSource } = getSettingsFromContext(context); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-component-definitions.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-component-definitions.ts index e06000bf3e..f3418f09d0 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-component-definitions.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-component-definitions.ts @@ -1,7 +1,7 @@ import * as AST from "@eslint-react/ast"; import { ComponentDetectionHint, - findParentAttribute, + findParentJsxAttribute, isClassComponent, isCreateElementCall, isDeclaredInRenderPropLoose, @@ -162,7 +162,7 @@ export function create(context: RuleContext): RuleListener { */ function isInsideJSXAttributeValue(node: AST.TSESTreeFunction) { return node.parent.type === T.JSXAttribute - || findParentAttribute(node, (n) => n.value?.type === T.JSXExpressionContainer) != null; + || findParentJsxAttribute(node, (n) => n.value?.type === T.JSXExpressionContainer) != null; } /** diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts index aaf42acbc6..e16f8558ea 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts @@ -33,6 +33,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `propTypes` is not present in the file if (!context.sourceCode.text.includes("propTypes")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-redundant-should-component-update.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-redundant-should-component-update.ts index 5a3fe0719c..8f0920aa48 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-redundant-should-component-update.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-redundant-should-component-update.ts @@ -39,6 +39,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `shouldComponentUpdate` is not present in the file if (!context.sourceCode.text.includes("shouldComponentUpdate")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-mount.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-mount.ts index 66cc64f1dd..75d2c5b29b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-mount.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-mount.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentDidMount` is not present in the file if (!context.sourceCode.text.includes("componentDidMount")) return {}; return { CallExpression(node: TSESTree.CallExpression) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-update.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-update.ts index 2e5e71df70..44c4f3a40f 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-update.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-did-update.ts @@ -32,6 +32,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentWillUpdate` is not present in the file if (!context.sourceCode.text.includes("componentDidUpdate")) return {}; return { CallExpression(node: TSESTree.CallExpression) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-will-update.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-will-update.ts index 5556cbec1c..348b0445c2 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-will-update.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-set-state-in-component-will-update.ts @@ -33,6 +33,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `componentWillUpdate` is not present in the file if (!context.sourceCode.text.includes("componentWillUpdate")) return {}; return { CallExpression(node: TSESTree.CallExpression) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.ts index f274dd5858..2f5c08f10c 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.ts @@ -35,6 +35,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `key=` is not present in the file if (!context.sourceCode.text.includes("key=")) return {}; return { JSXAttribute(node: TSESTree.JSXAttribute) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-callback.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-callback.ts index 6a531800ac..c868e81596 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-callback.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-callback.ts @@ -37,6 +37,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `useCallback` is not present in the file if (!context.sourceCode.text.includes("useCallback")) return {}; return { CallExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.ts index d1775bb08f..0e974a7d37 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.ts @@ -36,6 +36,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `useMemo` is not present in the file if (!context.sourceCode.text.includes("useMemo")) return {}; return { CallExpression(node) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-mount.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-mount.ts index 59c897bc12..2ad88a7e99 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-mount.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-mount.ts @@ -29,6 +29,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `UNSAFE_componentWillMount` is not present in the file if (!context.sourceCode.text.includes("UNSAFE_componentWillMount")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-receive-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-receive-props.ts index 3983126ce3..d2edbf55b6 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-receive-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-receive-props.ts @@ -29,6 +29,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `UNSAFE_componentWillReceiveProps` is not present in the file if (!context.sourceCode.text.includes("UNSAFE_componentWillReceiveProps")) { return {}; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-update.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-update.ts index fe292a7af7..889d1bf45b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-update.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unsafe-component-will-update.ts @@ -29,6 +29,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `UNSAFE_componentWillUpdate` is not present in the file if (!context.sourceCode.text.includes("UNSAFE_componentWillUpdate")) return {}; const { ctx, listeners } = useComponentCollectorLegacy(); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.ts index 00eaaf109c..ba764475e0 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { getElementType, isReactHookCall, useComponentCollector } from "@eslint-react/core"; +import { getJsxElementType, isReactHookCall, useComponentCollector } from "@eslint-react/core"; import { getOrElseUpdate } from "@eslint-react/eff"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; @@ -44,7 +44,7 @@ export function create(context: RuleContext): RuleListener { return { ...listeners, JSXOpeningElement(node) { - const fullName = getElementType(context, node.parent); + const fullName = getJsxElementType(context, node.parent); const selfName = fullName.split(".").at(-1); if (selfName == null) return; if (!isContextName(selfName, isReact18OrBelow)) return; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts index bd92cdf60d..56644acd7b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts @@ -37,6 +37,7 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + // Fast path: skip if `useContext` is not present in the file if (!context.sourceCode.text.includes("useContext")) return {}; const settings = getSettingsFromContext(context); if (compare(settings.version, "19.0.0", "<")) { 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 4f2cde0960..530df65b03 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 { hasAttribute, isFragmentElement, isHostElement, isJsxText } from "@eslint-react/core"; +import { hasJsxAttribute, 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"; @@ -60,7 +60,7 @@ export function create(context: RuleContext, [option]: Optio return { // Check JSX elements that might be fragments JSXElement(node) { - if (!isFragmentElement(context, node)) return; + if (!isJsxFragmentElement(context, node)) return; checkNode(context, node, allowExpressions); }, // Check JSX fragments @@ -112,12 +112,12 @@ function checkNode( 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 && hasAttribute(context, "key", node.openingElement.attributes, initialScope)) { + if (node.type === T.JSXElement && hasJsxAttribute(context, "key", node.openingElement.attributes, initialScope)) { return; } // Report fragment placed inside a host component (e.g.
<>
) - if (isHostElement(context, node.parent)) { + if (isJsxHostElement(context, node.parent)) { context.report({ messageId: "uselessFragment", node, @@ -217,7 +217,7 @@ function getFix(context: RuleContext, node: TSESTree.JSXElement | TSESTree.JSXFr function canFix(context: RuleContext, node: TSESTree.JSXElement | TSESTree.JSXFragment) { // Don't fix fragments inside custom components (might require children to be ReactElement) if (node.parent.type === T.JSXElement || node.parent.type === T.JSXFragment) { - return isHostElement(context, node.parent); + return isJsxHostElement(context, node.parent); } // Don't fix empty fragments without a JSX parent diff --git a/packages/plugins/eslint-plugin/README.md b/packages/plugins/eslint-plugin/README.md index 8290a07657..25edcffe33 100644 --- a/packages/plugins/eslint-plugin/README.md +++ b/packages/plugins/eslint-plugin/README.md @@ -169,8 +169,8 @@ export default defineConfig([ Contributions are welcome! -Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/main/.github/CONTRIBUTING.md). +Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/code-optimization-20/.github/CONTRIBUTING.md). ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/main/LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/code-optimization-20/LICENSE) file for details.