From 7cbefbd5df1e9521154ea5036d403d932defc180 Mon Sep 17 00:00:00 2001 From: fresh3nough Date: Tue, 3 Mar 2026 18:52:00 +0000 Subject: [PATCH] fix(eslint-plugin-react-hooks): use messageId instead of inline message in rule reports Convert both RulesOfHooks and ExhaustiveDeps rules to use ESLint's messageId API with meta.messages instead of inline message strings in context.report() calls. This follows ESLint best practices and fixes issues with the VS Code ESLint extension not showing rule codes in the Problems pane, which also causes other ESLint rules in the same file to break. Fixes: https://github.com/facebook/react/issues/35897 --- .../__tests__/ESLintRulesOfHooks-test.js | 91 +++--- .../src/rules/ExhaustiveDeps.ts | 261 +++++++++++------- .../src/rules/RulesOfHooks.ts | 162 ++++++----- 3 files changed, 287 insertions(+), 227 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3d60a36824d2..ea534b6e244d 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1971,16 +1971,10 @@ const allTests = { `, // Explicitly test error messages here for various cases errors: [ - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, + useEffectEventError('onClick', true), + useEffectEventError('onClick', true), + useEffectEventError('onClick', false), + useEffectEventError('onClick', false), ], }, { @@ -2008,16 +2002,10 @@ const allTests = { `, // Explicitly test error messages here for various cases errors: [ - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, + useEffectEventError('onClick', true), + useEffectEventError('onClick', true), + useEffectEventError('onClick', false), + useEffectEventError('onClick', false), ], }, ], @@ -2025,87 +2013,80 @@ const allTests = { function conditionalError(hook, hasPreviousFinalizer = false) { return { - message: - `React Hook "${hook}" is called conditionally. React Hooks must be ` + - 'called in the exact same order in every component render.' + - (hasPreviousFinalizer + messageId: 'calledConditionally', + data: { + hookName: hook, + earlyReturnHint: hasPreviousFinalizer ? ' Did you accidentally call a React Hook after an early return?' - : ''), + : '', + }, }; } function loopError(hook) { return { - message: - `React Hook "${hook}" may be executed more than once. Possibly ` + - 'because it is called in a loop. React Hooks must be called in the ' + - 'exact same order in every component render.', + messageId: 'executedMoreThanOnce', + data: {hookName: hook}, }; } function functionError(hook, fn) { return { - message: - `React Hook "${hook}" is called in function "${fn}" that is neither ` + - 'a React function component nor a custom React Hook function.' + - ' React component names must start with an uppercase letter.' + - ' React Hook names must start with the word "use".', + messageId: 'calledInInvalidFunction', + data: {hookName: hook, functionName: fn}, }; } function genericError(hook) { return { - message: - `React Hook "${hook}" cannot be called inside a callback. React Hooks ` + - 'must be called in a React function component or a custom React ' + - 'Hook function.', + messageId: 'calledInsideCallback', + data: {hookName: hook}, }; } function topLevelError(hook) { return { - message: - `React Hook "${hook}" cannot be called at the top level. React Hooks ` + - 'must be called in a React function component or a custom React ' + - 'Hook function.', + messageId: 'calledAtTopLevel', + data: {hookName: hook}, }; } function classError(hook) { return { - message: - `React Hook "${hook}" cannot be called in a class component. React Hooks ` + - 'must be called in a React function component or a custom React ' + - 'Hook function.', + messageId: 'calledInClass', + data: {hookName: hook}, }; } function useEffectEventError(fn, called) { if (fn === null) { return { - message: - `React Hook "useEffectEvent" can only be called at the top level of your component.` + - ` It cannot be passed down.`, + messageId: 'useEffectEventTopLevel', }; } return { - message: - `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.' + - (called ? '' : ' It cannot be assigned to a variable or passed down.'), + messageId: 'useEffectEventInvalidUsage', + data: { + functionName: fn, + passDownHint: called + ? '' + : ' It cannot be assigned to a variable or passed down.', + }, }; } function asyncComponentHookError(fn) { return { - message: `React Hook "${fn}" cannot be called in an async function.`, + messageId: 'calledInAsyncFunction', + data: {hookName: fn}, }; } function tryCatchUseError(fn) { return { - message: `React Hook "${fn}" cannot be called in a try/catch block.`, + messageId: 'useInTryCatch', + data: {hookName: fn}, }; } diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..ffef7ea918f3 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -51,6 +51,83 @@ const rule = { }, fixable: 'code', hasSuggestions: true, + messages: { + asyncEffectCallback: + 'Effect callbacks are synchronous to prevent race conditions. ' + + 'Put the async function inside:\n\n' + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + '}, [someId]); // Or [] if effect doesn\'t need props or state\n\n' + + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', + refCurrentInCleanup: + "The ref value '{{dependency}}.current' will likely have " + + 'changed by the time this effect cleanup function runs. If ' + + 'this ref points to a node rendered by React, copy ' + + "'{{dependency}}.current' to a variable inside the effect, and " + + 'use that variable in the cleanup function.', + staleAssignment: + "Assignments to the '{{key}}' variable from inside React Hook " + + '{{reactiveHookName}} will be lost after each ' + + 'render. To preserve the value over time, store it in a useRef ' + + "Hook and keep the mutable value in the '.current' property. " + + 'Otherwise, you can move this variable directly inside ' + + '{{reactiveHookName}}.', + setStateInEffectWithoutDeps: + "React Hook {{reactiveHookName}} contains a call to '{{stateCall}}'. " + + 'Without a list of dependencies, this can lead to an infinite chain of updates. ' + + 'To fix this, pass [{{suggestedDeps}}] as a second argument to the {{reactiveHookName}} Hook.', + depsNotArray: + 'React Hook {{reactiveHookName}} was passed a ' + + 'dependency list that is not an array literal. This means we ' + + "can't statically verify whether you've passed the correct " + + 'dependencies.', + spreadInDeps: + 'React Hook {{reactiveHookName}} has a spread ' + + "element in its dependency array. This means we can't " + + "statically verify whether you've passed the " + + 'correct dependencies.', + useEffectEventInDeps: + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + + 'Remove `{{depName}}` from the list.', + literalInDepsWithSuggestion: + 'The {{literalRaw}} literal is not a valid dependency ' + + 'because it never changes. ' + + 'Did you mean to include {{literalValue}} in the array instead?', + literalInDeps: + 'The {{literalRaw}} literal is not a valid dependency ' + + 'because it never changes. You can safely remove it.', + complexExpressionInDeps: + 'React Hook {{reactiveHookName}} has a ' + + 'complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + constructionChangesEveryRender: + "The '{{constructionName}}' {{depType}} {{causation}} the dependencies of " + + '{{reactiveHookName}} Hook (at line {{line}}) ' + + 'change on every render. {{advice}}', + depsChanged: + 'React Hook {{reactiveHookName}} has {{problem}}{{extraWarning}}', + noCallbackProvided: + 'React Hook {{reactiveHookName}} requires an effect callback. ' + + 'Did you forget to pass a callback to the hook?', + requiresExplicitDeps: + 'React Hook {{reactiveHookName}} always requires dependencies. ' + + 'Please add a dependency array or an explicit `undefined`', + noDepsForOptimization: + 'React Hook {{reactiveHookName}} does nothing when called with ' + + 'only one argument. Did you forget to pass an array of ' + + 'dependencies?', + unknownDependencies: + 'React Hook {{reactiveHookName}} received a function whose dependencies ' + + 'are unknown. Pass an inline function instead.', + missingDependencyFallback: + "React Hook {{reactiveHookName}} has a missing dependency: '{{callbackName}}'. " + + 'Either include it or remove the dependency array.', + }, schema: [ { type: 'object', @@ -189,18 +266,7 @@ const rule = { if (isEffect && node.async) { reportProblem({ node: node, - message: - `Effect callbacks are synchronous to prevent race conditions. ` + - `Put the async function inside:\n\n` + - 'useEffect(() => {\n' + - ' async function fetchData() {\n' + - ' // You can await here\n' + - ' const response = await MyAPI.getData(someId);\n' + - ' // ...\n' + - ' }\n' + - ' fetchData();\n' + - `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + - 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', + messageId: 'asyncEffectCallback', }); } @@ -627,12 +693,8 @@ const rule = { reportProblem({ // @ts-expect-error We can do better here (dependencyNode.parent has not been type narrowed) node: dependencyNode.parent.property, - message: - `The ref value '${dependency}.current' will likely have ` + - `changed by the time this effect cleanup function runs. If ` + - `this ref points to a node rendered by React, copy ` + - `'${dependency}.current' to a variable inside the effect, and ` + - `use that variable in the cleanup function.`, + messageId: 'refCurrentInCleanup', + data: {dependency}, }); }, ); @@ -647,13 +709,11 @@ const rule = { staleAssignments.add(key); reportProblem({ node: writeExpr, - message: - `Assignments to the '${key}' variable from inside React Hook ` + - `${getSourceCode().getText(reactiveHook)} will be lost after each ` + - `render. To preserve the value over time, store it in a useRef ` + - `Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside ` + - `${getSourceCode().getText(reactiveHook)}.`, + messageId: 'staleAssignment', + data: { + key, + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } @@ -718,12 +778,12 @@ const rule = { }); reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [` + - suggestedDependencies.join(', ') + - `] as a second argument to the ${reactiveHookName} Hook.`, + messageId: 'setStateInEffectWithoutDeps', + data: { + reactiveHookName, + stateCall: setStateInsideEffectWithoutDeps, + suggestedDeps: suggestedDependencies.join(', '), + }, suggest: [ { desc: `Add dependencies array: [${suggestedDependencies.join( @@ -763,11 +823,10 @@ const rule = { // the user this in an error. reportProblem({ node: declaredDependenciesNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} was passed a ` + - 'dependency list that is not an array literal. This means we ' + - "can't statically verify whether you've passed the correct " + - 'dependencies.', + messageId: 'depsNotArray', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } else { const arrayExpression = isTSAsArrayExpression @@ -784,22 +843,20 @@ const rule = { if (declaredDependencyNode.type === 'SpreadElement') { reportProblem({ node: declaredDependencyNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has a spread ` + - "element in its dependency array. This means we can't " + - "statically verify whether you've passed the " + - 'correct dependencies.', + messageId: 'spreadInDeps', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); return; } if (useEffectEventVariables.has(declaredDependencyNode)) { reportProblem({ node: declaredDependencyNode, - message: - 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + - `Remove \`${getSourceCode().getText( - declaredDependencyNode, - )}\` from the list.`, + messageId: 'useEffectEventInDeps', + data: { + depName: getSourceCode().getText(declaredDependencyNode), + }, suggest: [ { desc: `Remove the dependency \`${getSourceCode().getText( @@ -832,26 +889,28 @@ const rule = { ) { reportProblem({ node: declaredDependencyNode, - message: - `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + - `because it never changes. ` + - `Did you mean to include ${declaredDependencyNode.value} in the array instead?`, + messageId: 'literalInDepsWithSuggestion', + data: { + literalRaw: String(declaredDependencyNode.raw), + literalValue: String(declaredDependencyNode.value), + }, }); } else { reportProblem({ node: declaredDependencyNode, - message: - `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + - 'because it never changes. You can safely remove it.', + messageId: 'literalInDeps', + data: { + literalRaw: String(declaredDependencyNode.raw), + }, }); } } else { reportProblem({ node: declaredDependencyNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has a ` + - `complex expression in the dependency array. ` + - 'Extract it to a separate variable so it can be statically checked.', + messageId: 'complexExpressionInDeps', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } @@ -935,11 +994,6 @@ const rule = { ? 'could make' : 'makes'; - const message = - `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` + - `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc?.start.line}) ` + - `change on every render. ${advice}`; - let suggest: Rule.ReportDescriptor['suggest']; // Only handle the simple case of variable assignments. // Wrapping function declarations can mess up hoisting. @@ -977,7 +1031,17 @@ const rule = { reportProblem({ // TODO: Why not report this at the dependency site? node: construction.node, - message, + messageId: 'constructionChangesEveryRender', + data: { + constructionName: construction.name.name, + depType, + causation, + reactiveHookName, + line: String( + declaredDependenciesNode.loc?.start.line, + ), + advice, + }, suggest, }); }, @@ -1289,23 +1353,32 @@ const rule = { reportProblem({ node: declaredDependenciesNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has ` + + messageId: 'depsChanged', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), // To avoid a long message, show the next actionable item. - (getWarningMessage(missingDependencies, 'a', 'missing', 'include') || - getWarningMessage( - unnecessaryDependencies, - 'an', - 'unnecessary', - 'exclude', - ) || + problem: String( getWarningMessage( - duplicateDependencies, + missingDependencies, 'a', - 'duplicate', - 'omit', - )) + + 'missing', + 'include', + ) || + getWarningMessage( + unnecessaryDependencies, + 'an', + 'unnecessary', + 'exclude', + ) || + getWarningMessage( + duplicateDependencies, + 'a', + 'duplicate', + 'omit', + ), + ), extraWarning, + }, suggest: [ { desc: `Update the dependencies array to be: [${suggestedDeps @@ -1348,9 +1421,8 @@ const rule = { if (!callback) { reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} requires an effect callback. ` + - `Did you forget to pass a callback to the hook?`, + messageId: 'noCallbackProvided', + data: {reactiveHookName}, }); return; } @@ -1358,9 +1430,8 @@ const rule = { if (!maybeNode && isEffect && options.requireExplicitEffectDeps) { reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} always requires dependencies. ` + - `Please add a dependency array or an explicit \`undefined\``, + messageId: 'requiresExplicitDeps', + data: {reactiveHookName}, }); } @@ -1385,10 +1456,8 @@ const rule = { // TODO: Can this have a suggestion? reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} does nothing when called with ` + - `only one argument. Did you forget to pass an array of ` + - `dependencies?`, + messageId: 'noDepsForOptimization', + data: {reactiveHookName}, }); } return; @@ -1452,7 +1521,8 @@ const rule = { if (def.type === 'Parameter') { reportProblem({ node: reactiveHook, - message: getUnknownDependenciesMessage(reactiveHookName), + messageId: 'unknownDependencies', + data: {reactiveHookName}, }); return; } @@ -1500,7 +1570,8 @@ const rule = { // useEffect(generateEffectBody(), []); reportProblem({ node: reactiveHook, - message: getUnknownDependenciesMessage(reactiveHookName), + messageId: 'unknownDependencies', + data: {reactiveHookName}, }); return; // Handled } @@ -1508,9 +1579,11 @@ const rule = { // Something unusual. Fall back to suggesting to add the body itself as a dep. reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` + - `Either include it or remove the dependency array.`, + messageId: 'missingDependencyFallback', + data: { + reactiveHookName, + callbackName: callback.name, + }, suggest: [ { desc: `Update the dependencies array to be: [${callback.name}]`, @@ -2128,11 +2201,5 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function getUnknownDependenciesMessage(reactiveHookName: string): string { - return ( - `React Hook ${reactiveHookName} received a function whose dependencies ` + - `are unknown. Pass an inline function instead.` - ); -} export default rule; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f55..a09ed00de96f 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -171,21 +171,6 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function useEffectEventError(fn: string | null, called: boolean): string { - // no function identifier, i.e. it is not assigned to a variable - if (fn === null) { - return ( - `React Hook "useEffectEvent" can only be called at the top level of your component.` + - ` It cannot be passed down.` - ); - } - - return ( - `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.' + - (called ? '' : ' It cannot be assigned to a variable or passed down.') - ); -} function isUseIdentifier(node: Node): boolean { return isReactFunction(node, 'use'); @@ -199,6 +184,44 @@ const rule = { recommended: true, url: 'https://react.dev/reference/rules/rules-of-hooks', }, + messages: { + useInTryCatch: + 'React Hook "{{hookName}}" cannot be called in a try/catch block.', + executedMoreThanOnce: + 'React Hook "{{hookName}}" may be executed more than once. Possibly ' + + 'because it is called in a loop. React Hooks must be called in the ' + + 'exact same order in every component render.', + calledInAsyncFunction: + 'React Hook "{{hookName}}" cannot be called in an async function.', + calledConditionally: + 'React Hook "{{hookName}}" is called conditionally. React Hooks must be ' + + 'called in the exact same order in every component render.' + + '{{earlyReturnHint}}', + calledInClass: + 'React Hook "{{hookName}}" cannot be called in a class component. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + calledInInvalidFunction: + 'React Hook "{{hookName}}" is called in function "{{functionName}}" that is neither ' + + 'a React function component nor a custom React Hook function.' + + ' React component names must start with an uppercase letter.' + + ' React Hook names must start with the word "use".', + calledAtTopLevel: + 'React Hook "{{hookName}}" cannot be called at the top level. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + calledInsideCallback: + 'React Hook "{{hookName}}" cannot be called inside a callback. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + useEffectEventTopLevel: + 'React Hook "useEffectEvent" can only be called at the top level of your component.' + + ' It cannot be passed down.', + useEffectEventInvalidUsage: + '`{{functionName}}` is a function created with React Hook "useEffectEvent", and can only be called from ' + + 'Effects and Effect Events in the same component.' + + '{{passDownHint}}', + }, schema: [ { type: 'object', @@ -621,9 +644,8 @@ const rule = { if (isUseIdentifier(hook) && isInsideTryCatch(hook)) { context.report({ node: hook, - message: `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called in a try/catch block.`, + messageId: 'useInTryCatch', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -635,13 +657,8 @@ const rule = { ) { context.report({ node: hook, - message: - `React Hook "${getSourceCode().getText( - hook, - )}" may be executed ` + - 'more than once. Possibly because it is called in a loop. ' + - 'React Hooks must be called in the exact same order in ' + - 'every component render.', + messageId: 'executedMoreThanOnce', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -657,9 +674,8 @@ const rule = { if (isAsyncFunction) { context.report({ node: hook, - message: - `React Hook "${getSourceCode().getText(hook)}" cannot be ` + - 'called in an async function.', + messageId: 'calledInAsyncFunction', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -673,15 +689,17 @@ const rule = { !isUseIdentifier(hook) && // `use(...)` can be called conditionally. !isInsideDoWhileLoop(hook) // wrapping do/while loops are checked separately. ) { - const message = - `React Hook "${getSourceCode().getText(hook)}" is called ` + - 'conditionally. React Hooks must be called in the exact ' + - 'same order in every component render.' + - (possiblyHasEarlyReturn - ? ' Did you accidentally call a React Hook after an' + - ' early return?' - : ''); - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledConditionally', + data: { + hookName: getSourceCode().getText(hook), + earlyReturnHint: possiblyHasEarlyReturn + ? ' Did you accidentally call a React Hook after an' + + ' early return?' + : '', + }, + }); } } else if ( codePathNode.parent != null && @@ -692,32 +710,28 @@ const rule = { codePathNode.parent.value === codePathNode ) { // Custom message for hooks inside a class - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'in a class component. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInClass', + data: {hookName: getSourceCode().getText(hook)}, + }); } else if (codePathFunctionName) { // Custom message if we found an invalid function name. - const message = - `React Hook "${getSourceCode().getText(hook)}" is called in ` + - `function "${getSourceCode().getText(codePathFunctionName)}" ` + - 'that is neither a React function component nor a custom ' + - 'React Hook function.' + - ' React component names must start with an uppercase letter.' + - ' React Hook names must start with the word "use".'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInInvalidFunction', + data: { + hookName: getSourceCode().getText(hook), + functionName: getSourceCode().getText(codePathFunctionName), + }, + }); } else if (codePathNode.type === 'Program') { // These are dangerous if you have inline requires enabled. - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'at the top level. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledAtTopLevel', + data: {hookName: getSourceCode().getText(hook)}, + }); } else { // Assume in all other cases the user called a hook in some // random function callback. This should usually be true for @@ -726,13 +740,11 @@ const rule = { // uncommon cases doesn't matter. // `use(...)` can be called in callbacks. if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) { - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'inside a callback. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInsideCallback', + data: {hookName: getSourceCode().getText(hook)}, + }); } } } @@ -789,11 +801,9 @@ const rule = { // like in other hooks, calling useEffectEvent at component's top level without assignment is valid node.parent?.type !== 'ExpressionStatement' ) { - const message = useEffectEventError(null, false); - context.report({ node, - message, + messageId: 'useEffectEventTopLevel', }); } }, @@ -802,14 +812,16 @@ const rule = { // This identifier resolves to a useEffectEvent function, but isn't being referenced in an // effect or another event function. It isn't being called either. if (lastEffect == null && useEffectEventFunctions.has(node)) { - const message = useEffectEventError( - getSourceCode().getText(node), - node.parent.type === 'CallExpression', - ); - context.report({ node, - message, + messageId: 'useEffectEventInvalidUsage', + data: { + functionName: getSourceCode().getText(node), + passDownHint: + node.parent.type === 'CallExpression' + ? '' + : ' It cannot be assigned to a variable or passed down.', + }, }); } },