Skip to content

feat(eslint-plugin-react-hooks): support ESLint v9 #28772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default {
],
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();

// Parse the `additionalHooks` regex.
const additionalHooks =
context.options &&
Expand Down Expand Up @@ -67,7 +69,7 @@ export default {
context.report(problem);
}

const scopeManager = context.getSourceCode().scopeManager;
const scopeManager = sourceCode.scopeManager;

// Should be shared between visitors.
const setStateCallSites = new WeakMap();
Expand Down Expand Up @@ -526,11 +528,11 @@ export default {
node: writeExpr,
message:
`Assignments to the '${key}' variable from inside React Hook ` +
`${context.getSource(reactiveHook)} will be lost after each ` +
`${sourceCode.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 ` +
`${context.getSource(reactiveHook)}.`,
`${sourceCode.getText(reactiveHook)}.`,
});
}

Expand Down Expand Up @@ -630,7 +632,7 @@ export default {
reportProblem({
node: declaredDependenciesNode,
message:
`React Hook ${context.getSource(reactiveHook)} was passed a ` +
`React Hook ${sourceCode.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.',
Expand All @@ -650,7 +652,7 @@ export default {
reportProblem({
node: declaredDependencyNode,
message:
`React Hook ${context.getSource(reactiveHook)} has a spread ` +
`React Hook ${sourceCode.getText(reactiveHook)} has a spread ` +
"element in its dependency array. This means we can't " +
"statically verify whether you've passed the " +
'correct dependencies.',
Expand All @@ -662,12 +664,12 @@ export default {
node: declaredDependencyNode,
message:
'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
`Remove \`${context.getSource(
`Remove \`${sourceCode.getText(
declaredDependencyNode,
)}\` from the list.`,
suggest: [
{
desc: `Remove the dependency \`${context.getSource(
desc: `Remove the dependency \`${sourceCode.getText(
declaredDependencyNode,
)}\``,
fix(fixer) {
Expand Down Expand Up @@ -708,7 +710,7 @@ export default {
reportProblem({
node: declaredDependencyNode,
message:
`React Hook ${context.getSource(reactiveHook)} has a ` +
`React Hook ${sourceCode.getText(reactiveHook)} has a ` +
`complex expression in the dependency array. ` +
'Extract it to a separate variable so it can be statically checked.',
});
Expand Down Expand Up @@ -978,7 +980,7 @@ export default {
` However, 'props' will change when *any* prop changes, so the ` +
`preferred fix is to destructure the 'props' object outside of ` +
`the ${reactiveHookName} call and refer to those specific props ` +
`inside ${context.getSource(reactiveHook)}.`;
`inside ${sourceCode.getText(reactiveHook)}.`;
}
}

Expand Down Expand Up @@ -1128,7 +1130,7 @@ export default {
reportProblem({
node: declaredDependenciesNode,
message:
`React Hook ${context.getSource(reactiveHook)} has ` +
`React Hook ${sourceCode.getText(reactiveHook)} has ` +
// To avoid a long message, show the next actionable item.
(getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
getWarningMessage(
Expand Down Expand Up @@ -1250,7 +1252,10 @@ export default {
return; // Handled
}
// We'll do our best effort to find it, complain otherwise.
const variable = context.getScope().set.get(callback.name);
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
const variable = scope.set.get(callback.name);
if (variable == null || variable.defs == null) {
// If it's not in scope, we don't care.
return; // Handled
Expand Down
29 changes: 18 additions & 11 deletions packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ export default {
const codePathReactHooksMapStack = [];
const codePathSegmentStack = [];
const useEffectEventFunctions = new WeakSet();
const sourceCode = context.sourceCode ?? context.getSourceCode();

function getScope(node) {
return sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
}

// For a given scope, iterate through the references and add all useEffectEvent definitions. We can
// do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions
Expand Down Expand Up @@ -466,7 +473,7 @@ export default {
context.report({
node: hook,
message:
`React Hook "${context.getSource(hook)}" may be executed ` +
`React Hook "${sourceCode.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.',
Expand All @@ -485,7 +492,7 @@ export default {
context.report({
node: hook,
message:
`React Hook "${context.getSource(hook)}" cannot be ` +
`React Hook "${sourceCode.getText(hook)}" cannot be ` +
'called in an async function.',
});
}
Expand All @@ -500,7 +507,7 @@ export default {
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
) {
const message =
`React Hook "${context.getSource(hook)}" is called ` +
`React Hook "${sourceCode.getText(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' +
'same order in every component render.' +
(possiblyHasEarlyReturn
Expand All @@ -517,15 +524,15 @@ export default {
) {
// Custom message for hooks inside a class
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
`React Hook "${sourceCode.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});
} else if (codePathFunctionName) {
// Custom message if we found an invalid function name.
const message =
`React Hook "${context.getSource(hook)}" is called in ` +
`function "${context.getSource(codePathFunctionName)}" ` +
`React Hook "${sourceCode.getText(hook)}" is called in ` +
`function "${sourceCode.getText(codePathFunctionName)}" ` +
'that is neither a React function component nor a custom ' +
'React Hook function.' +
' React component names must start with an uppercase letter.' +
Expand All @@ -534,7 +541,7 @@ export default {
} else if (codePathNode.type === 'Program') {
// These are dangerous if you have inline requires enabled.
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
`React Hook "${sourceCode.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});
Expand All @@ -547,7 +554,7 @@ export default {
// `use(...)` can be called in callbacks.
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
`React Hook "${sourceCode.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});
Expand Down Expand Up @@ -600,7 +607,7 @@ export default {
context.report({
node,
message:
`\`${context.getSource(
`\`${sourceCode.getText(
node,
)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'the same component. They cannot be assigned to variables or passed down.',
Expand All @@ -617,14 +624,14 @@ export default {
FunctionDeclaration(node) {
// function MyComponent() { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope());
recordAllUseEffectEventFunctions(getScope(node));
}
},

ArrowFunctionExpression(node) {
// const MyComponent = () => { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope());
recordAllUseEffectEventFunctions(getScope(node));
}
},
};
Expand Down