|
| 1 | +import { closureCaptures } from "../../semantic/closure-captures.js"; |
| 2 | +import type { ScopeAnalysis, SymbolDescriptor } from "../../semantic/scope-analysis.js"; |
| 3 | +import type { EsTreeNode } from "../../utils/es-tree-node.js"; |
| 4 | +import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js"; |
| 5 | +import { getStaticTemplateLiteralValue } from "../../utils/get-static-template-literal-value.js"; |
| 6 | +import { isNodeOfType } from "../../utils/is-node-of-type.js"; |
| 7 | +import { |
| 8 | + getHookName, |
| 9 | + isOutsideAllFunctions, |
| 10 | + unwrapExpression, |
| 11 | +} from "./exhaustive-deps-low-level.js"; |
| 12 | + |
| 13 | +/** |
| 14 | + * Symbol-stability helpers consumed by the `exhaustive-deps` rule. |
| 15 | + * |
| 16 | + * One cohesive concept: "given a captured symbol, is its value |
| 17 | + * structurally stable across re-renders (and therefore unnecessary |
| 18 | + * in a deps array)?". The rule reads `symbolHasStableValue` / |
| 19 | + * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` / |
| 20 | + * `isRecursiveInitializerCapture` at multiple sites — extracting |
| 21 | + * them lets the rule body stay focused on the diff-the-captured-vs- |
| 22 | + * declared logic. |
| 23 | + * |
| 24 | + * Low-level helpers (`unwrapExpression`, `getHookName`, |
| 25 | + * `isOutsideAllFunctions`) live in |
| 26 | + * `./exhaustive-deps-low-level.ts` so both this module and the main |
| 27 | + * rule can import them without a circular dependency. |
| 28 | + */ |
| 29 | + |
| 30 | +/** |
| 31 | + * True for symbols whose returned value (or destructured pieces) are |
| 32 | + * stable across re-renders and don't need to live in deps arrays: |
| 33 | + * - useState's setter (`setX`) |
| 34 | + * - useReducer's dispatch |
| 35 | + * - useRef's ref object |
| 36 | + * - useEffectEvent's return value |
| 37 | + * - primitive-literal local consts (the value never changes |
| 38 | + * between renders unless the literal does) |
| 39 | + */ |
| 40 | +export const symbolHasStableHookOrigin = (symbol: SymbolDescriptor): boolean => { |
| 41 | + if (symbol.references.some((reference) => reference.flag !== "read")) return false; |
| 42 | + // We need the binding's parent context. The symbol's |
| 43 | + // declarationNode is the VariableDeclarator (when destructured) or |
| 44 | + // the binding identifier itself. |
| 45 | + let declarator: EsTreeNode | null | undefined = symbol.declarationNode; |
| 46 | + while (declarator && declarator.type !== "VariableDeclarator") { |
| 47 | + declarator = declarator.parent ?? null; |
| 48 | + } |
| 49 | + if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false; |
| 50 | + const initializerRaw = declarator.init; |
| 51 | + if (!initializerRaw) return false; |
| 52 | + const initializer = unwrapExpression(initializerRaw); |
| 53 | + |
| 54 | + // Primitive literal initializer of a `const` binding — the value |
| 55 | + // cannot change between renders, so the captured reference is |
| 56 | + // structurally stable for dep-array purposes. `let` / `var` could |
| 57 | + // be reassigned and don't qualify. |
| 58 | + if (symbol.kind === "const") { |
| 59 | + if ( |
| 60 | + isNodeOfType(initializer, "Literal") && |
| 61 | + (initializer.value === null || |
| 62 | + typeof initializer.value === "number" || |
| 63 | + typeof initializer.value === "string" || |
| 64 | + typeof initializer.value === "boolean") |
| 65 | + ) { |
| 66 | + return true; |
| 67 | + } |
| 68 | + if ( |
| 69 | + isNodeOfType(initializer, "TemplateLiteral") && |
| 70 | + getStaticTemplateLiteralValue(initializer) !== null |
| 71 | + ) { |
| 72 | + return true; |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + if (!isNodeOfType(initializer, "CallExpression")) return false; |
| 77 | + const initializerHookName = getHookName(initializer.callee); |
| 78 | + if (!initializerHookName) return false; |
| 79 | + // useRef returns a stable ref; the binding itself is the ref. |
| 80 | + if (initializerHookName === "useRef") return true; |
| 81 | + // useEffectEvent returns a stable callback (React's RFC). |
| 82 | + if (initializerHookName === "useEffectEvent") return true; |
| 83 | + // useState / useReducer: the SECOND destructure element (setter / |
| 84 | + // dispatch) is stable; the first is mutable. |
| 85 | + if ( |
| 86 | + initializerHookName === "useState" || |
| 87 | + initializerHookName === "useReducer" || |
| 88 | + initializerHookName === "useActionState" || |
| 89 | + initializerHookName === "useTransition" |
| 90 | + ) { |
| 91 | + if (!isNodeOfType(declarator.id, "ArrayPattern")) return false; |
| 92 | + const STABLE_RETURN_INDEX = 1; |
| 93 | + const elements = declarator.id.elements; |
| 94 | + const stableElement = elements[STABLE_RETURN_INDEX]; |
| 95 | + if (!stableElement) return false; |
| 96 | + const innerBinding = isNodeOfType(stableElement as EsTreeNode, "AssignmentPattern") |
| 97 | + ? (stableElement as EsTreeNodeOfType<"AssignmentPattern">).left |
| 98 | + : (stableElement as EsTreeNode); |
| 99 | + return isNodeOfType(innerBinding, "Identifier") && symbol.bindingIdentifier === innerBinding; |
| 100 | + } |
| 101 | + return false; |
| 102 | +}; |
| 103 | + |
| 104 | +export const symbolHasUseEffectEventOrigin = (symbol: SymbolDescriptor): boolean => { |
| 105 | + const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null; |
| 106 | + if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false; |
| 107 | + return getHookName(initializer.callee) === "useEffectEvent"; |
| 108 | +}; |
| 109 | + |
| 110 | +export const getFunctionValueNode = (symbol: SymbolDescriptor): EsTreeNode | null => { |
| 111 | + if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) { |
| 112 | + return symbol.declarationNode; |
| 113 | + } |
| 114 | + const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null; |
| 115 | + if ( |
| 116 | + initializer && |
| 117 | + (isNodeOfType(initializer, "FunctionExpression") || |
| 118 | + isNodeOfType(initializer, "ArrowFunctionExpression")) |
| 119 | + ) { |
| 120 | + return initializer; |
| 121 | + } |
| 122 | + return null; |
| 123 | +}; |
| 124 | + |
| 125 | +const isAstDescendant = (inner: EsTreeNode, outer: EsTreeNode): boolean => { |
| 126 | + let current: EsTreeNode | null | undefined = inner; |
| 127 | + while (current) { |
| 128 | + if (current === outer) return true; |
| 129 | + current = current.parent ?? null; |
| 130 | + } |
| 131 | + return false; |
| 132 | +}; |
| 133 | + |
| 134 | +export const isRecursiveInitializerCapture = ( |
| 135 | + symbol: SymbolDescriptor, |
| 136 | + callback: EsTreeNode, |
| 137 | +): boolean => { |
| 138 | + const initializer = symbol.initializer; |
| 139 | + return Boolean(initializer && isAstDescendant(callback, initializer)); |
| 140 | +}; |
| 141 | + |
| 142 | +const symbolHasStableFunctionOrigin = ( |
| 143 | + symbol: SymbolDescriptor, |
| 144 | + scopes: ScopeAnalysis, |
| 145 | + visitedSymbolIds: Set<number>, |
| 146 | +): boolean => { |
| 147 | + if (visitedSymbolIds.has(symbol.id)) return true; |
| 148 | + const functionNode = getFunctionValueNode(symbol); |
| 149 | + if (!functionNode) return false; |
| 150 | + visitedSymbolIds.add(symbol.id); |
| 151 | + for (const reference of closureCaptures(functionNode, scopes)) { |
| 152 | + const capturedSymbol = reference.resolvedSymbol; |
| 153 | + if (!capturedSymbol) continue; |
| 154 | + if (capturedSymbol.id === symbol.id) continue; |
| 155 | + if (isOutsideAllFunctions(capturedSymbol)) continue; |
| 156 | + if (symbolHasStableValue(capturedSymbol, scopes, visitedSymbolIds)) continue; |
| 157 | + return false; |
| 158 | + } |
| 159 | + return true; |
| 160 | +}; |
| 161 | + |
| 162 | +export const symbolHasStableValue = ( |
| 163 | + symbol: SymbolDescriptor, |
| 164 | + scopes: ScopeAnalysis, |
| 165 | + visitedSymbolIds: Set<number> = new Set(), |
| 166 | +): boolean => |
| 167 | + symbolHasStableHookOrigin(symbol) || |
| 168 | + symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds); |
0 commit comments