diff --git a/packages/core/src/hook/hook-hierarchy.ts b/packages/core/src/hook/hook-hierarchy.ts index ad0cf9e6dc..6e3b36d2e7 100644 --- a/packages/core/src/hook/hook-hierarchy.ts +++ b/packages/core/src/hook/hook-hierarchy.ts @@ -15,8 +15,9 @@ export function isFunctionOfUseEffectSetup(node: TSESTree.Node | _) { export function isFunctionOfUseEffectCleanup(node: TSESTree.Node | _) { if (node == null) return false; - const returnStatement = AST.findParentNode(node, AST.is(T.ReturnStatement)); - const enclosingFunction = AST.findParentNode(node, AST.isFunction); - const functionOfReturnStatement = AST.findParentNode(returnStatement, AST.isFunction); - return enclosingFunction === functionOfReturnStatement && isFunctionOfUseEffectSetup(enclosingFunction); + const pReturn = AST.findParentNode(node, AST.is(T.ReturnStatement)); + const pFaunction = AST.findParentNode(node, AST.isFunction); + const pFunctionOfReturn = AST.findParentNode(pReturn, AST.isFunction); + if (pFaunction !== pFunctionOfReturn) return false; + return isFunctionOfUseEffectSetup(pFaunction); } diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.ts index bfc31f05ae..17a8ece77b 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.ts @@ -40,13 +40,14 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? []; + const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", alias); return { CallExpression(node) { if (!ER.isReactHookCall(node)) { return; } const initialScope = context.sourceCode.getScope(node); - if (!ER.isUseCallbackCall(context, node) && !alias.some(ER.isReactHookCallWithNameLoose(node))) { + if (!isUseCallbackCall(node)) { return; } const scope = context.sourceCode.getScope(node); diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.ts index 1a06a17a23..5d21bb17ab 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.ts @@ -39,13 +39,14 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? []; + const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", alias); return { CallExpression(node) { if (!ER.isReactHookCall(node)) { return; } const initialScope = context.sourceCode.getScope(node); - if (!ER.isUseMemoCall(context, node) && !alias.some(ER.isReactHookCallWithNameLoose(node))) { + if (!isUseMemoCall(node)) { return; } const scope = context.sourceCode.getScope(node); diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.ts index e083d26d5f..af6049e448 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.ts @@ -1,7 +1,7 @@ +// Ported from https://github.com/jsx-eslint/eslint-plugin-react/pull/3579/commits/ebb739a0fe99a2ee77055870bfda9f67a2691374 import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -// Ported from https://github.com/jsx-eslint/eslint-plugin-react/pull/3579/commits/ebb739a0fe99a2ee77055870bfda9f67a2691374 import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import { getSettingsFromContext } from "@eslint-react/shared"; @@ -43,14 +43,14 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useState ?? []; + const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias); return { CallExpression(node) { if (!ER.isReactHookCall(node)) { return; } - if (!ER.isUseStateCall(context, node) && !alias.some(ER.isReactHookCallWithNameLoose(node))) { + if (!isUseStateCall(node)) { return; } const [useStateInput] = node.arguments; diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/use-state.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/use-state.ts index 54df43d8be..efac8bf06f 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/use-state.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/use-state.ts @@ -2,10 +2,11 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import * as ER from "@eslint-react/core"; +import { getSettingsFromContext } from "@eslint-react/shared"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { snakeCase } from "string-ts"; -import { match } from "ts-pattern"; +import { match } from "ts-pattern"; import { createRule } from "../utils"; export const RULE_NAME = "use-state"; @@ -35,27 +36,44 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + const alias = getSettingsFromContext(context).additionalHooks.useState ?? []; + const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias); return { - "CallExpression[callee.name='useState']"(node: TSESTree.CallExpression) { + CallExpression(node: TSESTree.CallExpression) { + if (!isUseStateCall(node)) { + return; + } if (node.parent.type !== T.VariableDeclarator) { - context.report({ messageId: "missingDestructuring", node }); + context.report({ + messageId: "missingDestructuring", + node, + }); return; } const id = ER.getInstanceId(node); if (id?.type !== T.ArrayPattern) { - context.report({ messageId: "missingDestructuring", node }); + context.report({ + messageId: "missingDestructuring", + node: id ?? node, + }); return; } const [value, setter] = id.elements; if (value == null || setter == null) { - context.report({ messageId: "missingDestructuring", node }); + context.report({ + messageId: "missingDestructuring", + node: id, + }); return; } const setterName = match(setter) .with({ type: T.Identifier }, (id) => id.name) .otherwise(() => null); if (setterName == null || !setterName.startsWith("set")) { - context.report({ messageId: "invalidSetterNaming", node }); + context.report({ + messageId: "invalidSetterNaming", + node: setter, + }); return; } const valueName = match(value) @@ -70,8 +88,18 @@ export function create(context: RuleContext): RuleListener { return values.join("_"); }) .otherwise(() => null); - if (valueName == null || `set_${valueName}` !== snakeCase(setterName)) { - context.report({ messageId: "invalidSetterNaming", node }); + if (valueName == null) { + context.report({ + messageId: "invalidSetterNaming", + node: value, + }); + return; + } + if (snakeCase(setterName) !== `set_${valueName}`) { + context.report({ + messageId: "invalidSetterNaming", + node: setter, + }); return; } }, 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 e84f64a3d3..3b9d7dac00 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 @@ -50,9 +50,10 @@ export function create(context: RuleContext): RuleListener { if (!ER.isForwardRefCall(context, node)) { return; } + const id = AST.getFunctionIdentifier(node); context.report({ messageId: "noForwardRef", - node, + node: id ?? node, fix: getFix(context, node), }); }, 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 aa57eee9ee..7f9e1e3f0b 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 @@ -38,15 +38,15 @@ export function create(context: RuleContext): RuleListener { return { JSXOpeningElement(node: TSESTree.JSXOpeningElement) { const initialScope = context.sourceCode.getScope(node); - const keyPropFound = ER.getAttribute(context, "key", node.attributes, initialScope); - const keyPropOnElement = node.attributes + const keyProp = ER.getAttribute(context, "key", node.attributes, initialScope); + const isKeyPropOnElement = node.attributes .some((n) => n.type === T.JSXAttribute && n.name.type === T.JSXIdentifier && n.name.name === "key" ); - if (keyPropFound != null && !keyPropOnElement) { - context.report({ messageId: "noImplicitKey", node: keyPropFound }); + if (keyProp != null && !isKeyPropOnElement) { + context.report({ messageId: "noImplicitKey", node: keyProp }); } }, }; 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 efd086eb07..fd809e5efe 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 @@ -55,9 +55,10 @@ export function create(context: RuleContext): RuleListener { continue; } if (displayName == null) { + const id = AST.getFunctionIdentifier(node); context.report({ messageId: "noMissingComponentDisplayName", - node, + node: id ?? node, }); } } 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 b31d814604..d2d70fab71 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 @@ -61,7 +61,7 @@ export function create(context: RuleContext): RuleListener { if (!hasDisplayNameAssignment) { context.report({ messageId: "noMissingContextDisplayName", - node: call, + node: id, }); } } 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 2253c23dad..b77dd4c320 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 @@ -39,31 +39,17 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { if (!context.sourceCode.text.includes("useContext")) return {}; const settings = getSettingsFromContext(context); - const useContextAlias = new Set(); if (compare(settings.version, "19.0.0", "<")) { return {}; } + const useContextNames = new Set(); + const hookCalls = new Set(); return { CallExpression(node) { if (!ER.isReactHookCall(node)) { return; } - if (!ER.isReactHookCallWithNameAlias(context, "useContext", [...useContextAlias])(node)) { - return; - } - context.report({ - messageId: "noUseContext", - node: node.callee, - fix(fixer) { - switch (node.callee.type) { - case T.Identifier: - return fixer.replaceText(node.callee, "use"); - case T.MemberExpression: - return fixer.replaceText(node.callee.property, "use"); - } - return null; - }, - }); + hookCalls.add(node); }, ImportDeclaration(node) { if (node.source.value !== settings.importSource) { @@ -78,7 +64,7 @@ export function create(context: RuleContext): RuleListener { // import { useContext as useCtx } from 'react' if (specifier.local.name !== "useContext") { // add alias to useContextAlias to keep track of it in future call expressions - useContextAlias.add(specifier.local.name); + useContextNames.add(specifier.local.name); } context.report({ messageId: "noUseContext", @@ -91,7 +77,7 @@ export function create(context: RuleContext): RuleListener { ...tokenBefore?.value === "," ? [fixer.replaceTextRange([tokenBefore.range[1], specifier.range[0]], "")] : [], - ...getAssociatedTokens( + ...getCorrelativeTokens( context, specifier, ).map((token) => fixer.remove(token)), @@ -103,26 +89,45 @@ export function create(context: RuleContext): RuleListener { } } }, + "Program:exit"() { + const isUseContextCall = ER.isReactHookCallWithNameAlias(context, "useContext", [...useContextNames]); + for (const node of hookCalls) { + if (!isUseContextCall(node)) { + continue; + } + context.report({ + messageId: "noUseContext", + node: node.callee, + fix(fixer) { + switch (node.callee.type) { + case T.Identifier: + return fixer.replaceText(node.callee, "use"); + case T.MemberExpression: + return fixer.replaceText(node.callee.property, "use"); + } + return null; + }, + }); + } + }, }; } -function getAssociatedTokens(context: RuleContext, node: TSESTree.Node) { - { - const tokenBefore = context.sourceCode.getTokenBefore(node); - const tokenAfter = context.sourceCode.getTokenAfter(node); - const tokens = []; - - // If this is not the only entry, then the line above this one - // will become the last line, and should not have a trailing comma. - if (tokenAfter?.value !== "," && tokenBefore?.value === ",") { - tokens.push(tokenBefore); - } +function getCorrelativeTokens(context: RuleContext, node: TSESTree.Node) { + const tokenBefore = context.sourceCode.getTokenBefore(node); + const tokenAfter = context.sourceCode.getTokenAfter(node); + const tokens = []; - // If this is not the last entry, then we need to remove the comma from this line. - if (tokenAfter?.value === ",") { - tokens.push(tokenAfter); - } + // If this is not the only entry, then the line above this one + // will become the last line, and should not have a trailing comma. + if (tokenAfter?.value !== "," && tokenBefore?.value === ",") { + tokens.push(tokenBefore); + } - return tokens; + // If this is not the last entry, then we need to remove the comma from this line. + if (tokenAfter?.value === ",") { + tokens.push(tokenAfter); } + + return tokens; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts index ce82b6c3bd..fd5af7171f 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts @@ -1,7 +1,7 @@ +// Ported from https://github.com/jsx-eslint/eslint-plugin-react/pull/3667 import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -// Ported from https://github.com/jsx-eslint/eslint-plugin-react/pull/3667 import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; @@ -46,7 +46,7 @@ export function create(context: RuleContext): RuleListener { } context.report({ messageId: "noUselessForwardRef", - node: component, + node: node.callee, }); }, }; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-boolean.ts b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-boolean.ts index c70741d5cb..eeb04c2c8e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-boolean.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-boolean.ts @@ -48,7 +48,7 @@ export function create(context: RuleContext): RuleListener { } context.report({ messageId: "preferShorthandBoolean", - node, + node: node.value ?? node, data: { propName, },