From 46c02721fc58794a98994a8ce9e0e801d74b53f8 Mon Sep 17 00:00:00 2001 From: REL1CX Date: Tue, 25 Feb 2025 15:57:47 +0800 Subject: [PATCH] refactor: code optimization (#945) --- .pkgs/eslint-plugin-local/dist/index.js | 2 +- .../src/utils/is-initialized-from-source.ts | 2 +- .../-internal-/interfaces/TSTypeOperator.md | 2 +- packages/core/docs/README.md | 6 + .../core/docs/functions/hasValidHierarchy.md | 29 +++ packages/core/docs/functions/isFromReact.md | 6 - .../core/docs/functions/isFromReactLoose.md | 23 +++ .../core/docs/functions/isFromReactMember.md | 37 ++++ .../docs/functions/isFromReactMemberLoose.md | 27 +++ .../docs/functions/isFromReactMemberStrict.md | 35 ++++ .../core/docs/functions/isFromReactStrict.md | 31 +++ .../core/src/component/component-collector.ts | 34 +--- packages/core/src/component/hierarchy.ts | 61 ++++++ packages/core/src/component/index.ts | 1 + packages/core/src/utils/index.ts | 1 + packages/core/src/utils/is-call-from-react.ts | 47 +++++ packages/core/src/utils/is-from-react.ts | 186 +++++++----------- .../src/utils/is-initialized-from-react.ts | 2 +- packages/core/src/utils/is-react-api.ts | 3 +- .../no-direct-set-state-in-use-effect.ts | 3 +- ...o-direct-set-state-in-use-layout-effect.ts | 3 +- .../src/utils/index.ts | 1 + .../is-function-of-immediately-invoked.ts | 5 +- .../src/rules/no-array-index-key.ts | 20 +- .../shared/docs/functions/defineSettings.md | 28 +++ .../get-identifiers-from-binary-expression.ts | 20 -- packages/utilities/ast/src/index.ts | 8 +- .../ast/src/is-function-of-class-method.ts | 15 -- .../ast/src/is-function-of-class-property.ts | 15 -- .../ast/src/is-function-of-object-method.ts | 13 -- .../utilities/ast/src/is-kind-of-literal.ts | 31 +++ packages/utilities/ast/src/is-map-call.ts | 12 +- .../utilities/ast/src/is-regexp-literal.ts | 6 - .../utilities/ast/src/is-string-literal.ts | 6 - .../utilities/ast/src/is-this-expression.ts | 5 +- packages/utilities/var/src/index.ts | 1 - .../var/src/is-initialized-from-source.ts | 64 ------ 37 files changed, 471 insertions(+), 320 deletions(-) create mode 100644 packages/core/docs/functions/hasValidHierarchy.md create mode 100644 packages/core/docs/functions/isFromReactLoose.md create mode 100644 packages/core/docs/functions/isFromReactMember.md create mode 100644 packages/core/docs/functions/isFromReactMemberLoose.md create mode 100644 packages/core/docs/functions/isFromReactMemberStrict.md create mode 100644 packages/core/docs/functions/isFromReactStrict.md create mode 100644 packages/core/src/component/hierarchy.ts create mode 100644 packages/core/src/utils/is-call-from-react.ts rename packages/{utilities/ast/src => plugins/eslint-plugin-react-hooks-extra/src/utils}/is-function-of-immediately-invoked.ts (59%) delete mode 100644 packages/utilities/ast/src/get-identifiers-from-binary-expression.ts delete mode 100644 packages/utilities/ast/src/is-function-of-class-method.ts delete mode 100644 packages/utilities/ast/src/is-function-of-class-property.ts delete mode 100644 packages/utilities/ast/src/is-function-of-object-method.ts create mode 100644 packages/utilities/ast/src/is-kind-of-literal.ts delete mode 100644 packages/utilities/ast/src/is-regexp-literal.ts delete mode 100644 packages/utilities/ast/src/is-string-literal.ts delete mode 100644 packages/utilities/var/src/is-initialized-from-source.ts diff --git a/.pkgs/eslint-plugin-local/dist/index.js b/.pkgs/eslint-plugin-local/dist/index.js index a2e683486..96621d893 100644 --- a/.pkgs/eslint-plugin-local/dist/index.js +++ b/.pkgs/eslint-plugin-local/dist/index.js @@ -26,7 +26,7 @@ function isInitializedFromSource(name2, source, initialScope) { } const args = getRequireExpressionArguments(init); const arg0 = args?.[0]; - if (arg0 == null || !AST.isStringLiteral(arg0)) { + if (arg0 == null || !AST.isKindOfLiteral(arg0, "string")) { return false; } return arg0.value === source || arg0.value.startsWith(`${source}/`); diff --git a/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts b/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts index e3828ff1e..50f85225f 100644 --- a/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts +++ b/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts @@ -33,7 +33,7 @@ export function isInitializedFromSource( // check for: `variable = require('source')` or `variable = require('source').variable` const args = getRequireExpressionArguments(init); const arg0 = args?.[0]; - if (arg0 == null || !AST.isStringLiteral(arg0)) { + if (arg0 == null || !AST.isKindOfLiteral(arg0, "string")) { return false; } // check for: `require('source')` or `require('source/...')` diff --git a/packages/core/docs/-internal-/interfaces/TSTypeOperator.md b/packages/core/docs/-internal-/interfaces/TSTypeOperator.md index 75343d220..d550645e4 100644 --- a/packages/core/docs/-internal-/interfaces/TSTypeOperator.md +++ b/packages/core/docs/-internal-/interfaces/TSTypeOperator.md @@ -28,7 +28,7 @@ The loc property is defined as nullable by ESTree, but ESLint requires this prop ### operator -> **operator**: `"keyof"` \| `"readonly"` \| `"unique"` +> **operator**: `"readonly"` \| `"keyof"` \| `"unique"` *** diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index c0bf17348..2d7bc0d0f 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -52,6 +52,7 @@ - [getComponentNameFromIdentifier](functions/getComponentNameFromIdentifier.md) - [getFunctionComponentIdentifier](functions/getFunctionComponentIdentifier.md) - [hasNoneOrValidComponentName](functions/hasNoneOrValidComponentName.md) +- [hasValidHierarchy](functions/hasValidHierarchy.md) - [isCallFromReact](functions/isCallFromReact.md) - [isCallFromReactMember](functions/isCallFromReactMember.md) - [isChildrenCount](functions/isChildrenCount.md) @@ -83,6 +84,11 @@ - [isForwardRef](functions/isForwardRef.md) - [isForwardRefCall](functions/isForwardRefCall.md) - [isFromReact](functions/isFromReact.md) +- [isFromReactLoose](functions/isFromReactLoose.md) +- [isFromReactMember](functions/isFromReactMember.md) +- [isFromReactMemberLoose](functions/isFromReactMemberLoose.md) +- [isFromReactMemberStrict](functions/isFromReactMemberStrict.md) +- [isFromReactStrict](functions/isFromReactStrict.md) - [isFunctionOfComponentDidMount](functions/isFunctionOfComponentDidMount.md) - [isFunctionOfComponentWillUnmount](functions/isFunctionOfComponentWillUnmount.md) - [isFunctionOfRenderMethod](functions/isFunctionOfRenderMethod.md) diff --git a/packages/core/docs/functions/hasValidHierarchy.md b/packages/core/docs/functions/hasValidHierarchy.md new file mode 100644 index 000000000..9a85577ef --- /dev/null +++ b/packages/core/docs/functions/hasValidHierarchy.md @@ -0,0 +1,29 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / hasValidHierarchy + +# Function: hasValidHierarchy() + +> **hasValidHierarchy**(`node`, `context`, `hint`): `boolean` + +internal + +## Parameters + +### node + +[`TSESTreeFunction`](../-internal-/type-aliases/TSESTreeFunction.md) + +### context + +[`Readonly`](../-internal-/type-aliases/Readonly.md) + +### hint + +`bigint` + +## Returns + +`boolean` diff --git a/packages/core/docs/functions/isFromReact.md b/packages/core/docs/functions/isFromReact.md index 8d2903023..d1e7d5623 100644 --- a/packages/core/docs/functions/isFromReact.md +++ b/packages/core/docs/functions/isFromReact.md @@ -8,22 +8,16 @@ > **isFromReact**(`name`): (`node`, `context`) => `boolean` -Checks if the given node is a call expression to the given function or method of the pragma - ## Parameters ### name `string` -The name of the function or method to check - ## Returns `Function` -A predicate that checks if the given node is a call expression to the given function or method - ### Parameters #### node diff --git a/packages/core/docs/functions/isFromReactLoose.md b/packages/core/docs/functions/isFromReactLoose.md new file mode 100644 index 000000000..488661013 --- /dev/null +++ b/packages/core/docs/functions/isFromReactLoose.md @@ -0,0 +1,23 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isFromReactLoose + +# Function: isFromReactLoose() + +> **isFromReactLoose**(`node`, `name`): `boolean` + +## Parameters + +### node + +[`Identifier`](../-internal-/interfaces/Identifier.md) | [`MemberExpression`](../-internal-/type-aliases/MemberExpression.md) + +### name + +`string` + +## Returns + +`boolean` diff --git a/packages/core/docs/functions/isFromReactMember.md b/packages/core/docs/functions/isFromReactMember.md new file mode 100644 index 000000000..f6459fb23 --- /dev/null +++ b/packages/core/docs/functions/isFromReactMember.md @@ -0,0 +1,37 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isFromReactMember + +# Function: isFromReactMember() + +> **isFromReactMember**(`memberName`, `name`): (`node`, `context`) => `boolean` + +## Parameters + +### memberName + +`string` + +### name + +`string` + +## Returns + +`Function` + +### Parameters + +#### node + +[`MemberExpression`](../-internal-/type-aliases/MemberExpression.md) + +#### context + +[`Readonly`](../-internal-/type-aliases/Readonly.md) + +### Returns + +`boolean` diff --git a/packages/core/docs/functions/isFromReactMemberLoose.md b/packages/core/docs/functions/isFromReactMemberLoose.md new file mode 100644 index 000000000..c9facdfa6 --- /dev/null +++ b/packages/core/docs/functions/isFromReactMemberLoose.md @@ -0,0 +1,27 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isFromReactMemberLoose + +# Function: isFromReactMemberLoose() + +> **isFromReactMemberLoose**(`node`, `memberName`, `name`): `boolean` + +## Parameters + +### node + +[`MemberExpression`](../-internal-/type-aliases/MemberExpression.md) + +### memberName + +`string` + +### name + +`string` + +## Returns + +`boolean` diff --git a/packages/core/docs/functions/isFromReactMemberStrict.md b/packages/core/docs/functions/isFromReactMemberStrict.md new file mode 100644 index 000000000..0df5df792 --- /dev/null +++ b/packages/core/docs/functions/isFromReactMemberStrict.md @@ -0,0 +1,35 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isFromReactMemberStrict + +# Function: isFromReactMemberStrict() + +> **isFromReactMemberStrict**(`node`, `memberName`, `name`, `importSource`, `initialScope`): `boolean` + +## Parameters + +### node + +[`MemberExpression`](../-internal-/type-aliases/MemberExpression.md) + +### memberName + +`string` + +### name + +`string` + +### importSource + +`string` + +### initialScope + +[`Scope`](../-internal-/type-aliases/Scope.md) + +## Returns + +`boolean` diff --git a/packages/core/docs/functions/isFromReactStrict.md b/packages/core/docs/functions/isFromReactStrict.md new file mode 100644 index 000000000..dc57358db --- /dev/null +++ b/packages/core/docs/functions/isFromReactStrict.md @@ -0,0 +1,31 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isFromReactStrict + +# Function: isFromReactStrict() + +> **isFromReactStrict**(`node`, `name`, `importSource`, `initialScope`): `boolean` + +## Parameters + +### node + +[`Identifier`](../-internal-/interfaces/Identifier.md) | [`MemberExpression`](../-internal-/type-aliases/MemberExpression.md) + +### name + +`string` + +### importSource + +`string` + +### initialScope + +[`Scope`](../-internal-/type-aliases/Scope.md) + +## Returns + +`boolean` diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts index a835b13f3..31fae8931 100644 --- a/packages/core/src/component/component-collector.ts +++ b/packages/core/src/component/component-collector.ts @@ -7,15 +7,14 @@ import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { ESLintUtils } from "@typescript-eslint/utils"; -import { isChildrenOfCreateElement } from "../element"; import { isReactHookCall } from "../hook"; import { DISPLAY_NAME_ASSIGNMENT_SELECTOR } from "../utils"; -import { DEFAULT_COMPONENT_HINT, ERComponentHint } from "./component-collector-hint"; +import { DEFAULT_COMPONENT_HINT } from "./component-collector-hint"; import { ERComponentFlag } from "./component-flag"; import { getFunctionComponentIdentifier } from "./component-id"; -import { isFunctionOfRenderMethod } from "./component-lifecycle"; import { getComponentNameFromIdentifier, hasNoneOrValidComponentName } from "./component-name"; import type { ERFunctionComponent } from "./component-semantic-node"; +import { hasValidHierarchy } from "./hierarchy"; type FunctionEntry = { key: string; @@ -172,35 +171,6 @@ export function useComponentCollector( return { ctx, listeners } as const; } -function hasValidHierarchy(node: AST.TSESTreeFunction, context: RuleContext, hint: bigint) { - if (isChildrenOfCreateElement(node, context) || isFunctionOfRenderMethod(node)) { - return false; - } - if (hint & ERComponentHint.SkipMapCallback && AST.isMapCallLoose(node.parent)) { - return false; - } - if (hint & ERComponentHint.SkipObjectMethod && AST.isFunctionOfObjectMethod(node.parent)) { - return false; - } - if (hint & ERComponentHint.SkipClassMethod && AST.isFunctionOfClassMethod(node.parent)) { - return false; - } - if (hint & ERComponentHint.SkipClassProperty && AST.isFunctionOfClassProperty(node.parent)) { - return false; - } - const boundaryNode = AST.findParentNode( - node, - AST.isOneOf([ - T.JSXExpressionContainer, - T.ArrowFunctionExpression, - T.FunctionExpression, - T.Property, - T.ClassBody, - ]), - ); - return boundaryNode == null || boundaryNode.type !== T.JSXExpressionContainer; -} - function getComponentFlag(initPath: ERFunctionComponent["initPath"]) { let flag = ERComponentFlag.None; if (initPath != null && AST.hasCallInFunctionInitPath("memo", initPath)) { diff --git a/packages/core/src/component/hierarchy.ts b/packages/core/src/component/hierarchy.ts new file mode 100644 index 000000000..14db55bb8 --- /dev/null +++ b/packages/core/src/component/hierarchy.ts @@ -0,0 +1,61 @@ +/* eslint-disable jsdoc/require-param */ +import * as AST from "@eslint-react/ast"; +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 { isChildrenOfCreateElement } from "../element"; +import { ERComponentHint } from "./component-collector-hint"; +import { isFunctionOfRenderMethod } from "./component-lifecycle"; + +/** internal */ +export function hasValidHierarchy(node: AST.TSESTreeFunction, context: RuleContext, hint: bigint) { + if (isChildrenOfCreateElement(node, context) || isFunctionOfRenderMethod(node)) { + return false; + } + if (hint & ERComponentHint.SkipMapCallback && AST.isMapCallLoose(node.parent)) { + return false; + } + if (hint & ERComponentHint.SkipObjectMethod && isFunctionOfObjectMethod(node.parent)) { + return false; + } + if (hint & ERComponentHint.SkipClassMethod && isFunctionOfClassMethod(node.parent)) { + return false; + } + if (hint & ERComponentHint.SkipClassProperty && isFunctionOfClassProperty(node.parent)) { + return false; + } + const boundaryNode = AST.findParentNode( + node, + AST.isOneOf([ + T.JSXExpressionContainer, + T.ArrowFunctionExpression, + T.FunctionExpression, + T.Property, + T.ClassBody, + ]), + ); + return boundaryNode == null || boundaryNode.type !== T.JSXExpressionContainer; +} + +function isFunctionOfClassMethod(node: TSESTree.Node): node is + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression +{ + return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) + && node.parent.type === T.MethodDefinition; +} + +function isFunctionOfClassProperty(node: TSESTree.Node): node is + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression +{ + return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) + && node.parent.type === T.Property; +} + +function isFunctionOfObjectMethod(node: TSESTree.Node) { + return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) + && node.parent.type === T.Property + && node.parent.parent.type === T.ObjectExpression; +} diff --git a/packages/core/src/component/index.ts b/packages/core/src/component/index.ts index 12d5cc1a1..9690a4611 100644 --- a/packages/core/src/component/index.ts +++ b/packages/core/src/component/index.ts @@ -10,4 +10,5 @@ export * from "./component-phase"; export * from "./component-render-prop"; export type * from "./component-semantic-node"; export * from "./component-state"; +export * from "./hierarchy"; export * from "./is"; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ea222efa3..81bc317ed 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./is-call-from-react"; export * from "./is-display-name-assignment"; export * from "./is-from-react"; export * from "./is-initialized-from-react"; diff --git a/packages/core/src/utils/is-call-from-react.ts b/packages/core/src/utils/is-call-from-react.ts new file mode 100644 index 000000000..1399e35c5 --- /dev/null +++ b/packages/core/src/utils/is-call-from-react.ts @@ -0,0 +1,47 @@ +import * as AST from "@eslint-react/ast"; +import { dual } 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 { isFromReact, isFromReactMember } from "./is-from-react"; + +export declare namespace isCallFromReact { + type ReturnType = { + (context: RuleContext): (node: TSESTree.Node) => node is TSESTree.CallExpression; + (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression; + }; +} + +export function isCallFromReact(name: string): isCallFromReact.ReturnType { + return dual(2, (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression => { + if (node.type !== T.CallExpression) return false; + if (!AST.isOneOf([T.Identifier, T.MemberExpression])(node.callee)) return false; + return isFromReact(name)(node.callee, context); + }); +} + +export declare namespace isCallFromReactMember { + type ReturnType = { + (context: RuleContext): (node: TSESTree.Node) => node is + & TSESTree.CallExpression + & { callee: TSESTree.MemberExpression }; + (node: TSESTree.Node, context: RuleContext): node is + & TSESTree.CallExpression + & { callee: TSESTree.MemberExpression }; + }; +} + +export function isCallFromReactMember( + pragmaMemberName: string, + name: string, +): isCallFromReactMember.ReturnType { + return dual(2, (node: TSESTree.Node, context: RuleContext): node is + & TSESTree.CallExpression + & { callee: TSESTree.MemberExpression } => + { + if (node.type !== T.CallExpression) return false; + if (!AST.is(T.MemberExpression)(node.callee)) return false; + return isFromReactMember(pragmaMemberName, name)(node.callee, context); + }); +} diff --git a/packages/core/src/utils/is-from-react.ts b/packages/core/src/utils/is-from-react.ts index f8bf029aa..dcc66ae80 100644 --- a/packages/core/src/utils/is-from-react.ts +++ b/packages/core/src/utils/is-from-react.ts @@ -1,131 +1,95 @@ -import * as AST from "@eslint-react/ast"; -import { dual } from "@eslint-react/eff"; import type { RuleContext } from "@eslint-react/shared"; import { DEFAULT_ESLINT_REACT_SETTINGS, unsafeDecodeSettings } from "@eslint-react/shared"; +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 { isInitializedFromReact } from "./is-initialized-from-react"; -/** - * Checks if the given node is a call expression to the given function or method of the pragma - * @param name The name of the function or method to check - * @returns A predicate that checks if the given node is a call expression to the given function or method - */ -export function isFromReact(name: string) { - return ( - node: TSESTree.Identifier | TSESTree.MemberExpression, - context: RuleContext, - ) => { - const settings = unsafeDecodeSettings(context.settings); - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!settings.strictImportCheck) { - if (node.type === T.MemberExpression) { - return node.object.type === T.Identifier - && node.property.type === T.Identifier - && node.property.name === name; - } - return node.name === name; - } - const importSource = settings.importSource ?? DEFAULT_ESLINT_REACT_SETTINGS.importSource; - const initialScope = context.sourceCode.getScope(node); - if (node.type === T.MemberExpression) { - return node.object.type === T.Identifier - && node.property.type === T.Identifier - && node.property.name === name - && isInitializedFromReact(node.object.name, importSource, initialScope); - } - if (node.name === name) { - return isInitializedFromReact(name, importSource, initialScope); - } - return false; - }; +const defaultImportSource = DEFAULT_ESLINT_REACT_SETTINGS.importSource; + +/* @internal */ +export function isFromReactLoose(node: TSESTree.Identifier | TSESTree.MemberExpression, name: string) { + if (node.type === T.MemberExpression) { + return node.object.type === T.Identifier + && node.property.type === T.Identifier + && node.property.name === name; + } + return node.name === name; } -/** - * @internal - * @param memberName The name of the member object - * @param name The name of the member property - * @returns A function that checks if a given node is a member expression of a Pragma member. - */ -export function isFromReactMember( - memberName: string, +/* @internal */ +export function isFromReactStrict( + node: TSESTree.Identifier | TSESTree.MemberExpression, name: string, -): (node: TSESTree.MemberExpression, context: RuleContext) => boolean { - return ( - node: TSESTree.MemberExpression, - context: RuleContext, - ) => { - const settings = unsafeDecodeSettings(context.settings); - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!settings.strictImportCheck) { - if (node.property.type !== T.Identifier || node.property.name !== name) return false; - if (node.object.type === T.Identifier && node.object.name === memberName) return true; - if ( - node.object.type === T.MemberExpression - && node.object.object.type === T.Identifier - && node.object.property.type === T.Identifier - ) { - return node.object.property.name === memberName; - } - return false; - } - const importSource = settings.importSource ?? DEFAULT_ESLINT_REACT_SETTINGS.importSource; - const initialScope = context.sourceCode.getScope(node); - if (node.property.type !== T.Identifier || node.property.name !== name) { - return false; - } - if (node.object.type === T.Identifier && node.object.name === memberName) { - return isInitializedFromReact(node.object.name, importSource, initialScope); - } - if ( - node.object.type === T.MemberExpression - && node.object.object.type === T.Identifier - && isInitializedFromReact(node.object.object.name, importSource, initialScope) - && node.object.property.type === T.Identifier - ) { - return node.object.property.name === memberName; - } - return false; - }; + importSource: string, + initialScope: Scope, +) { + if (node.type === T.MemberExpression) { + return node.object.type === T.Identifier + && node.property.type === T.Identifier + && node.property.name === name + && isInitializedFromReact(node.object.name, importSource, initialScope); + } + if (node.name === name) { + return isInitializedFromReact(name, importSource, initialScope); + } + return false; } -export declare namespace isCallFromReact { - type ReturnType = { - (context: RuleContext): (node: TSESTree.Node) => node is TSESTree.CallExpression; - (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression; +export function isFromReact(name: string) { + return (node: TSESTree.Identifier | TSESTree.MemberExpression, context: RuleContext) => { + const { importSource = defaultImportSource, strictImportCheck = false } = unsafeDecodeSettings(context.settings); + if (!strictImportCheck) return isFromReactLoose(node, name); + return isFromReactStrict(node, name, importSource, context.sourceCode.getScope(node)); }; } -export function isCallFromReact(name: string): isCallFromReact.ReturnType { - return dual(2, (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression => { - if (node.type !== T.CallExpression) return false; - if (!AST.isOneOf([T.Identifier, T.MemberExpression])(node.callee)) return false; - return isFromReact(name)(node.callee, context); - }); +/* @internal */ +export function isFromReactMemberLoose(node: TSESTree.MemberExpression, memberName: string, name: string) { + const { object, property } = node; + if (property.type !== T.Identifier || property.name !== name) return false; + if (object.type === T.Identifier && object.name === memberName) return true; + if ( + object.type === T.MemberExpression + && object.object.type === T.Identifier + && object.property.type === T.Identifier + ) { + return object.property.name === memberName; + } + return false; } -export declare namespace isCallFromReactMember { - type ReturnType = { - (context: RuleContext): (node: TSESTree.Node) => node is - & TSESTree.CallExpression - & { callee: TSESTree.MemberExpression }; - (node: TSESTree.Node, context: RuleContext): node is - & TSESTree.CallExpression - & { callee: TSESTree.MemberExpression }; - }; +/* @internal */ +export function isFromReactMemberStrict( + node: TSESTree.MemberExpression, + memberName: string, + name: string, + importSource: string, + initialScope: Scope, +) { + const { object, property } = node; + if (property.type !== T.Identifier || property.name !== name) { + return false; + } + if (object.type === T.Identifier && object.name === memberName) { + return isInitializedFromReact(object.name, importSource, initialScope); + } + if ( + object.type === T.MemberExpression + && object.object.type === T.Identifier + && isInitializedFromReact(object.object.name, importSource, initialScope) + && object.property.type === T.Identifier + ) { + return object.property.name === memberName; + } + return false; } -export function isCallFromReactMember( - pragmaMemberName: string, - name: string, -): isCallFromReactMember.ReturnType { - return dual(2, (node: TSESTree.Node, context: RuleContext): node is - & TSESTree.CallExpression - & { callee: TSESTree.MemberExpression } => - { - if (node.type !== T.CallExpression) return false; - if (!AST.is(T.MemberExpression)(node.callee)) return false; - return isFromReactMember(pragmaMemberName, name)(node.callee, context); - }); +export function isFromReactMember(memberName: string, name: string) { + return (node: TSESTree.MemberExpression, context: RuleContext) => { + const { importSource = defaultImportSource, strictImportCheck = false } = unsafeDecodeSettings(context.settings); + if (!strictImportCheck) return isFromReactMemberLoose(node, memberName, name); + return isFromReactMemberStrict(node, memberName, name, importSource, context.sourceCode.getScope(node)); + }; } diff --git a/packages/core/src/utils/is-initialized-from-react.ts b/packages/core/src/utils/is-initialized-from-react.ts index d7e257fe6..1ce361eca 100644 --- a/packages/core/src/utils/is-initialized-from-react.ts +++ b/packages/core/src/utils/is-initialized-from-react.ts @@ -49,7 +49,7 @@ export function isInitializedFromReact( // check for: `variable = require('source')` or `variable = require('source').variable` const args = getRequireExpressionArguments(init); const arg0 = args?.[0]; - if (arg0 == null || !AST.isStringLiteral(arg0)) { + if (arg0 == null || !AST.isKindOfLiteral(arg0, "string")) { return false; } // check for: `require('source')` or `require('source/...')` diff --git a/packages/core/src/utils/is-react-api.ts b/packages/core/src/utils/is-react-api.ts index fd2b820d3..dedb30eae 100644 --- a/packages/core/src/utils/is-react-api.ts +++ b/packages/core/src/utils/is-react-api.ts @@ -1,4 +1,5 @@ -import { isCallFromReact, isCallFromReactMember, isFromReact, isFromReactMember } from "./is-from-react"; +import { isCallFromReact, isCallFromReactMember } from "./is-call-from-react"; +import { isFromReact, isFromReactMember } from "./is-from-react"; export function isReactAPIWithName(name: string): ReturnType; export function isReactAPIWithName(name: string, member: string): ReturnType; 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 b6ecaccbd..6ba3d8f88 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 @@ -13,6 +13,7 @@ import { match } from "ts-pattern"; import { createRule, isFromUseStateCall, + isFunctionOfImmediatelyInvoked, isSetFunctionCall, isThenCall, isVariableDeclaratorFromHookCall, @@ -90,7 +91,7 @@ export default createRule<[], MessageID>({ function getFunctionKind(node: AST.TSESTreeFunction) { return match(node) .when(isFunctionOfUseEffectSetup, () => "setup") - .when(AST.isFunctionOfImmediatelyInvoked, () => "immediate") + .when(isFunctionOfImmediatelyInvoked, () => "immediate") .otherwise(() => "other"); } return { diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts index 69988dcef..2705089c4 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts @@ -13,6 +13,7 @@ import { match } from "ts-pattern"; import { createRule, isFromUseStateCall, + isFunctionOfImmediatelyInvoked, isSetFunctionCall, isThenCall, isVariableDeclaratorFromHookCall, @@ -95,7 +96,7 @@ export default createRule<[], MessageID>({ function getFunctionKind(node: AST.TSESTreeFunction) { return match(node) .when(isFunctionOfUseEffectSetup, () => "setup") - .when(AST.isFunctionOfImmediatelyInvoked, () => "immediate") + .when(isFunctionOfImmediatelyInvoked, () => "immediate") .otherwise(() => "other"); } return { diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts index 8807643c6..416b9a5d6 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts @@ -1,6 +1,7 @@ export * from "./create-rule"; export * from "./is-from-hook-call"; export * from "./is-from-use-state-call"; +export * from "./is-function-of-immediately-invoked"; export * from "./is-react-hook-identifier"; export * from "./is-set-function-call"; export * from "./is-then-call"; diff --git a/packages/utilities/ast/src/is-function-of-immediately-invoked.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts similarity index 59% rename from packages/utilities/ast/src/is-function-of-immediately-invoked.ts rename to packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts index 11173f4c8..3ba5e9e58 100644 --- a/packages/utilities/ast/src/is-function-of-immediately-invoked.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts @@ -1,8 +1,7 @@ +import type * as AST from "@eslint-react/ast"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import type { TSESTreeFunction } from "./types"; - -export function isFunctionOfImmediatelyInvoked(node: TSESTreeFunction): boolean { +export function isFunctionOfImmediatelyInvoked(node: AST.TSESTreeFunction): boolean { return node.type !== T.FunctionDeclaration && node.parent.type === T.CallExpression && node.parent.callee === node; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts index 77f8f5b06..fdeab3d37 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts @@ -150,7 +150,7 @@ export default createRule<[], MessageID>({ const descriptors: ReportDescriptor[] = []; const expressions = node.type === T.TemplateLiteral ? node.expressions - : AST.getIdentifiersFromBinaryExpression(node); + : getIdentifiersFromBinaryExpression(node); for (const expression of expressions) { if (isArrayIndex(expression)) { descriptors.push({ @@ -238,3 +238,21 @@ export default createRule<[], MessageID>({ }, defaultOptions: [], }); + +function getIdentifiersFromBinaryExpression( + side: + | TSESTree.BinaryExpression + | TSESTree.BinaryExpression["left"] + | TSESTree.BinaryExpression["right"], +): readonly TSESTree.Identifier[] { + if (side.type === T.Identifier) { + return [side]; + } + if (side.type === T.BinaryExpression) { + return [ + ...getIdentifiersFromBinaryExpression(side.left), + ...getIdentifiersFromBinaryExpression(side.right), + ] as const; + } + return [] as const; +} diff --git a/packages/shared/docs/functions/defineSettings.md b/packages/shared/docs/functions/defineSettings.md index a8f4a45dd..112bb10c8 100644 --- a/packages/shared/docs/functions/defineSettings.md +++ b/packages/shared/docs/functions/defineSettings.md @@ -188,6 +188,20 @@ This is used to determine the type of the component. `"as"` ``` +#### strictImportCheck + +`boolean` = `...` + +Check both the shape and the import to determine if a API is from React. + +**Description** + +This can prevent false positives when using a irrelevant third-party library that has similar APIs to React. + +**Default** + +`false` + #### version? `string` = `...` @@ -383,6 +397,20 @@ This is used to determine the type of the component. `"as"` ``` +### strictImportCheck + +> **strictImportCheck**: `boolean` + +Check both the shape and the import to determine if a API is from React. + +#### Description + +This can prevent false positives when using a irrelevant third-party library that has similar APIs to React. + +#### Default + +`false` + ### version? > `optional` **version**: `string` diff --git a/packages/utilities/ast/src/get-identifiers-from-binary-expression.ts b/packages/utilities/ast/src/get-identifiers-from-binary-expression.ts deleted file mode 100644 index c591a0a77..000000000 --- a/packages/utilities/ast/src/get-identifiers-from-binary-expression.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export function getIdentifiersFromBinaryExpression( - side: - | TSESTree.BinaryExpression - | TSESTree.BinaryExpression["left"] - | TSESTree.BinaryExpression["right"], -): readonly TSESTree.Identifier[] { - if (side.type === T.Identifier) { - return [side]; - } - if (side.type === T.BinaryExpression) { - return [ - ...getIdentifiersFromBinaryExpression(side.left), - ...getIdentifiersFromBinaryExpression(side.right), - ] as const; - } - return [] as const; -} diff --git a/packages/utilities/ast/src/index.ts b/packages/utilities/ast/src/index.ts index c9db0af13..d4f42debd 100644 --- a/packages/utilities/ast/src/index.ts +++ b/packages/utilities/ast/src/index.ts @@ -2,7 +2,6 @@ export * from "./find-parent-node"; export * from "./function-init-path"; export * from "./get-class-identifier"; export * from "./get-function-identifier"; -export * from "./get-identifiers-from-binary-expression"; export * from "./get-literal-value-type"; export * from "./get-nested-call-expressions"; export * from "./get-nested-expressions-of-type"; @@ -12,16 +11,11 @@ export * from "./get-nested-return-statements"; export * from "./get-top-level-identifier"; export * from "./is"; export * from "./is-empty-function"; -export * from "./is-function-of-class-method"; -export * from "./is-function-of-class-property"; -export * from "./is-function-of-immediately-invoked"; -export * from "./is-function-of-object-method"; export * from "./is-key-literal"; +export * from "./is-kind-of-literal"; export * from "./is-map-call"; export * from "./is-multi-line"; export * from "./is-node-equal"; -export * from "./is-regexp-literal"; -export * from "./is-string-literal"; export * from "./is-this-expression"; export * from "./to-readable-node-name"; export * from "./to-readable-node-type"; diff --git a/packages/utilities/ast/src/is-function-of-class-method.ts b/packages/utilities/ast/src/is-function-of-class-method.ts deleted file mode 100644 index 7ac609aaf..000000000 --- a/packages/utilities/ast/src/is-function-of-class-method.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -/** - * Checks if the given node is a function expression or arrow function expression of a class method. - * @param node The node to check. - * @returns `true` if the node is a function expression or arrow function expression of a class method, `false` otherwise. - */ -export function isFunctionOfClassMethod(node: TSESTree.Node): node is - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression -{ - return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) - && node.parent.type === T.MethodDefinition; -} diff --git a/packages/utilities/ast/src/is-function-of-class-property.ts b/packages/utilities/ast/src/is-function-of-class-property.ts deleted file mode 100644 index a37dd86e7..000000000 --- a/packages/utilities/ast/src/is-function-of-class-property.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -/** - * Checks if the given node is a function expression or arrow function expression of a class property. - * @param node The node to check. - * @returns `true` if the node is a function expression or arrow function expression of a class property, `false` otherwise. - */ -export function isFunctionOfClassProperty(node: TSESTree.Node): node is - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression -{ - return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) - && node.parent.type === T.Property; -} diff --git a/packages/utilities/ast/src/is-function-of-object-method.ts b/packages/utilities/ast/src/is-function-of-object-method.ts deleted file mode 100644 index 22b6d5420..000000000 --- a/packages/utilities/ast/src/is-function-of-object-method.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -/** - * Checks if the given node is a function expression or arrow function expression of a object method. - * @param node The node to check. - * @returns `true` if the node is a function expression or arrow function expression of a object method, `false` otherwise. - */ -export function isFunctionOfObjectMethod(node: TSESTree.Node) { - return (node.type === T.FunctionExpression || node.type === T.ArrowFunctionExpression) - && node.parent.type === T.Property - && node.parent.parent.type === T.ObjectExpression; -} diff --git a/packages/utilities/ast/src/is-kind-of-literal.ts b/packages/utilities/ast/src/is-kind-of-literal.ts new file mode 100644 index 000000000..19e8cdc2e --- /dev/null +++ b/packages/utilities/ast/src/is-kind-of-literal.ts @@ -0,0 +1,31 @@ +/* eslint-disable local/prefer-eqeq-nullish-comparison */ +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + +type LiteralType = + | "boolean" + | "null" + | "number" + | "regexp" + | "string"; + +export function isKindOfLiteral(node: TSESTree.Node, kind: "boolean"): node is TSESTree.BooleanLiteral; +export function isKindOfLiteral(node: TSESTree.Node, kind: "null"): node is TSESTree.NullLiteral; +export function isKindOfLiteral(node: TSESTree.Node, kind: "number"): node is TSESTree.NumberLiteral; +export function isKindOfLiteral(node: TSESTree.Node, kind: "regexp"): node is TSESTree.RegExpLiteral; +export function isKindOfLiteral(node: TSESTree.Node, kind: "string"): node is TSESTree.StringLiteral; +export function isKindOfLiteral(node: TSESTree.Node, kind: LiteralType) { + if (node.type !== T.Literal) return false; + switch (kind) { + case "boolean": + return typeof node.value === "boolean"; + case "null": + return node.value === null; + case "number": + return typeof node.value === "number"; + case "regexp": + return "regex" in node; + case "string": + return typeof node.value === "string"; + } +} diff --git a/packages/utilities/ast/src/is-map-call.ts b/packages/utilities/ast/src/is-map-call.ts index f9ccdd2cf..9dbfb64b6 100644 --- a/packages/utilities/ast/src/is-map-call.ts +++ b/packages/utilities/ast/src/is-map-call.ts @@ -2,15 +2,9 @@ import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; export function isMapCallLoose(node: TSESTree.Node): node is TSESTree.CallExpression { - if (node.type !== T.CallExpression) { - return false; - } - if (node.callee.type !== T.MemberExpression) { - return false; - } - if (node.callee.property.type !== T.Identifier) { - return false; - } + if (node.type !== T.CallExpression) return false; + if (node.callee.type !== T.MemberExpression) return false; + if (node.callee.property.type !== T.Identifier) return false; const { name } = node.callee.property; return name === "map" || name.endsWith("Map"); } diff --git a/packages/utilities/ast/src/is-regexp-literal.ts b/packages/utilities/ast/src/is-regexp-literal.ts deleted file mode 100644 index aa43c91b6..000000000 --- a/packages/utilities/ast/src/is-regexp-literal.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export function isRegExpLiteral(node: TSESTree.Node): node is TSESTree.RegExpLiteral { - return node.type === T.Literal && "regex" in node; -} diff --git a/packages/utilities/ast/src/is-string-literal.ts b/packages/utilities/ast/src/is-string-literal.ts deleted file mode 100644 index e1fb40d6f..000000000 --- a/packages/utilities/ast/src/is-string-literal.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export function isStringLiteral(node: TSESTree.Node): node is TSESTree.StringLiteral { - return node.type === T.Literal && typeof node.value === "string"; -} diff --git a/packages/utilities/ast/src/is-this-expression.ts b/packages/utilities/ast/src/is-this-expression.ts index d70a5df67..35d7f28e9 100644 --- a/packages/utilities/ast/src/is-this-expression.ts +++ b/packages/utilities/ast/src/is-this-expression.ts @@ -4,9 +4,6 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { isTypeExpression } from "./is"; export function isThisExpression(node: TSESTree.Expression) { - if (isTypeExpression(node)) { - return isThisExpression(node.expression); - } - + if (isTypeExpression(node)) return isThisExpression(node.expression); return node.type === T.ThisExpression; } diff --git a/packages/utilities/var/src/index.ts b/packages/utilities/var/src/index.ts index 9b2ca8055..47119c235 100644 --- a/packages/utilities/var/src/index.ts +++ b/packages/utilities/var/src/index.ts @@ -5,7 +5,6 @@ export * from "./get-variable-declarator-id"; export * from "./get-variable-id"; export * from "./get-variable-node"; export * from "./get-variables"; -export * from "./is-initialized-from-source"; export * from "./is-node-value-equal"; export * from "./is-variable-id-equal"; export * from "./lazy-value"; diff --git a/packages/utilities/var/src/is-initialized-from-source.ts b/packages/utilities/var/src/is-initialized-from-source.ts deleted file mode 100644 index b9d59228c..000000000 --- a/packages/utilities/var/src/is-initialized-from-source.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as AST from "@eslint-react/ast"; -import { _ } from "@eslint-react/eff"; -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 { findVariable } from "./find-variable"; - -/** - * Check if an identifier is initialized from the given source - * @param name The top-level identifier's name - * @param source The import source to check against - * @param initialScope Initial scope to search for the identifier - * @returns Whether the identifier is initialized from the given source - */ -export function isInitializedFromSource( - name: string, - source: string, - initialScope: Scope, -): boolean { - const latestDef = findVariable(name, initialScope)?.defs.at(-1); - if (latestDef == null) return false; - const { node, parent } = latestDef; - if (node.type === T.VariableDeclarator && node.init != null) { - const { init } = node; - // check for: `variable = Source.variable` - if (init.type === T.MemberExpression && init.object.type === T.Identifier) { - return isInitializedFromSource(init.object.name, source, initialScope); - } - // check for: `{ variable } = Source` - if (init.type === T.Identifier) { - return isInitializedFromSource(init.name, source, initialScope); - } - // check for: `variable = require('source')` or `variable = require('source').variable` - const args = getRequireExpressionArguments(init); - const arg0 = args?.[0]; - if (arg0 == null || !AST.isStringLiteral(arg0)) { - return false; - } - // check for: `require('source')` or `require('source/...')` - return arg0.value === source - || arg0 - .value - .startsWith(`${source}/`); - } - // latest definition is an import declaration: import { variable } from 'source' - return parent?.type === T.ImportDeclaration && parent.source.value === source; -} - -function getRequireExpressionArguments(node: TSESTree.Node): TSESTree.CallExpressionArgument[] | _ { - switch (true) { - // require('source') - case node.type === T.CallExpression - && node.callee.type === T.Identifier - && node.callee.name === "require": { - return node.arguments; - } - // require('source').variable - case node.type === T.MemberExpression: { - return getRequireExpressionArguments(node.object); - } - } - return _; -}