diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index a0d9e2061..ca6c4d5d3 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -26,7 +26,6 @@ - [ComponentKind](type-aliases/ComponentKind.md) - [ComponentLifecyclePhaseKind](type-aliases/ComponentLifecyclePhaseKind.md) - [ComponentPhaseKind](type-aliases/ComponentPhaseKind.md) -- [ComponentStateKind](type-aliases/ComponentStateKind.md) - [JsxAttributeValue](type-aliases/JsxAttributeValue.md) - [JsxDetectionHint](type-aliases/JsxDetectionHint.md) @@ -118,8 +117,6 @@ - [getJsxElementType](functions/getJsxElementType.md) - [getPhaseKindOfFunction](functions/getPhaseKindOfFunction.md) - [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md) -- [isAssignmentToThisState](functions/isAssignmentToThisState.md) -- [isChildrenOfCreateElement](functions/isChildrenOfCreateElement.md) - [isClassComponent](functions/isClassComponent.md) - [isComponentDefinition](functions/isComponentDefinition.md) - [isComponentName](functions/isComponentName.md) @@ -129,7 +126,6 @@ - [isDeclaredInRenderPropLoose](functions/isDeclaredInRenderPropLoose.md) - [isFunctionOfComponentDidMount](functions/isFunctionOfComponentDidMount.md) - [isFunctionOfComponentWillUnmount](functions/isFunctionOfComponentWillUnmount.md) -- [isFunctionOfRenderMethod](functions/isFunctionOfRenderMethod.md) - [isFunctionOfUseEffectCleanup](functions/isFunctionOfUseEffectCleanup.md) - [isFunctionOfUseEffectSetup](functions/isFunctionOfUseEffectSetup.md) - [isInitializedFromReact](functions/isInitializedFromReact.md) @@ -149,7 +145,6 @@ - [isRenderFunctionLoose](functions/isRenderFunctionLoose.md) - [isRenderMethodLike](functions/isRenderMethodLike.md) - [isRenderPropLoose](functions/isRenderPropLoose.md) -- [isThisSetState](functions/isThisSetState.md) - [isUseEffectLikeCall](functions/isUseEffectLikeCall.md) - [resolveJsxAttributeValue](functions/resolveJsxAttributeValue.md) - [stringifyJsx](functions/stringifyJsx.md) diff --git a/packages/core/docs/functions/getComponentNameFromId.md b/packages/core/docs/functions/getComponentNameFromId.md index bd08b82f2..b89c1efe6 100644 --- a/packages/core/docs/functions/getComponentNameFromId.md +++ b/packages/core/docs/functions/getComponentNameFromId.md @@ -6,11 +6,13 @@ function getComponentNameFromId(id: Identifier | Identifier[] | undefined): string | undefined; ``` +Get component name from an identifier or identifier sequence (e.g., MemberExpression) + ## Parameters -| Parameter | Type | -| ------ | ------ | -| `id` | `Identifier` \| `Identifier`[] \| `undefined` | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `id` | `Identifier` \| `Identifier`[] \| `undefined` | The identifier or identifier sequence | ## Returns diff --git a/packages/core/docs/functions/hasNoneOrLooseComponentName.md b/packages/core/docs/functions/hasNoneOrLooseComponentName.md index e0a863348..c2a3f2e93 100644 --- a/packages/core/docs/functions/hasNoneOrLooseComponentName.md +++ b/packages/core/docs/functions/hasNoneOrLooseComponentName.md @@ -6,12 +6,14 @@ function hasNoneOrLooseComponentName(context: RuleContext, fn: TSESTreeFunction): boolean; ``` +Check if the function has no name or a loose component name + ## Parameters -| Parameter | Type | -| ------ | ------ | -| `context` | `RuleContext` | -| `fn` | `TSESTreeFunction` | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `context` | `RuleContext` | The rule context | +| `fn` | `TSESTreeFunction` | The function node | ## Returns diff --git a/packages/core/docs/functions/isAssignmentToThisState.md b/packages/core/docs/functions/isAssignmentToThisState.md deleted file mode 100644 index 3287b992d..000000000 --- a/packages/core/docs/functions/isAssignmentToThisState.md +++ /dev/null @@ -1,17 +0,0 @@ -[@eslint-react/core](../README.md) / isAssignmentToThisState - -# Function: isAssignmentToThisState() - -```ts -function isAssignmentToThisState(node: AssignmentExpression): boolean; -``` - -## Parameters - -| Parameter | Type | -| ------ | ------ | -| `node` | `AssignmentExpression` | - -## Returns - -`boolean` diff --git a/packages/core/docs/functions/isChildrenOfCreateElement.md b/packages/core/docs/functions/isChildrenOfCreateElement.md deleted file mode 100644 index b8a2f6c8b..000000000 --- a/packages/core/docs/functions/isChildrenOfCreateElement.md +++ /dev/null @@ -1,22 +0,0 @@ -[@eslint-react/core](../README.md) / isChildrenOfCreateElement - -# Function: isChildrenOfCreateElement() - -```ts -function isChildrenOfCreateElement(context: RuleContext, node: Node): boolean; -``` - -Determines whether inside `createElement`'s children. - -## Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `context` | `RuleContext` | The rule context | -| `node` | `Node` | The AST node to check | - -## Returns - -`boolean` - -`true` if the node is inside createElement's children diff --git a/packages/core/docs/functions/isComponentDefinition.md b/packages/core/docs/functions/isComponentDefinition.md index d8712a477..74ded85d6 100644 --- a/packages/core/docs/functions/isComponentDefinition.md +++ b/packages/core/docs/functions/isComponentDefinition.md @@ -16,11 +16,11 @@ Determines if a function node represents a valid React component definition | Parameter | Type | Description | | ------ | ------ | ------ | | `context` | `RuleContext` | The rule context | -| `node` | `TSESTreeFunction` | The function node to check | -| `hint` | `bigint` | Component detection hints as bit flags | +| `node` | `TSESTreeFunction` | The function node to analyze | +| `hint` | `bigint` | Component detection hints (bit flags) to customize detection logic | ## Returns `boolean` -`true` if the node is a valid component definition, `false` otherwise +`true` if the node is considered a component definition diff --git a/packages/core/docs/functions/isComponentName.md b/packages/core/docs/functions/isComponentName.md index 24b95e86b..b4a6bec63 100644 --- a/packages/core/docs/functions/isComponentName.md +++ b/packages/core/docs/functions/isComponentName.md @@ -6,11 +6,13 @@ function isComponentName(name: string): boolean; ``` +Check if a string matches the strict component name pattern + ## Parameters -| Parameter | Type | -| ------ | ------ | -| `name` | `string` | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `name` | `string` | The name to check | ## Returns diff --git a/packages/core/docs/functions/isComponentNameLoose.md b/packages/core/docs/functions/isComponentNameLoose.md index cfcbd157e..7f53a436a 100644 --- a/packages/core/docs/functions/isComponentNameLoose.md +++ b/packages/core/docs/functions/isComponentNameLoose.md @@ -6,11 +6,13 @@ function isComponentNameLoose(name: string): boolean; ``` +Check if a string matches the loose component name pattern + ## Parameters -| Parameter | Type | -| ------ | ------ | -| `name` | `string` | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `name` | `string` | The name to check | ## Returns diff --git a/packages/core/docs/functions/isFunctionOfRenderMethod.md b/packages/core/docs/functions/isFunctionOfRenderMethod.md deleted file mode 100644 index a2673bee9..000000000 --- a/packages/core/docs/functions/isFunctionOfRenderMethod.md +++ /dev/null @@ -1,30 +0,0 @@ -[@eslint-react/core](../README.md) / isFunctionOfRenderMethod - -# Function: isFunctionOfRenderMethod() - -```ts -function isFunctionOfRenderMethod(node: TSESTreeFunction): boolean; -``` - -Check whether given node is a function of a render method of a class component - -## Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `node` | `TSESTreeFunction` | The AST node to check | - -## Returns - -`boolean` - -`true` if node is a render function, `false` if not - -## Example - -```tsx -class Component extends React.Component { - renderHeader = () =>
; - renderFooter = () => ; -} -``` diff --git a/packages/core/docs/functions/isThisSetState.md b/packages/core/docs/functions/isThisSetState.md deleted file mode 100644 index b28b320ae..000000000 --- a/packages/core/docs/functions/isThisSetState.md +++ /dev/null @@ -1,17 +0,0 @@ -[@eslint-react/core](../README.md) / isThisSetState - -# Function: isThisSetState() - -```ts -function isThisSetState(node: CallExpression): boolean; -``` - -## Parameters - -| Parameter | Type | -| ------ | ------ | -| `node` | `CallExpression` | - -## Returns - -`boolean` diff --git a/packages/core/docs/functions/stringifyJsx.md b/packages/core/docs/functions/stringifyJsx.md index ba4b14ee1..f081230f1 100644 --- a/packages/core/docs/functions/stringifyJsx.md +++ b/packages/core/docs/functions/stringifyJsx.md @@ -4,13 +4,13 @@ ```ts function stringifyJsx(node: - | JSXClosingElement - | JSXClosingFragment | JSXIdentifier - | JSXMemberExpression | JSXNamespacedName + | JSXMemberExpression | JSXOpeningElement + | JSXClosingElement | JSXOpeningFragment + | JSXClosingFragment | JSXText): string; ``` @@ -20,7 +20,7 @@ Incomplete but sufficient stringification of JSX nodes for common use cases | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | \| `JSXClosingElement` \| `JSXClosingFragment` \| `JSXIdentifier` \| `JSXMemberExpression` \| `JSXNamespacedName` \| `JSXOpeningElement` \| `JSXOpeningFragment` \| `JSXText` | JSX node from TypeScript ESTree | +| `node` | \| `JSXIdentifier` \| `JSXNamespacedName` \| `JSXMemberExpression` \| `JSXOpeningElement` \| `JSXClosingElement` \| `JSXOpeningFragment` \| `JSXClosingFragment` \| `JSXText` | JSX node from TypeScript ESTree | ## Returns diff --git a/packages/core/docs/interfaces/ClassComponent.md b/packages/core/docs/interfaces/ClassComponent.md index 14a35cf2f..213558ac4 100644 --- a/packages/core/docs/interfaces/ClassComponent.md +++ b/packages/core/docs/interfaces/ClassComponent.md @@ -2,20 +2,22 @@ # Interface: ClassComponent +Represents a React class component + ## Extends - [`SemanticNode`](SemanticNode.md) ## Properties -| Property | Type | Overrides | Inherited from | -| ------ | ------ | ------ | ------ | -| `displayName` | `Expression` \| `undefined` | - | - | -| `flag` | `bigint` | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) | - | -| `hint` | `bigint` | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) | - | -| `id` | `Identifier` \| `undefined` | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - | -| `key` | `string` | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) | -| `kind` | `"class"` | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) | - | -| `methods` | `TSESTreeMethodOrProperty`[] | - | - | -| `name` | `string` \| `undefined` | - | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | -| `node` | `TSESTreeClass` | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - | +| Property | Type | Description | Overrides | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `displayName` | `Expression` \| `undefined` | The display name of the component | - | - | +| `flag` | `bigint` | Flags describing the component's characteristics | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) | - | +| `hint` | `bigint` | Hint for how the component was detected | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) | - | +| `id` | `Identifier` \| `undefined` | The identifier of the component | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - | +| `key` | `string` | - | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) | +| `kind` | `"class"` | The kind of component | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) | - | +| `methods` | `TSESTreeMethodOrProperty`[] | List of methods and properties in the class | - | - | +| `name` | `string` \| `undefined` | - | - | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | +| `node` | `TSESTreeClass` | The AST node of the class | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - | diff --git a/packages/core/docs/interfaces/FunctionComponent.md b/packages/core/docs/interfaces/FunctionComponent.md index 0c98b037b..06cd81424 100644 --- a/packages/core/docs/interfaces/FunctionComponent.md +++ b/packages/core/docs/interfaces/FunctionComponent.md @@ -2,21 +2,23 @@ # Interface: FunctionComponent +Represents a React function component + ## Extends - [`SemanticNode`](SemanticNode.md) ## Properties -| Property | Type | Overrides | Inherited from | -| ------ | ------ | ------ | ------ | -| `displayName` | `Expression` \| `undefined` | - | - | -| `flag` | `bigint` | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) | - | -| `hint` | `bigint` | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) | - | -| `hookCalls` | `CallExpression`[] | - | - | -| `id` | `Identifier` \| `Identifier`[] \| `undefined` | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - | -| `initPath` | `FunctionInitPath` \| `undefined` | - | - | -| `key` | `string` | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) | -| `kind` | `"function"` | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) | - | -| `name` | `string` \| `undefined` | - | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | -| `node` | `TSESTreeFunction` | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - | +| Property | Type | Description | Overrides | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `displayName` | `Expression` \| `undefined` | The display name of the component | - | - | +| `flag` | `bigint` | Flags describing the component's characteristics | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) | - | +| `hint` | `bigint` | Hint for how the component was detected | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) | - | +| `hookCalls` | `CallExpression`[] | List of hook calls within the component | - | - | +| `id` | `Identifier` \| `Identifier`[] \| `undefined` | The identifier or identifier sequence of the component | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - | +| `initPath` | `FunctionInitPath` \| `undefined` | The initialization path of the function | - | - | +| `key` | `string` | - | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) | +| `kind` | `"function"` | The kind of component | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) | - | +| `name` | `string` \| `undefined` | - | - | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | +| `node` | `TSESTreeFunction` | The AST node of the function | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - | diff --git a/packages/core/docs/type-aliases/Component.md b/packages/core/docs/type-aliases/Component.md index 4e66b4e7c..348c87cd7 100644 --- a/packages/core/docs/type-aliases/Component.md +++ b/packages/core/docs/type-aliases/Component.md @@ -7,3 +7,5 @@ type Component = | ClassComponent | FunctionComponent; ``` + +Union type representing either a class or function component diff --git a/packages/core/docs/type-aliases/ComponentStateKind.md b/packages/core/docs/type-aliases/ComponentStateKind.md deleted file mode 100644 index 352c40c72..000000000 --- a/packages/core/docs/type-aliases/ComponentStateKind.md +++ /dev/null @@ -1,7 +0,0 @@ -[@eslint-react/core](../README.md) / ComponentStateKind - -# Type Alias: ComponentStateKind - -```ts -type ComponentStateKind = "actionState" | "state"; -``` diff --git a/packages/core/docs/variables/ComponentFlag.md b/packages/core/docs/variables/ComponentFlag.md index 15b6e0904..5c49b848a 100644 --- a/packages/core/docs/variables/ComponentFlag.md +++ b/packages/core/docs/variables/ComponentFlag.md @@ -15,11 +15,11 @@ ComponentFlag: { ## Type Declaration -| Name | Type | Default value | -| ------ | ------ | ------ | -| `Async` | `bigint` | - | -| `CreateElement` | `bigint` | - | -| `ForwardRef` | `bigint` | - | -| `Memo` | `bigint` | - | -| `None` | `bigint` | `0n` | -| `PureComponent` | `bigint` | - | +| Name | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `Async` | `bigint` | - | Indicates the component is asynchronous | +| `CreateElement` | `bigint` | - | Indicates the component creates elements using `createElement` instead of JSX | +| `ForwardRef` | `bigint` | - | Indicates the component forwards a ref (e.g. React.forwardRef) | +| `Memo` | `bigint` | - | Indicates the component is memoized (e.g. React.memo) | +| `None` | `bigint` | `0n` | No flags set | +| `PureComponent` | `bigint` | - | Indicates the component is a pure component (e.g. extends PureComponent) | diff --git a/packages/core/src/component/component-children.ts b/packages/core/src/component/component-children.ts deleted file mode 100644 index 36c1fcb1a..000000000 --- a/packages/core/src/component/component-children.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RuleContext } from "@eslint-react/shared"; -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import { isCreateElementCall } from "../utils/is-react-api"; - -/** - * Determines whether inside `createElement`'s children. - * @param context The rule context - * @param node The AST node to check - * @returns `true` if the node is inside createElement's children - */ -export function isChildrenOfCreateElement(context: RuleContext, node: TSESTree.Node) { - const parent = node.parent; - if (parent == null || parent.type !== T.CallExpression) return false; - if (!isCreateElementCall(context, parent)) return false; - return parent.arguments - .slice(2) - .some((arg) => arg === node); -} diff --git a/packages/core/src/component/component-collector-legacy.ts b/packages/core/src/component/component-collector-legacy.ts index 1e137b491..227ec0677 100644 --- a/packages/core/src/component/component-collector-legacy.ts +++ b/packages/core/src/component/component-collector-legacy.ts @@ -1,11 +1,12 @@ import * as AST from "@eslint-react/ast"; import { unit } from "@eslint-react/eff"; +import { IdGenerator } from "@eslint-react/shared"; import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; -import type { ClassComponent } from "./component-semantic-node"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; -import { IdGenerator } from "@eslint-react/shared"; import { ComponentFlag } from "./component-flag"; import { isClassComponent, isPureComponent } from "./component-is"; +import type { ClassComponent } from "./component-semantic-node"; const idGen = new IdGenerator("class_component_"); @@ -66,3 +67,30 @@ export function useComponentCollectorLegacy(): useComponentCollectorLegacy.Retur return { ctx, listeners } as const; } + +/** + * Check whether the given node is a this.setState() call + * @param node - The node to check + * @internal + */ +export function isThisSetState(node: TSESTree.CallExpression) { + const { callee } = node; + return ( + callee.type === T.MemberExpression + && AST.isThisExpression(callee.object) + && callee.property.type === T.Identifier + && callee.property.name === "setState" + ); +} + +/** + * Check whether the given node is an assignment to this.state + * @param node - The node to check + * @internal + */ +export function isAssignmentToThisState(node: TSESTree.AssignmentExpression) { + const { left } = node; + return left.type === T.MemberExpression + && AST.isThisExpression(left.object) + && AST.getPropertyName(left.property) === "state"; +} diff --git a/packages/core/src/component/component-definition.ts b/packages/core/src/component/component-definition.ts index cff3edb98..33e88a463 100644 --- a/packages/core/src/component/component-definition.ts +++ b/packages/core/src/component/component-definition.ts @@ -1,27 +1,27 @@ import * as AST from "@eslint-react/ast"; import { type RuleContext } from "@eslint-react/shared"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types"; import { P, isMatching } from "ts-pattern"; -import { isChildrenOfCreateElement } from "./component-children"; + +import { isCreateElementCall } from "../utils"; import { ComponentDetectionHint } from "./component-detection-hint"; import { isClassComponent } from "./component-is"; import { isRenderMethodLike } from "./component-render-method"; /** - * Function pattern matchers for different contexts + * Function patterns for matching specific AST structures + * Used to identify where a function is defined (e.g., method, property) */ -const functionPatterns = { - classMethod: { +const FUNCTION_PATTERNS = { + CLASS_METHOD: { type: P.union(T.ArrowFunctionExpression, T.FunctionExpression), parent: T.MethodDefinition, }, - - classProperty: { + CLASS_PROPERTY: { type: P.union(T.ArrowFunctionExpression, T.FunctionExpression), parent: T.Property, }, - - objectMethod: { + OBJECT_METHOD: { type: P.union(T.ArrowFunctionExpression, T.FunctionExpression), parent: { type: T.Property, @@ -30,40 +30,50 @@ const functionPatterns = { }, }, }, -}; +} as const; /** - * Check whether given node is a function of a render method of a class component + * Checks if the given node is a function within a render method of a class component. + * + * @param node - The AST node to check + * @returns `true` if the node is a render function inside a class component + * * @example * ```tsx * class Component extends React.Component { - * renderHeader = () => ; - * renderFooter = () => ; + * renderHeader = () => ; // Returns true * } * ``` - * @param node The AST node to check - * @returns `true` if node is a render function, `false` if not */ -export function isFunctionOfRenderMethod(node: AST.TSESTreeFunction) { - return isRenderMethodLike(node.parent) && isClassComponent(node.parent.parent.parent); +function isFunctionOfRenderMethod(node: AST.TSESTreeFunction) { + const parent = node.parent; + const grandparent = parent.parent; + const greatGrandparent = grandparent?.parent; + + return ( + greatGrandparent != null + && isRenderMethodLike(parent) + && isClassComponent(greatGrandparent) + ); } /** - * Checks if a function node should be excluded based on detection hints - * @param node The function node to check - * @param hint Component detection hints as bit flags - * @returns `true` if the function should be excluded, `false` otherwise + * Checks if a function node should be excluded based on provided detection hints + * + * @param node - The function node to check + * @param hint - Component detection hints as bit flags + * @returns `true` if the function matches an exclusion hint */ -function shouldExcludeBasedOnHint(node: AST.TSESTreeFunction, hint: bigint) { - if ((hint & ComponentDetectionHint.SkipObjectMethod) && isMatching(functionPatterns.objectMethod)(node)) { +function shouldExcludeBasedOnHint(node: AST.TSESTreeFunction, hint: bigint): boolean { + if ((hint & ComponentDetectionHint.SkipObjectMethod) && isMatching(FUNCTION_PATTERNS.OBJECT_METHOD)(node)) { return true; } - if ((hint & ComponentDetectionHint.SkipClassMethod) && isMatching(functionPatterns.classMethod)(node)) { + if ((hint & ComponentDetectionHint.SkipClassMethod) && isMatching(FUNCTION_PATTERNS.CLASS_METHOD)(node)) { return true; } - if ((hint & ComponentDetectionHint.SkipClassProperty) && isMatching(functionPatterns.classProperty)(node)) { + if ((hint & ComponentDetectionHint.SkipClassProperty) && isMatching(FUNCTION_PATTERNS.CLASS_PROPERTY)(node)) { return true; } @@ -74,24 +84,55 @@ function shouldExcludeBasedOnHint(node: AST.TSESTreeFunction, hint: bigint) { return false; } +/** + * Determines if the node is an argument within `createElement`'s children list (3rd argument onwards) + * + * @param context - The rule context + * @param node - The AST node to check + * @returns `true` if the node is passed as a child to `createElement` + */ +function isChildrenOfCreateElement(context: RuleContext, node: TSESTree.Node): boolean { + const parent = node.parent; + + if (parent?.type !== T.CallExpression) { + return false; + } + + if (!isCreateElementCall(context, parent)) { + return false; + } + + // The first two arguments are 'type' and 'props', children start at index 2 + return parent.arguments + .slice(2) + .some((arg) => arg === node); +} + /** * Determines if a function node represents a valid React component definition - * @param context The rule context - * @param node The function node to check - * @param hint Component detection hints as bit flags - * @returns `true` if the node is a valid component definition, `false` otherwise + * + * @param context - The rule context + * @param node - The function node to analyze + * @param hint - Component detection hints (bit flags) to customize detection logic + * @returns `true` if the node is considered a component definition */ -export function isComponentDefinition(context: RuleContext, node: AST.TSESTreeFunction, hint: bigint) { - // Check for immediate exclusion cases +export function isComponentDefinition( + context: RuleContext, + node: AST.TSESTreeFunction, + hint: bigint, +) { + // 1. Check immediate contextual exclusions if (isChildrenOfCreateElement(context, node) || isFunctionOfRenderMethod(node)) { return false; } - // Check for hint-based exclusions + // 2. Check explicit hints provided by the caller if (shouldExcludeBasedOnHint(node, hint)) { return false; } + // 3. Check if the function is embedded directly inside JSX (e.g., inline callbacks) + // We look for the closest parent that is significant (Function, Class, or JSXContainer) const significantParent = AST.findParentNode( node, AST.isOneOf([ @@ -103,5 +144,6 @@ export function isComponentDefinition(context: RuleContext, node: AST.TSESTreeFu ]), ); + // If the immediate significant parent is a JSX expression, this is likely an event handler or a render prop, not a component definition itself return significantParent == null || significantParent.type !== T.JSXExpressionContainer; } diff --git a/packages/core/src/component/component-flag.ts b/packages/core/src/component/component-flag.ts index a24ffe9bb..99681f018 100644 --- a/packages/core/src/component/component-flag.ts +++ b/packages/core/src/component/component-flag.ts @@ -2,10 +2,16 @@ export type ComponentFlag = bigint; export const ComponentFlag = { + /** No flags set */ None: 0n, + /** Indicates the component is a pure component (e.g. extends PureComponent) */ PureComponent: 1n << 0n, + /** Indicates the component creates elements using `createElement` instead of JSX */ CreateElement: 1n << 1n, + /** Indicates the component is memoized (e.g. React.memo) */ Memo: 1n << 2n, + /** Indicates the component forwards a ref (e.g. React.forwardRef) */ ForwardRef: 1n << 3n, + /** Indicates the component is asynchronous */ Async: 1n << 4n, }; diff --git a/packages/core/src/component/component-id.ts b/packages/core/src/component/component-id.ts index 01ebe7e63..96e5558fc 100644 --- a/packages/core/src/component/component-id.ts +++ b/packages/core/src/component/component-id.ts @@ -3,6 +3,7 @@ import { unit } from "@eslint-react/eff"; import type { RuleContext } from "@eslint-react/shared"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + import { isComponentWrapperCallLoose } from "./component-wrapper"; export function getFunctionComponentId( diff --git a/packages/core/src/component/component-init-path.ts b/packages/core/src/component/component-init-path.ts index 46c2f188b..6805f4e24 100644 --- a/packages/core/src/component/component-init-path.ts +++ b/packages/core/src/component/component-init-path.ts @@ -1,4 +1,5 @@ import * as AST from "@eslint-react/ast"; + import { ComponentFlag } from "./component-flag"; import type { FunctionComponent } from "./component-semantic-node"; diff --git a/packages/core/src/component/component-name.ts b/packages/core/src/component/component-name.ts index 3fa3ec1a3..59e58b9d7 100644 --- a/packages/core/src/component/component-name.ts +++ b/packages/core/src/component/component-name.ts @@ -5,14 +5,26 @@ import type { TSESTree } from "@typescript-eslint/types"; import { getFunctionComponentId } from "./component-id"; +/** + * Check if a string matches the strict component name pattern + * @param name - The name to check + */ export function isComponentName(name: string) { return RE_COMPONENT_NAME.test(name); } +/** + * Check if a string matches the loose component name pattern + * @param name - The name to check + */ export function isComponentNameLoose(name: string) { return RE_COMPONENT_NAME_LOOSE.test(name); } +/** + * Get component name from an identifier or identifier sequence (e.g., MemberExpression) + * @param id - The identifier or identifier sequence + */ export function getComponentNameFromId(id: TSESTree.Identifier | TSESTree.Identifier[] | unit) { if (id == null) return unit; return Array.isArray(id) @@ -20,6 +32,11 @@ export function getComponentNameFromId(id: TSESTree.Identifier | TSESTree.Identi : id.name; } +/** + * Check if the function has no name or a loose component name + * @param context - The rule context + * @param fn - The function node + */ export function hasNoneOrLooseComponentName(context: RuleContext, fn: AST.TSESTreeFunction) { const id = getFunctionComponentId(context, fn); if (id == null) return true; diff --git a/packages/core/src/component/component-semantic-node.ts b/packages/core/src/component/component-semantic-node.ts index f81400a50..00bae5e63 100644 --- a/packages/core/src/component/component-semantic-node.ts +++ b/packages/core/src/component/component-semantic-node.ts @@ -6,38 +6,103 @@ import type { SemanticNode } from "../semantic"; import type { ComponentDetectionHint } from "./component-detection-hint"; import type { ComponentFlag } from "./component-flag"; -/* eslint-disable perfectionist/sort-interfaces */ +/** + * Represents a React function component + */ export interface FunctionComponent extends SemanticNode { + /** + * The identifier or identifier sequence of the component + */ id: | unit | TSESTree.Identifier | TSESTree.Identifier[]; + + /** + * The kind of component + */ kind: "function"; + + /** + * The AST node of the function + */ node: AST.TSESTreeFunction; + + /** + * Flags describing the component's characteristics + */ flag: ComponentFlag; + + /** + * Hint for how the component was detected + */ hint: ComponentDetectionHint; + + /** + * The initialization path of the function + */ initPath: | unit | AST.FunctionInitPath; + + /** + * List of hook calls within the component + */ hookCalls: TSESTree.CallExpression[]; + + /** + * The display name of the component + */ displayName: | unit | TSESTree.Expression; } +/** + * Represents a React class component + */ export interface ClassComponent extends SemanticNode { + /** + * The identifier of the component + */ id: | unit | TSESTree.Identifier; + + /** + * The kind of component + */ kind: "class"; + + /** + * The AST node of the class + */ node: AST.TSESTreeClass; + + /** + * Flags describing the component's characteristics + */ flag: ComponentFlag; + + /** + * Hint for how the component was detected + */ hint: ComponentDetectionHint; + + /** + * List of methods and properties in the class + */ methods: AST.TSESTreeMethodOrProperty[]; + + /** + * The display name of the component + */ displayName: | unit | TSESTree.Expression; } -/* eslint-enable perfectionist/sort-interfaces */ +/** + * Union type representing either a class or function component + */ export type Component = ClassComponent | FunctionComponent; diff --git a/packages/core/src/component/component-state.ts b/packages/core/src/component/component-state.ts deleted file mode 100644 index 9c5bd0a68..000000000 --- a/packages/core/src/component/component-state.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as AST from "@eslint-react/ast"; -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export type ComponentStateKind = "actionState" | "state"; - -export function isThisSetState(node: TSESTree.CallExpression) { - const { callee } = node; - return ( - callee.type === T.MemberExpression - && AST.isThisExpression(callee.object) - && callee.property.type === T.Identifier - && callee.property.name === "setState" - ); -} - -export function isAssignmentToThisState(node: TSESTree.AssignmentExpression) { - const { left } = node; - return left.type === T.MemberExpression - && AST.isThisExpression(left.object) - && AST.getPropertyName(left.property) === "state"; -} diff --git a/packages/core/src/component/index.ts b/packages/core/src/component/index.ts index e5512f54d..05c10bd53 100644 --- a/packages/core/src/component/index.ts +++ b/packages/core/src/component/index.ts @@ -1,4 +1,3 @@ -export * from "./component-children"; export * from "./component-collector"; export * from "./component-collector-legacy"; export * from "./component-definition"; @@ -16,5 +15,4 @@ export * from "./component-phase-helpers"; export * from "./component-render-method"; export * from "./component-render-prop"; export type * from "./component-semantic-node"; -export * from "./component-state"; export * from "./component-wrapper";