Inline eslint-import-resolver-typescript
This commit is contained in:
1903
packages/eslint-plugin-react-hooks/ExhaustiveDeps.ts
Normal file
1903
packages/eslint-plugin-react-hooks/ExhaustiveDeps.ts
Normal file
File diff suppressed because it is too large
Load Diff
709
packages/eslint-plugin-react-hooks/RulesOfHooks.ts
Normal file
709
packages/eslint-plugin-react-hooks/RulesOfHooks.ts
Normal file
@ -0,0 +1,709 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/* global BigInt */
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
import type { Rule, Scope } from 'eslint';
|
||||
import type {
|
||||
CallExpression,
|
||||
Expression,
|
||||
Super,
|
||||
Node,
|
||||
Identifier,
|
||||
BaseFunction,
|
||||
} from 'estree';
|
||||
import { __EXPERIMENTAL__ } from './index';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
* character to exclude identifiers like "user".
|
||||
*/
|
||||
|
||||
function isHookName(s: string) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return s === 'use' || /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
function isHook(node: Node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
isHook(node.property)
|
||||
) {
|
||||
const obj = node.object;
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a React component name. React component names must
|
||||
* always start with an uppercase letter.
|
||||
*/
|
||||
|
||||
function isComponentName(node: Node) {
|
||||
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
|
||||
}
|
||||
|
||||
function isReactFunction(node: Expression | Super, functionName: string) {
|
||||
return (
|
||||
(node as Identifier).name === functionName ||
|
||||
(node.type === 'MemberExpression' &&
|
||||
(node.object as Identifier).name === 'React' &&
|
||||
(node.property as Identifier).name === functionName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a callback argument of forwardRef. This render function
|
||||
* should follow the rules of hooks.
|
||||
*/
|
||||
function isForwardRefCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'forwardRef')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a callback argument of React.memo. This anonymous
|
||||
* functional component should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isMemoCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'memo')
|
||||
);
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node: Rule.Node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
if (isComponentName(functionName) || isHook(functionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isForwardRefCallback(node) || isMemoCallback(node)) {
|
||||
return true;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'use';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforces the Rules of Hooks',
|
||||
recommended: true,
|
||||
url: 'https://reactjs.org/docs/hooks-rules.html',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let lastEffect: CallExpression | null = null;
|
||||
const codePathReactHooksMapStack: Map<
|
||||
Rule.CodePathSegment,
|
||||
(Expression | Super)[]
|
||||
>[] = [];
|
||||
const codePathSegmentStack: Rule.CodePathSegment[] = [];
|
||||
const useEffectEventFunctions = new WeakSet<Identifier>();
|
||||
|
||||
// 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
|
||||
// can only be declared within a component or hook at its top level.
|
||||
function recordAllUseEffectEventFunctions(scope: Scope.Scope) {
|
||||
for (const reference of scope.references) {
|
||||
const parent = reference.identifier.parent!;
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.init &&
|
||||
parent.init.type === 'CallExpression' &&
|
||||
parent.init.callee &&
|
||||
isUseEffectEventIdentifier(parent.init.callee)
|
||||
) {
|
||||
for (const ref of reference.resolved!.references) {
|
||||
if (ref !== reference) {
|
||||
useEffectEventFunctions.add(ref.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
|
||||
|
||||
// Maintain code path stack as we traverse.
|
||||
onCodePathStart: () => codePathReactHooksMapStack.push(new Map()),
|
||||
|
||||
// Process our code path.
|
||||
//
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop()!;
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the segments which are cyclic are recorded in this set.
|
||||
const cyclic = new Set();
|
||||
|
||||
/**
|
||||
* Count the number of code paths from the start of the function to this
|
||||
* segment. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* } else {
|
||||
* // Segment 2
|
||||
* }
|
||||
* // Segment 3
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 1 and 2 have one path to the beginning of `MyComponent` and
|
||||
* segment 3 has two paths to the beginning of `MyComponent` since we
|
||||
* could have either taken the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
) {
|
||||
const { cache } = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
|
||||
// If `pathList` includes the current segment then we've found a cycle!
|
||||
// We need to fill `cyclic` with all segments inside cycle
|
||||
if (pathList.has(segment.id)) {
|
||||
const pathArray = [...pathList];
|
||||
const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
|
||||
for (const cyclicSegment of cyclicSegments) {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
pathList.add(segment.id);
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0n;
|
||||
} else if (segment.prevSegments.length === 0) {
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = 0n;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment, pathList);
|
||||
}
|
||||
}
|
||||
|
||||
// If our segment is reachable then there should be at least one path
|
||||
// to it from the start of our code path.
|
||||
if (segment.reachable && paths === BigInt('0')) {
|
||||
cache.delete(segment.id);
|
||||
} else {
|
||||
cache.set(segment.id, paths);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of code paths from this segment to the end of the
|
||||
* function. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* // Segment 1
|
||||
* if (condition) {
|
||||
* // Segment 2
|
||||
* } else {
|
||||
* // Segment 3
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 2 and 3 have one path to the end of `MyComponent` and
|
||||
* segment 1 has two paths to the end of `MyComponent` since we could
|
||||
* either take the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
): bigint {
|
||||
const { cache } = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
|
||||
// If `pathList` includes the current segment then we've found a cycle!
|
||||
// We need to fill `cyclic` with all segments inside cycle
|
||||
if (pathList.has(segment.id)) {
|
||||
const pathArray = Array.from(pathList);
|
||||
const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
|
||||
for (const cyclicSegment of cyclicSegments) {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
pathList.add(segment.id);
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0n;
|
||||
} else if (segment.nextSegments.length === 0) {
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = 0n;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment, pathList);
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(segment.id, paths);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shortest path length to the start of a code path.
|
||||
* For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* }
|
||||
* // Segment 2
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* There is only one path from segment 1 to the code path start. Its
|
||||
* length is one so that is the shortest path.
|
||||
*
|
||||
* There are two paths from segment 2 to the code path start. One
|
||||
* through segment 1 with a length of two and another directly to the
|
||||
* start with a length of one. The shortest path has a length of one
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment: Rule.CodePathSegment): number {
|
||||
const { cache } = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
// If `length` is null then we found a cycle! Return infinity since
|
||||
// the shortest path is definitely not the one where we looped.
|
||||
if (length === null) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// We have a cached `length`. Return it.
|
||||
if (length !== undefined) {
|
||||
return length;
|
||||
}
|
||||
|
||||
// Compute `length` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.prevSegments.length === 0) {
|
||||
length = 1;
|
||||
} else {
|
||||
length = Infinity;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
const prevLength = shortestPathLengthToStart(prevSegment);
|
||||
if (prevLength < length) {
|
||||
length = prevLength;
|
||||
}
|
||||
}
|
||||
length += 1;
|
||||
}
|
||||
cache.set(segment.id, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map<string, bigint>();
|
||||
countPathsToEnd.cache = new Map<string, bigint>();
|
||||
shortestPathLengthToStart.cache = new Map<string, number | null>();
|
||||
|
||||
// Count all code paths to the end of our component/hook. Also primes
|
||||
// the `countPathsToEnd` cache.
|
||||
const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
|
||||
|
||||
// Gets the function name for our code path. If the function name is
|
||||
// `undefined` then we know either that we have an anonymous function
|
||||
// expression or our code path is not in a function. In both cases we
|
||||
// will want to error since neither are React function components or
|
||||
// hook functions - unless it is an anonymous function argument to
|
||||
// forwardRef or memo.
|
||||
const codePathFunctionName = getFunctionName(codePathNode);
|
||||
|
||||
// This is a valid code path for React hooks if we are directly in a React
|
||||
// function component or we are in a hook function.
|
||||
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
|
||||
const isDirectlyInsideComponentOrHook = codePathFunctionName
|
||||
? isComponentName(codePathFunctionName) || isHook(codePathFunctionName)
|
||||
: isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);
|
||||
|
||||
// Compute the earliest finalizer level using information from the
|
||||
// cache. We expect all reachable final segments to have a cache entry
|
||||
// after calling `visitSegment()`.
|
||||
let shortestFinalPathLength = Infinity;
|
||||
for (const finalSegment of codePath.finalSegments) {
|
||||
if (!finalSegment.reachable) {
|
||||
continue;
|
||||
}
|
||||
const length = shortestPathLengthToStart(finalSegment);
|
||||
if (length < shortestFinalPathLength) {
|
||||
shortestFinalPathLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all React Hooks pass our lint invariants. Log warnings
|
||||
// if not.
|
||||
for (const [segment, reactHooks] of reactHooksMap) {
|
||||
// NOTE: We could report here that the hook is not reachable, but
|
||||
// that would be redundant with more general "no unreachable"
|
||||
// lint rules.
|
||||
if (!segment.reachable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are any final segments with a shorter path to start then
|
||||
// we possibly have an early return.
|
||||
//
|
||||
// If our segment is a final segment itself then siblings could
|
||||
// possibly be early returns.
|
||||
const possiblyHasEarlyReturn =
|
||||
segment.nextSegments.length === 0
|
||||
? shortestFinalPathLength <= shortestPathLengthToStart(segment)
|
||||
: shortestFinalPathLength < shortestPathLengthToStart(segment);
|
||||
|
||||
// Count all the paths from the start of our code path to the end of
|
||||
// our code path that go _through_ this segment. The critical piece
|
||||
// of this is _through_. If we just call `countPathsToEnd(segment)`
|
||||
// then we neglect that we may have gone through multiple paths to get
|
||||
// to this point! Consider:
|
||||
//
|
||||
// ```js
|
||||
// function MyComponent() {
|
||||
// if (a) {
|
||||
// // Segment 1
|
||||
// } else {
|
||||
// // Segment 2
|
||||
// }
|
||||
// // Segment 3
|
||||
// if (b) {
|
||||
// // Segment 4
|
||||
// } else {
|
||||
// // Segment 5
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// In this component we have four code paths:
|
||||
//
|
||||
// 1. `a = true; b = true`
|
||||
// 2. `a = true; b = false`
|
||||
// 3. `a = false; b = true`
|
||||
// 4. `a = false; b = false`
|
||||
//
|
||||
// From segment 3 there are two code paths to the end through segment
|
||||
// 4 and segment 5. However, we took two paths to get here through
|
||||
// segment 1 and segment 2.
|
||||
//
|
||||
// If we multiply the paths from start (two) by the paths to end (two)
|
||||
// for segment 3 we get four. Which is our desired count.
|
||||
const pathsFromStartToEnd =
|
||||
countPathsFromStart(segment) * countPathsToEnd(segment);
|
||||
|
||||
// Is this hook a part of a cyclic segment?
|
||||
const cycled = cyclic.has(segment.id);
|
||||
|
||||
for (const hook of reactHooks) {
|
||||
// Report an error if a hook may be called more then once.
|
||||
// `use(...)` can be called in loops.
|
||||
if (cycled && !isUseIdentifier(hook)) {
|
||||
context.report({
|
||||
node: hook,
|
||||
message:
|
||||
`React Hook "${context.getSource(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.',
|
||||
});
|
||||
}
|
||||
|
||||
// If this is not a valid code path for React hooks then we need to
|
||||
// log a warning for every hook in this code path.
|
||||
//
|
||||
// Pick a special message depending on the scope this hook was
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if the hook is called inside an async function.
|
||||
const isAsyncFunction = (codePathNode as BaseFunction).async;
|
||||
if (isAsyncFunction) {
|
||||
context.report({
|
||||
node: hook,
|
||||
message:
|
||||
`React Hook "${context.getSource(hook)}" cannot be ` +
|
||||
'called in an async function.',
|
||||
});
|
||||
}
|
||||
|
||||
// Report an error if a hook does not reach all finalizing code
|
||||
// path segments.
|
||||
//
|
||||
// Special case when we think there might be an early return.
|
||||
if (
|
||||
!cycled &&
|
||||
pathsFromStartToEnd !== allPathsFromStartToEnd &&
|
||||
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
|
||||
) {
|
||||
const message =
|
||||
`React Hook "${context.getSource(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 });
|
||||
}
|
||||
} else if (
|
||||
codePathNode.parent &&
|
||||
(codePathNode.parent.type === 'MethodDefinition' ||
|
||||
codePathNode.parent.type === 'ClassProperty') &&
|
||||
codePathNode.parent.value === codePathNode
|
||||
) {
|
||||
// Custom message for hooks inside a class
|
||||
const message =
|
||||
`React Hook "${context.getSource(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)}" ` +
|
||||
'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 });
|
||||
} else if (codePathNode.type === 'Program') {
|
||||
// These are dangerous if you have inline requires enabled.
|
||||
const message =
|
||||
`React Hook "${context.getSource(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 });
|
||||
} else {
|
||||
// Assume in all other cases the user called a hook in some
|
||||
// random function callback. This should usually be true for
|
||||
// anonymous function expressions. Hopefully this is clarifying
|
||||
// enough in the common case that the incorrect message in
|
||||
// uncommon cases doesn't matter.
|
||||
// `use(...)` can be called in callbacks.
|
||||
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
|
||||
const message =
|
||||
`React Hook "${context.getSource(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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Missed opportunity...We could visit all `Identifier`s instead of all
|
||||
// `CallExpression`s and check that _every use_ of a hook name is valid.
|
||||
// But that gets complicated and enters type-system territory, so we're
|
||||
// only being strict about hook calls for now.
|
||||
CallExpression(node) {
|
||||
if (isHook(node.callee)) {
|
||||
// Add the hook node to a map keyed by the code path segment. We will
|
||||
// do full code path analysis at the end of our code path.
|
||||
const reactHooksMap = codePathReactHooksMapStack.at(-1)!;
|
||||
const codePathSegment = codePathSegmentStack.at(-1)!;
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
reactHooksMap.set(codePathSegment, reactHooks);
|
||||
}
|
||||
reactHooks.push(node.callee);
|
||||
}
|
||||
|
||||
// useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in
|
||||
// another useEffectEvent
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
(node.callee.name === 'useEffect' || isUseEffectEventIdentifier(node.callee)) &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
// Denote that we have traversed into a useEffect call, and stash the CallExpr for
|
||||
// comparison later when we exit
|
||||
lastEffect = node;
|
||||
}
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
// 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) &&
|
||||
node.parent.type !== 'CallExpression'
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
`\`${context.getSource(
|
||||
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.',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'CallExpression:exit'(node) {
|
||||
if (node === lastEffect) {
|
||||
lastEffect = null;
|
||||
}
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
// function MyComponent() { const onClick = useEffectEvent(...) }
|
||||
if (isInsideComponentOrHook(node)) {
|
||||
recordAllUseEffectEventFunctions(context.getScope());
|
||||
}
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
// const MyComponent = () => { const onClick = useEffectEvent(...) }
|
||||
if (isInsideComponentOrHook(node)) {
|
||||
recordAllUseEffectEventFunctions(context.getScope());
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the static name of a function AST node. For function declarations it is
|
||||
* easy. For anonymous function expressions it is much harder. If you search for
|
||||
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our use case.
|
||||
*/
|
||||
function getFunctionName(node: Node) {
|
||||
const parent = node.parent!;
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
) {
|
||||
// function useHook() {}
|
||||
// const whatever = function useHook() {};
|
||||
//
|
||||
// Function declaration or function expression names win over any
|
||||
// assignment statements or other renames.
|
||||
return node.id;
|
||||
} else if (
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (parent.type === 'VariableDeclarator' && parent.init === node) {
|
||||
// const useHook = () => {};
|
||||
return parent.id;
|
||||
} else if (
|
||||
parent.type === 'AssignmentExpression' &&
|
||||
parent.right === node &&
|
||||
parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return parent.left;
|
||||
} else if (parent.type === 'Property' && parent.value === node && !parent.computed) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return parent.key;
|
||||
|
||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
||||
// we don't allow it to error early.
|
||||
//
|
||||
// class {useHook = () => {}}
|
||||
// class {useHook() {}}
|
||||
} else if (
|
||||
parent.type === 'AssignmentPattern' &&
|
||||
parent.right === node &&
|
||||
!parent.computed
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
return parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default rule;
|
26
packages/eslint-plugin-react-hooks/index.ts
Normal file
26
packages/eslint-plugin-react-hooks/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import type { Linter } from 'eslint';
|
||||
import RulesOfHooks from './RulesOfHooks';
|
||||
import ExhaustiveDeps from './ExhaustiveDeps';
|
||||
|
||||
export const __EXPERIMENTAL__ = false;
|
||||
|
||||
export const configs = {
|
||||
recommended: {
|
||||
plugins: ['react-hooks'],
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
} as Linter.BaseConfig,
|
||||
};
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RulesOfHooks,
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
};
|
12
packages/eslint-plugin-react-hooks/package.json
Normal file
12
packages/eslint-plugin-react-hooks/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"upstream": {
|
||||
"version": 1,
|
||||
"sources": {
|
||||
"main": {
|
||||
"repository": "git@github.com:facebook/react.git",
|
||||
"commit": "899cb95f52cc83ab5ca1eb1e268c909d3f0961e7",
|
||||
"branch": "main"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
packages/eslint-plugin-react-hooks/types.d.ts
vendored
Normal file
65
packages/eslint-plugin-react-hooks/types.d.ts
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import type { BaseNode } from 'estree';
|
||||
|
||||
declare module 'eslint' {
|
||||
namespace Rule {
|
||||
interface RuleContext {
|
||||
getSource(node: BaseNode): string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'estree' {
|
||||
interface BaseNodeWithoutComments {
|
||||
parent?: Node;
|
||||
}
|
||||
interface NodeMap {
|
||||
OptionalCallExpression: OptionalCallExpression;
|
||||
OptionalMemberExpression: OptionalMemberExpression;
|
||||
TSAsExpression: TSAsExpression;
|
||||
TSTypeQuery: TSTypeQuery;
|
||||
TSTypeReference: TSTypeReference;
|
||||
TypeCastExpression: TypeCastExpression;
|
||||
}
|
||||
interface ExpressionMap {
|
||||
OptionalCallExpression: OptionalCallExpression;
|
||||
OptionalMemberExpression: OptionalMemberExpression;
|
||||
TSAsExpression: TSAsExpression;
|
||||
TypeCastExpression: TypeCastExpression;
|
||||
}
|
||||
interface AssignmentPattern {
|
||||
computed?: boolean;
|
||||
}
|
||||
interface ChainExpression {
|
||||
computed?: boolean;
|
||||
}
|
||||
interface TypeCastExpression extends BaseNode {
|
||||
type: 'TypeCastExpression';
|
||||
expression: Expression;
|
||||
}
|
||||
interface TSAsExpression extends BaseNode {
|
||||
type: 'TSAsExpression';
|
||||
expression: Expression;
|
||||
}
|
||||
interface TSTypeQuery extends BaseNode {
|
||||
type: 'TSTypeQuery';
|
||||
}
|
||||
interface TSTypeReference extends BaseNode {
|
||||
type: 'TSTypeReference';
|
||||
}
|
||||
/** @deprecated flow only */
|
||||
interface TypeParameter extends BaseNode {
|
||||
type: 'TypeParameter';
|
||||
}
|
||||
interface OptionalMemberExpression extends BaseNode {
|
||||
type: 'OptionalMemberExpression';
|
||||
object: Expression | Super;
|
||||
property: Expression | PrivateIdentifier;
|
||||
computed: boolean;
|
||||
optional: boolean;
|
||||
}
|
||||
interface OptionalCallExpression extends BaseNode {
|
||||
type: 'OptionalCallExpression';
|
||||
callee: Expression | Super;
|
||||
arguments: (Expression | SpreadElement)[];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user