Update
This commit is contained in:
@ -7,15 +7,23 @@
|
||||
|
||||
/* global BigInt */
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
|
||||
'use strict';
|
||||
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) {
|
||||
function isHookName(s: string) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return s === 'use' || /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
@ -26,8 +34,7 @@ function isHookName(s) {
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(node) {
|
||||
function isHook(node: Node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
@ -48,16 +55,16 @@ function isHook(node) {
|
||||
* always start with an uppercase letter.
|
||||
*/
|
||||
|
||||
function isComponentName(node) {
|
||||
function isComponentName(node: Node) {
|
||||
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
|
||||
}
|
||||
|
||||
function isReactFunction(node, functionName) {
|
||||
function isReactFunction(node: Expression | Super, functionName: string) {
|
||||
return (
|
||||
node.name === functionName ||
|
||||
(node as Identifier).name === functionName ||
|
||||
(node.type === 'MemberExpression' &&
|
||||
node.object.name === 'React' &&
|
||||
node.property.name === functionName)
|
||||
(node.object as Identifier).name === 'React' &&
|
||||
(node.property as Identifier).name === functionName)
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,12 +72,10 @@ function isReactFunction(node, functionName) {
|
||||
* Checks if the node is a callback argument of forwardRef. This render function
|
||||
* should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isForwardRefCallback(node) {
|
||||
function isForwardRefCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
node.parent &&
|
||||
node.parent.callee &&
|
||||
isReactFunction(node.parent.callee, 'forwardRef')
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'forwardRef')
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,15 +84,14 @@ function isForwardRefCallback(node) {
|
||||
* functional component should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isMemoCallback(node) {
|
||||
function isMemoCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
node.parent &&
|
||||
node.parent.callee &&
|
||||
isReactFunction(node.parent.callee, 'memo')
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'memo')
|
||||
);
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node) {
|
||||
function isInsideComponentOrHook(node: Rule.Node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
@ -103,21 +107,21 @@ function isInsideComponentOrHook(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node) {
|
||||
function isUseEffectEventIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseIdentifier(node) {
|
||||
function isUseIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'use';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
@ -127,17 +131,20 @@ export default {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let lastEffect = null;
|
||||
const codePathReactHooksMapStack = [];
|
||||
const codePathSegmentStack = [];
|
||||
const useEffectEventFunctions = new WeakSet();
|
||||
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) {
|
||||
function recordAllUseEffectEventFunctions(scope: Scope.Scope) {
|
||||
for (const reference of scope.references) {
|
||||
const parent = reference.identifier.parent;
|
||||
const parent = reference.identifier.parent!;
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.init &&
|
||||
@ -145,7 +152,7 @@ export default {
|
||||
parent.init.callee &&
|
||||
isUseEffectEventIdentifier(parent.init.callee)
|
||||
) {
|
||||
for (const ref of reference.resolved.references) {
|
||||
for (const ref of reference.resolved!.references) {
|
||||
if (ref !== reference) {
|
||||
useEffectEventFunctions.add(ref.identifier);
|
||||
}
|
||||
@ -167,7 +174,7 @@ export default {
|
||||
// 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();
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop()!;
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
@ -197,7 +204,10 @@ export default {
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(segment, pathHistory) {
|
||||
function countPathsFromStart(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
) {
|
||||
const { cache } = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
@ -211,7 +221,7 @@ export default {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return BigInt('0');
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
@ -223,11 +233,11 @@ export default {
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
} else if (segment.prevSegments.length === 0) {
|
||||
paths = BigInt('1');
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment, pathList);
|
||||
}
|
||||
@ -266,7 +276,10 @@ export default {
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(segment, pathHistory) {
|
||||
function countPathsToEnd(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
): bigint {
|
||||
const { cache } = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
@ -280,7 +293,7 @@ export default {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return BigInt('0');
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
@ -292,11 +305,11 @@ export default {
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
} else if (segment.nextSegments.length === 0) {
|
||||
paths = BigInt('1');
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment, pathList);
|
||||
}
|
||||
@ -328,7 +341,7 @@ export default {
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment) {
|
||||
function shortestPathLengthToStart(segment: Rule.CodePathSegment): number {
|
||||
const { cache } = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
@ -361,9 +374,9 @@ export default {
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map();
|
||||
countPathsToEnd.cache = new Map();
|
||||
shortestPathLengthToStart.cache = new Map();
|
||||
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.
|
||||
@ -480,7 +493,7 @@ export default {
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if the hook is called inside an async function.
|
||||
const isAsyncFunction = codePathNode.async;
|
||||
const isAsyncFunction = (codePathNode as BaseFunction).async;
|
||||
if (isAsyncFunction) {
|
||||
context.report({
|
||||
node: hook,
|
||||
@ -565,8 +578,8 @@ export default {
|
||||
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 = last(codePathReactHooksMapStack);
|
||||
const codePathSegment = last(codePathSegmentStack);
|
||||
const reactHooksMap = codePathReactHooksMapStack.at(-1)!;
|
||||
const codePathSegment = codePathSegmentStack.at(-1)!;
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
@ -637,8 +650,8 @@ export default {
|
||||
* 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) {
|
||||
function getFunctionName(node: Node) {
|
||||
const parent = node.parent!;
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
@ -653,24 +666,20 @@ function getFunctionName(node) {
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
|
||||
if (parent.type === 'VariableDeclarator' && parent.init === node) {
|
||||
// const useHook = () => {};
|
||||
return node.parent.id;
|
||||
return parent.id;
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.right === node &&
|
||||
node.parent.operator === '='
|
||||
parent.type === 'AssignmentExpression' &&
|
||||
parent.right === node &&
|
||||
parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return node.parent.left;
|
||||
} else if (
|
||||
node.parent.type === 'Property' &&
|
||||
node.parent.value === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
return parent.left;
|
||||
} else if (parent.type === 'Property' && parent.value === node && !parent.computed) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return node.parent.key;
|
||||
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
|
||||
@ -679,16 +688,16 @@ function getFunctionName(node) {
|
||||
// class {useHook = () => {}}
|
||||
// class {useHook() {}}
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentPattern' &&
|
||||
node.parent.right === node &&
|
||||
!node.parent.computed
|
||||
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 node.parent.left;
|
||||
return parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@ -697,10 +706,4 @@ function getFunctionName(node) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for peeking the last item in a stack.
|
||||
*/
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
export default rule;
|
||||
|
Reference in New Issue
Block a user