Mashiro

Markdown Style Guide

Mashiro

This page is a visual regression fixture for the Markdown elements supported by this site.

Heading One ​

A paragraph can include strong text, emphasis, both, strikethrough, inline code, Ctrl + K, and an escaped character: *not emphasis*.

Inline link, https://vitepress.dev/, reference link, and an external link with code.

Local favicon

Heading Two ​

Heading anchors, section spacing, and the absence of a divider are checked here.

Heading Three ​

Heading Four ​

Heading Five ​
Heading Six ​

Setext Heading One ​

Setext Heading Two ​


Paragraphs and Quotes ​

Separate paragraphs retain vertical rhythm.

This paragraph has a hard line break after this sentence.
This line starts after the break.

A blockquote supports formatting, links, and inline code.

Nested quotations remain readable.

A final quoted paragraph.

Native HTML details

This is standard HTML embedded in Markdown.

Lists and Tasks ​

  • Unordered item
    • Nested item
      • Nested item with code
  • Another item
  1. Ordered item
  2. Another item
    1. Nested ordered item
    2. Second nested ordered item
  3. Final item

Table ​

AlignmentPlain textInline formatting
LeftCenterRight
codeboldlink

Code ​

Inline const answer = 42 code should wrap when its content is long: thisIsAnIntentionallyLongInlineCodeSampleThatShouldWrapInsteadOfOverflowingItsContainer.

ts
1const greeting = "Hello, Markdown"
2console.log(greeting)
ts
1// highlighted line
2const active = true
3console.log(active)
ts
1const previous = "before"
2const next = "after"
3const warning = "check this"
4const error = "this failed"
ts
1const ordinary = "dimmed"
2const highlighted = "emphasized"
3const focused = "sharp"
ts
1const first = "dimmed"
2const second = "focused range"
3const third = "focused range"
4const fourth = "dimmed"
ts
1const inactive = false
2const active = true
3console.log(active)
bash
1bun run typecheck
2bun run build
ts
1import { defineConfig } from "vitepress"
2
3export default defineConfig({})
ts
1export const message: string = "Hello"
js
1export const message = "Hello"
json
1{
2  "message": "Hello"
3}

Custom Containers ​

INFO

Information container with a link and inline code.

Optional Title

A note container with a custom title.

TIP

A tip container.

IMPORTANT

An important container.

WARNING

A warning container.

DANGER

A danger container.

CAUTION

A caution container.

Expandable Details

A details container with formatting, a link, and code.

ts
1const expanded = true

Footnotes ​

A footnote reference appears here.[1]

Raw HTML ​

Marked text and superscript can be rendered through HTML.

This block is intentionally raw HTML.

ts
1/**
2 * Vendored from React. See ./exhaustiveDeps.md and update that file for every
3 * future maintenance change to this vendored rule.
4 *
5 * Copyright (c) Meta Platforms, Inc. and affiliates.
6 *
7 * This source code is licensed under the MIT license found in the
8 * LICENSE file in the root directory of this source tree.
9 */
10/* eslint-disable max-lines, max-lines-per-function, eqeqeq, prefer-template */
11/* eslint-disable no-for-of-loops/no-for-of-loops */
12import type { Rule, Scope } from 'eslint';
13import type {
14  ArrayExpression,
15  ArrowFunctionExpression,
16  CallExpression,
17  Expression,
18  FunctionDeclaration,
19  FunctionExpression,
20  Identifier,
21  Node,
22  Pattern,
23  PrivateIdentifier,
24  Super,
25  VariableDeclarator,
26} from 'estree';
27
28// @ts-expect-error Oxlint loads local JS plugins through Node ESM, which requires the explicit .ts extension.
29import { getAdditionalEffectHooksFromSettings } from './utils.ts';
30
31type AstNode = {
32  type: string;
33  parent?: AstNode | null;
34  range?: [number, number];
35  computed?: boolean;
36  optional?: boolean;
37  value?: unknown;
38  name?: string;
39  kind?: string;
40  id?: AstNode | null;
41  init?: AstNode | null;
42  expression?: AstNode;
43  object?: AstNode;
44  property?: AstNode;
45  callee?: AstNode;
46  left?: AstNode;
47  right?: AstNode;
48  block?: AstNode;
49  elements?: Array<AstNode | null>;
50  arguments?: Array<AstNode | null>;
51  properties?: Array<AstNode | null>;
52  key?: AstNode;
53  [key: string]: unknown;
54};
55
56function asAstNode(node: unknown): AstNode {
57  return node as AstNode;
58}
59
60function isTypeCastExpression(
61  node: AstNode | null | undefined,
62): node is AstNode & { expression: AstNode } {
63  return (
64    node != null &&
65    (node.type === 'TSAsExpression' ||
66      node.type === 'AsExpression' ||
67      node.type === 'TypeCastExpression')
68  );
69}
70
71type DeclaredDependency = {
72  key: string;
73  node: Node;
74};
75
76type Dependency = {
77  isStable: boolean;
78  references: Array<Scope.Reference>;
79};
80
81type DependencyTreeNode = {
82  isUsed: boolean; // True if used in code
83  isSatisfiedRecursively: boolean; // True if specified in deps
84  isSubtreeUsed: boolean; // True if something deeper is used by code
85  children: Map<string, DependencyTreeNode>; // Nodes for properties
86};
87
88type AdditionalHookDependencyPathSegment = number | string;
89
90type AdditionalHookOption = {
91  dependencyPath: AdditionalHookDependencyPathSegment[];
92  dependencyRequired?: boolean;
93  hook: string;
94};
95
96type NormalizedAdditionalHook = {
97  dependencyPath?: ReadonlyArray<AdditionalHookDependencyPathSegment>;
98  dependencyRequired: boolean;
99  hook: string;
100  treatAsEffect: boolean;
101};
102
103type NormalizedAdditionalHooks = {
104  configuredHooks: Map<string, NormalizedAdditionalHook>;
105  regex: RegExp | undefined;
106};
107
108type ReactiveHookConfiguration = {
109  callbackIndex: 0 | 1;
110  dependencyPath?: ReadonlyArray<AdditionalHookDependencyPathSegment>;
111  dependencyRequired?: boolean;
112  treatAsEffect?: boolean;
113};
114
115const rule = {
116  meta: {
117    type: 'suggestion',
118    docs: {
119      description: 'verifies the list of dependencies for Hooks like useEffect and similar',
120      recommended: true,
121      url: 'https://github.com/facebook/react/issues/14920',
122    },
123    fixable: 'code',
124    hasSuggestions: true,
125    schema: [
126      {
127        type: 'object',
128        additionalProperties: false,
129        enableDangerousAutofixThisMayCauseInfiniteLoops: false,
130        properties: {
131          additionalHooks: {
132            oneOf: [
133              {
134                type: 'string',
135              },
136              {
137                type: 'array',
138                items: {
139                  oneOf: [
140                    {
141                      type: 'string',
142                    },
143                    {
144                      type: 'object',
145                      additionalProperties: false,
146                      required: ['hook', 'dependencyPath'],
147                      properties: {
148                        hook: {
149                          type: 'string',
150                        },
151                        dependencyPath: {
152                          type: 'array',
153                          items: {
154                            anyOf: [
155                              {
156                                type: 'integer',
157                              },
158                              {
159                                type: 'string',
160                              },
161                            ],
162                          },
163                        },
164                        dependencyRequired: {
165                          type: 'boolean',
166                        },
167                      },
168                    },
169                  ],
170                },
171              },
172            ],
173          },
174          enableDangerousAutofixThisMayCauseInfiniteLoops: {
175            type: 'boolean',
176          },
177          experimental_autoDependenciesHooks: {
178            type: 'array',
179            items: {
180              type: 'string',
181            },
182          },
183          temporaryChineseMessage: {
184            type: 'boolean',
185          },
186          requireExplicitEffectDeps: {
187            type: 'boolean',
188          },
189        },
190      },
191    ],
192  },
193  create(context: Rule.RuleContext) {
194    const rawOptions = context.options && context.options[0];
195    const settings = context.settings || {};
196    const sourceCode = context.getSourceCode();
197
198    // Parse the `additionalHooks` regex.
199    // Use rule-level additionalHooks if provided, otherwise fall back to settings
200    const additionalHooks = normalizeAdditionalHooksOption(rawOptions?.additionalHooks, settings);
201
202    const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean =
203      (rawOptions && rawOptions.enableDangerousAutofixThisMayCauseInfiniteLoops) || false;
204
205    const experimental_autoDependenciesHooks: ReadonlyArray<string> =
206      rawOptions && Array.isArray(rawOptions.experimental_autoDependenciesHooks)
207        ? rawOptions.experimental_autoDependenciesHooks
208        : [];
209
210    const requireExplicitEffectDeps: boolean =
211      (rawOptions && rawOptions.requireExplicitEffectDeps) || false;
212
213    const temporaryChineseMessage: boolean =
214      (rawOptions && rawOptions.temporaryChineseMessage) || false;
215
216    const options = {
217      additionalHooks,
218      experimental_autoDependenciesHooks,
219      enableDangerousAutofixThisMayCauseInfiniteLoops,
220      temporaryChineseMessage,
221      requireExplicitEffectDeps,
222    };
223
224    function reportProblem(problem: Rule.ReportDescriptor) {
225      if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
226        // Used to enable legacy behavior. Dangerous.
227        // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
228        if (Array.isArray(problem.suggest) && problem.suggest.length > 0 && problem.suggest[0]) {
229          problem.fix = problem.suggest[0].fix;
230        }
231      }
232
233      context.report(problem);
234    }
235
236    const getScope = (node: Node) => {
237      return sourceCode.getScope(node);
238    };
239
240    const scopeManager = sourceCode.scopeManager;
241
242    // Should be shared between visitors.
243    const setStateCallSites = new WeakMap<Expression | Super, Pattern | null | undefined>();
244    const stateVariables = new WeakSet<Identifier>();
245    const stableKnownValueCache = new WeakMap<Scope.Variable, boolean>();
246    const functionWithoutCapturedValueCache = new WeakMap<Scope.Variable, boolean>();
247    const useEffectEventVariables = new WeakSet<Expression>();
248
249    function memoizeWithWeakMap(
250      fn: (resolved: Scope.Variable) => boolean,
251      map: WeakMap<Scope.Variable, boolean>,
252    ) {
253      return function (arg: Scope.Variable): boolean {
254        if (map.has(arg)) {
255          // to verify cache hits:
256          // console.log(arg.name)
257          return map.get(arg)!;
258        }
259
260        const result = fn(arg);
261
262        map.set(arg, result);
263
264        return result;
265      };
266    }
267
268    /**
269     * Visitor for both function expressions and arrow function expressions.
270     */
271    function visitFunctionWithDependencies(
272      node: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression,
273      declaredDependenciesNode: Node | undefined,
274      reactiveHook: Node,
275      reactiveHookName: string,
276      isEffect: boolean,
277      isAutoDepsHook: boolean,
278    ): void {
279      if (isEffect && node.async) {
280        reportProblem({
281          node: node,
282          message:
283            `Effect callbacks are synchronous to prevent race conditions. ` +
284            `Put the async function inside:\n\n` +
285            'useEffect(() => {\n' +
286            '  async function fetchData() {\n' +
287            '    // You can await here\n' +
288            '    const response = await MyAPI.getData(someId);\n' +
289            '    // ...\n' +
290            '  }\n' +
291            '  fetchData();\n' +
292            `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
293            'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching',
294        });
295      }
296
297      // Get the current scope.
298      const scope = scopeManager.acquire(node);
299
300      if (!scope) {
301        throw new Error(
302          'Unable to acquire scope for the current node. This is a bug in eslint-plugin-react-hooks, please file an issue.',
303        );
304      }
305
306      // Find all our "pure scopes". On every re-render of a component these
307      // pure scopes may have changes to the variables declared within. So all
308      // variables used in our reactive hook callback but declared in a pure
309      // scope need to be listed as dependencies of our reactive hook callback.
310      //
311      // According to the rules of React you can't read a mutable value in pure
312      // scope. We can't enforce this in a lint so we trust that all variables
313      // declared outside of pure scope are indeed frozen.
314      const pureScopes = new Set();
315      let componentScope: Scope.Scope | null = null;
316
317      {
318        let currentScope = scope.upper;
319
320        while (currentScope) {
321          pureScopes.add(currentScope);
322          const currentScopeType = currentScope.type as string;
323
324          if (
325            currentScopeType === 'function' ||
326            currentScopeType === 'hook' ||
327            currentScopeType === 'component'
328          ) {
329            break;
330          }
331
332          currentScope = currentScope.upper;
333        }
334
335        // If there is no parent function scope then there are no pure scopes.
336        // The ones we've collected so far are incorrect. So don't continue with
337        // the lint.
338        if (!currentScope) {
339          return;
340        }
341
342        componentScope = currentScope;
343      }
344
345      const isArray = Array.isArray;
346
347      // Next we'll define a few helpers that helps us
348      // tell if some values don't have to be declared as deps.
349
350      // Some are known to be stable based on Hook calls.
351      // const [state, setState] = useState() / React.useState()
352      //               ^^^ true for this reference
353      // const [state, dispatch] = useReducer() / React.useReducer()
354      //               ^^^ true for this reference
355      // const [state, dispatch] = useActionState() / React.useActionState()
356      //               ^^^ true for this reference
357      // const ref = useRef()
358      //       ^^^ true for this reference
359      // const onStuff = useEffectEvent(() => {})
360      //       ^^^ true for this reference
361      // False for everything else.
362      function isStableKnownHookValue(resolved: Scope.Variable): boolean {
363        if (!isArray(resolved.defs)) {
364          return false;
365        }
366
367        const def = resolved.defs[0];
368
369        if (def == null) {
370          return false;
371        }
372
373        // Look for `let stuff = ...`
374        const defNode: VariableDeclarator = def.node;
375
376        if (defNode.type !== 'VariableDeclarator') {
377          return false;
378        }
379
380        let init = defNode.init == null ? null : asAstNode(defNode.init);
381
382        if (init == null) {
383          return false;
384        }
385
386        while (isTypeCastExpression(init)) {
387          init = init.expression;
388        }
389
390        // Detect primitive constants
391        // const foo = 42
392        let declaration = asAstNode(defNode).parent;
393
394        if (declaration == null && componentScope != null) {
395          // This might happen if variable is declared after the callback.
396          // In that case ESLint won't set up .parent refs.
397          // So we'll set them up manually.
398          fastFindReferenceWithParent(componentScope.block, def.node.id);
399          declaration = asAstNode(def.node).parent;
400
401          if (declaration == null) {
402            return false;
403          }
404        }
405
406        if (
407          declaration != null &&
408          'kind' in declaration &&
409          declaration.kind === 'const' &&
410          init.type === 'Literal' &&
411          (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)
412        ) {
413          // Definitely stable
414          return true;
415        }
416
417        // Detect known Hook calls
418        // const [_, setState] = useState()
419        if (init.type !== 'CallExpression') {
420          return false;
421        }
422
423        const initCallee = init.callee;
424
425        if (initCallee == null) {
426          return false;
427        }
428
429        let callee: Expression | PrivateIdentifier | Super = initCallee as
430          | Expression
431          | PrivateIdentifier
432          | Super;
433
434        // Step into `= React.something` initializer.
435        if (
436          callee.type === 'MemberExpression' &&
437          'name' in callee.object &&
438          callee.object.name === 'React' &&
439          callee.property != null &&
440          !callee.computed
441        ) {
442          callee = callee.property;
443        }
444
445        if (callee.type !== 'Identifier') {
446          return false;
447        }
448
449        const definitionNode: VariableDeclarator = def.node;
450        const id = definitionNode.id;
451        const { name } = callee;
452
453        if (name === 'useRef' && id.type === 'Identifier') {
454          // useRef() return value is stable.
455          return true;
456        } else if (isUseEffectEventIdentifier(callee) && id.type === 'Identifier') {
457          for (const ref of resolved.references) {
458            // @ts-expect-error These types are not compatible (Reference and Identifier)
459            if (ref !== id) {
460              useEffectEventVariables.add(ref.identifier);
461            }
462          }
463
464          // useEffectEvent() return value is always unstable.
465          return true;
466        } else if (name === 'useState' || name === 'useReducer' || name === 'useActionState') {
467          // Only consider second value in initializing tuple stable.
468          if (
469            id.type === 'ArrayPattern' &&
470            id.elements.length === 2 &&
471            isArray(resolved.identifiers)
472          ) {
473            // Is second tuple value the same reference we're checking?
474            if (id.elements[1] === resolved.identifiers[0]) {
475              if (name === 'useState') {
476                const references = resolved.references;
477                let writeCount = 0;
478
479                for (const reference of references) {
480                  if (reference.isWrite()) {
481                    writeCount++;
482                  }
483
484                  if (writeCount > 1) {
485                    return false;
486                  }
487
488                  setStateCallSites.set(reference.identifier, id.elements[0]);
489                }
490              }
491
492              // Setter is stable.
493              return true;
494            } else if (id.elements[0] === resolved.identifiers[0]) {
495              if (name === 'useState') {
496                const references = resolved.references;
497
498                for (const reference of references) {
499                  stateVariables.add(reference.identifier);
500                }
501              }
502
503              // State variable itself is dynamic.
504              return false;
505            }
506          }
507        } else if (name === 'useTransition') {
508          // Only consider second value in initializing tuple stable.
509          if (
510            id.type === 'ArrayPattern' &&
511            id.elements.length === 2 &&
512            Array.isArray(resolved.identifiers)
513          ) {
514            // Is second tuple value the same reference we're checking?
515            if (id.elements[1] === resolved.identifiers[0]) {
516              // Setter is stable.
517              return true;
518            }
519          }
520        }
521
522        // By default assume it's dynamic.
523        return false;
524      }
525
526      // Some are just functions that don't reference anything dynamic.
527      function isFunctionWithoutCapturedValues(resolved: Scope.Variable): boolean {
528        if (!isArray(resolved.defs)) {
529          return false;
530        }
531
532        const def = resolved.defs[0];
533
534        if (def == null) {
535          return false;
536        }
537
538        if (def.node == null || def.node.id == null) {
539          return false;
540        }
541
542        // Search the direct component subscopes for
543        // top-level function definitions matching this reference.
544        const fnNode = asAstNode(def.node);
545        const childScopes = componentScope?.childScopes || [];
546        let fnScope = null;
547
548        for (const childScope of childScopes) {
549          const childScopeBlock = asAstNode(childScope.block);
550
551          if (
552            // function handleChange() {}
553            (fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode) ||
554            // const handleChange = () => {}
555            // const handleChange = function() {}
556            (fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode)
557          ) {
558            // Found it!
559            fnScope = childScope;
560            break;
561          }
562        }
563
564        if (fnScope == null) {
565          return false;
566        }
567
568        // Does this function capture any values
569        // that are in pure scopes (aka render)?
570        for (const ref of fnScope.through) {
571          if (ref.resolved == null) {
572            continue;
573          }
574
575          if (
576            pureScopes.has(ref.resolved.scope) &&
577            // Stable values are fine though,
578            // although we won't check functions deeper.
579            !memoizedIsStableKnownHookValue(ref.resolved)
580          ) {
581            return false;
582          }
583        }
584
585        // If we got here, this function doesn't capture anything
586        // from render--or everything it captures is known stable.
587        return true;
588      }
589
590      // Remember such values. Avoid re-running extra checks on them.
591      const memoizedIsStableKnownHookValue = memoizeWithWeakMap(
592        isStableKnownHookValue,
593        stableKnownValueCache,
594      );
595      const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
596        isFunctionWithoutCapturedValues,
597        functionWithoutCapturedValueCache,
598      );
599
600      // These are usually mistaken. Collect them.
601      const currentRefsInEffectCleanup = new Map<
602        string,
603        {
604          reference: Scope.Reference;
605          dependencyNode: AstNode;
606        }
607      >();
608
609      // Is this reference inside a cleanup function for this effect node?
610      // We can check by traversing scopes upwards from the reference, and checking
611      // if the last "return () => " we encounter is located directly inside the effect.
612      function isInsideEffectCleanup(reference: Scope.Reference): boolean {
613        let curScope: Scope.Scope | null = reference.from;
614        let isInReturnedFunction = false;
615
616        while (curScope != null && curScope.block !== node) {
617          if (curScope.type === 'function') {
618            const scopeBlock = asAstNode(curScope.block);
619
620            isInReturnedFunction =
621              scopeBlock.parent != null && scopeBlock.parent.type === 'ReturnStatement';
622          }
623
624          curScope = curScope.upper;
625        }
626
627        return isInReturnedFunction;
628      }
629
630      // Get dependencies from all our resolved references in pure scopes.
631      // Key is dependency string, value is whether it's stable.
632      const dependencies = new Map<string, Dependency>();
633      const optionalChains = new Map<string, boolean>();
634
635      gatherDependenciesRecursively(scope);
636
637      function gatherDependenciesRecursively(currentScope: Scope.Scope): void {
638        for (const reference of currentScope.references) {
639          // If this reference is not resolved or it is not declared in a pure
640          // scope then we don't care about this reference.
641          if (!reference.resolved) {
642            continue;
643          }
644
645          if (!pureScopes.has(reference.resolved.scope)) {
646            continue;
647          }
648
649          // Narrow the scope of a dependency if it is, say, a member expression.
650          // Then normalize the narrowed dependency.
651          const referenceNode = fastFindReferenceWithParent(node, reference.identifier);
652
653          if (referenceNode == null) {
654            continue;
655          }
656
657          const dependencyNode = getDependency(referenceNode);
658          const dependency = analyzePropertyChain(dependencyNode, optionalChains);
659          const dependencyParent = dependencyNode.parent;
660
661          // Accessing ref.current inside effect cleanup is bad.
662          if (
663            // We're in an effect...
664            isEffect &&
665            // ... and this look like accessing .current...
666            dependencyNode.type === 'Identifier' &&
667            (dependencyParent?.type === 'MemberExpression' ||
668              dependencyParent?.type === 'OptionalMemberExpression') &&
669            !dependencyParent.computed &&
670            dependencyParent.property?.type === 'Identifier' &&
671            dependencyParent.property.name === 'current' &&
672            // ...in a cleanup function or below...
673            isInsideEffectCleanup(reference)
674          ) {
675            currentRefsInEffectCleanup.set(dependency, {
676              reference,
677              dependencyNode,
678            });
679          }
680
681          if (
682            dependencyParent?.type === 'TSTypeQuery' ||
683            dependencyParent?.type === 'TSTypeReference'
684          ) {
685            continue;
686          }
687
688          const def = reference.resolved.defs[0];
689
690          if (def == null) {
691            continue;
692          }
693
694          // Ignore references to the function itself as it's not defined yet.
695          if (def.node != null && def.node.init === asAstNode(node).parent) {
696            continue;
697          }
698
699          // Ignore Flow type parameters
700          if (
701            // @ts-expect-error We don't have flow types
702            def.type === 'TypeParameter' ||
703            dependencyParent?.type === 'GenericTypeAnnotation'
704          ) {
705            continue;
706          }
707
708          // Add the dependency to a map so we can make sure it is referenced
709          // again in our dependencies array. Remember whether it's stable.
710          if (!dependencies.has(dependency)) {
711            const resolved = reference.resolved;
712            const isStable =
713              memoizedIsStableKnownHookValue(resolved) ||
714              memoizedIsFunctionWithoutCapturedValues(resolved);
715
716            dependencies.set(dependency, {
717              isStable,
718              references: [reference],
719            });
720          } else {
721            dependencies.get(dependency)?.references.push(reference);
722          }
723        }
724
725        for (const childScope of currentScope.childScopes) {
726          gatherDependenciesRecursively(childScope);
727        }
728      }
729
730      // Warn about accessing .current in cleanup effects.
731      currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => {
732        const dependencyParent = dependencyNode.parent;
733        const references = reference.resolved?.references || [];
734        // Is React managing this ref or us?
735        // Let's see if we can find a .current assignment.
736        let foundCurrentAssignment = false;
737
738        for (const ref of references) {
739          const { parent } = asAstNode(ref.identifier);
740
741          if (
742            parent != null &&
743            // ref.current
744            // Note: no need to handle OptionalMemberExpression because it can't be LHS.
745            parent.type === 'MemberExpression' &&
746            !parent.computed &&
747            parent.property?.type === 'Identifier' &&
748            parent.property.name === 'current' &&
749            // ref.current = <something>
750            parent.parent?.type === 'AssignmentExpression' &&
751            parent.parent.left === parent
752          ) {
753            foundCurrentAssignment = true;
754            break;
755          }
756        }
757
758        // We only want to warn about React-managed refs.
759        if (foundCurrentAssignment) {
760          return;
761        }
762
763        reportProblem({
764          node: dependencyParent?.property as Node,
765          message:
766            `The ref value '${dependency}.current' will likely have ` +
767            `changed by the time this effect cleanup function runs. If ` +
768            `this ref points to a node rendered by React, copy ` +
769            `'${dependency}.current' to a variable inside the effect, and ` +
770            `use that variable in the cleanup function.`,
771        });
772      });
773
774      // Warn about assigning to variables in the outer scope.
775      // Those are usually bugs.
776      const staleAssignments = new Set<string>();
777
778      function reportStaleAssignment(writeExpr: Node, key: string): void {
779        if (staleAssignments.has(key)) {
780          return;
781        }
782
783        staleAssignments.add(key);
784        reportProblem({
785          node: writeExpr,
786          message:
787            `Assignments to the '${key}' variable from inside React Hook ` +
788            `${sourceCode.getText(reactiveHook)} will be lost after each ` +
789            `render. To preserve the value over time, store it in a useRef ` +
790            `Hook and keep the mutable value in the '.current' property. ` +
791            `Otherwise, you can move this variable directly inside ` +
792            `${sourceCode.getText(reactiveHook)}.`,
793        });
794      }
795
796      // Remember which deps are stable and report bad usage first.
797      const stableDependencies = new Set<string>();
798
799      dependencies.forEach(({ isStable, references }, key) => {
800        if (isStable) {
801          stableDependencies.add(key);
802        }
803
804        references.forEach((reference) => {
805          if (reference.writeExpr) {
806            reportStaleAssignment(reference.writeExpr, key);
807          }
808        });
809      });
810
811      if (staleAssignments.size > 0) {
812        // The intent isn't clear so we'll wait until you fix those first.
813        return;
814      }
815
816      if (!declaredDependenciesNode) {
817        if (isAutoDepsHook) {
818          return;
819        }
820
821        // Check if there are any top-level setState() calls.
822        // Those tend to lead to infinite loops.
823        let setStateInsideEffectWithoutDeps: string | null = null;
824
825        dependencies.forEach(({ references }, key) => {
826          if (setStateInsideEffectWithoutDeps) {
827            return;
828          }
829
830          references.forEach((reference) => {
831            if (setStateInsideEffectWithoutDeps) {
832              return;
833            }
834
835            const id = reference.identifier;
836            const isSetState = setStateCallSites.has(id);
837
838            if (!isSetState) {
839              return;
840            }
841
842            let fnScope: Scope.Scope | null = reference.from;
843
844            while (fnScope != null && fnScope.type !== 'function') {
845              fnScope = fnScope.upper;
846            }
847
848            const isDirectlyInsideEffect = fnScope?.block === node;
849
850            if (isDirectlyInsideEffect) {
851              // TODO: we could potentially ignore early returns.
852              setStateInsideEffectWithoutDeps = key;
853            }
854          });
855        });
856
857        if (setStateInsideEffectWithoutDeps) {
858          const { suggestedDependencies } = collectRecommendations({
859            dependencies,
860            declaredDependencies: [],
861            stableDependencies,
862            externalDependencies: new Set<string>(),
863            isEffect: true,
864          });
865
866          reportProblem({
867            node: reactiveHook,
868            message:
869              `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` +
870              `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
871              `To fix this, pass [` +
872              suggestedDependencies.join(', ') +
873              `] as a second argument to the ${reactiveHookName} Hook.`,
874            suggest: [
875              {
876                desc: `Add dependencies array: [${suggestedDependencies.join(', ')}]`,
877                fix(fixer) {
878                  return fixer.insertTextAfter(node, `, [${suggestedDependencies.join(', ')}]`);
879                },
880              },
881            ],
882          });
883        }
884
885        return;
886      }
887
888      if (
889        isAutoDepsHook &&
890        declaredDependenciesNode.type === 'Literal' &&
891        declaredDependenciesNode.value === null
892      ) {
893        return;
894      }
895
896      const declaredDependencies: Array<DeclaredDependency> = [];
897      const externalDependencies = new Set<string>();
898      const astDeclaredDependenciesNode = asAstNode(declaredDependenciesNode);
899      const isArrayExpression = astDeclaredDependenciesNode.type === 'ArrayExpression';
900      const isTSAsArrayExpression =
901        isTypeCastExpression(astDeclaredDependenciesNode) &&
902        astDeclaredDependenciesNode.expression.type === 'ArrayExpression';
903
904      if (!isArrayExpression && !isTSAsArrayExpression) {
905        // If the declared dependencies are not an array expression then we
906        // can't verify that the user provided the correct dependencies. Tell
907        // the user this in an error.
908        reportProblem({
909          node: declaredDependenciesNode,
910          message:
911            `React Hook ${sourceCode.getText(reactiveHook)} was passed a ` +
912            'dependency list that is not an array literal. This means we ' +
913            "can't statically verify whether you've passed the correct " +
914            'dependencies.',
915        });
916      } else {
917        const arrayExpression = isTSAsArrayExpression
918          ? astDeclaredDependenciesNode.expression
919          : astDeclaredDependenciesNode;
920
921        (arrayExpression as ArrayExpression).elements.forEach((declaredDependencyNode) => {
922          // Skip elided elements.
923          if (declaredDependencyNode === null) {
924            return;
925          }
926
927          // If we see a spread element then add a special warning.
928          if (declaredDependencyNode.type === 'SpreadElement') {
929            reportProblem({
930              node: declaredDependencyNode,
931              message:
932                `React Hook ${sourceCode.getText(reactiveHook)} has a spread ` +
933                "element in its dependency array. This means we can't " +
934                "statically verify whether you've passed the " +
935                'correct dependencies.',
936            });
937
938            return;
939          }
940
941          if (useEffectEventVariables.has(declaredDependencyNode)) {
942            reportProblem({
943              node: declaredDependencyNode,
944              message:
945                'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
946                `Remove \`${sourceCode.getText(declaredDependencyNode)}\` from the list.`,
947              suggest: [
948                {
949                  desc: `Remove the dependency \`${sourceCode.getText(declaredDependencyNode)}\``,
950                  fix(fixer) {
951                    return fixer.removeRange(declaredDependencyNode.range!);
952                  },
953                },
954              ],
955            });
956          }
957
958          // Try to normalize the declared dependency. If we can't then an error
959          // will be thrown. We will catch that error and report an error.
960          let declaredDependency;
961
962          try {
963            declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
964          } catch (error: unknown) {
965            if (error instanceof Error && /Unsupported node type/.test(error.message)) {
966              if (declaredDependencyNode.type === 'Literal') {
967                if (
968                  declaredDependencyNode.value &&
969                  dependencies.has(declaredDependencyNode.value as string)
970                ) {
971                  reportProblem({
972                    node: declaredDependencyNode,
973                    message:
974                      `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
975                      `because it never changes. ` +
976                      `Did you mean to include ${declaredDependencyNode.value} in the array instead?`,
977                  });
978                } else {
979                  reportProblem({
980                    node: declaredDependencyNode,
981                    message:
982                      `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
983                      'because it never changes. You can safely remove it.',
984                  });
985                }
986              } else {
987                reportProblem({
988                  node: declaredDependencyNode,
989                  message:
990                    `React Hook ${sourceCode.getText(reactiveHook)} has a ` +
991                    `complex expression in the dependency array. ` +
992                    'Extract it to a separate variable so it can be statically checked.',
993                });
994              }
995
996              return;
997            } else {
998              throw error;
999            }
1000          }
1001
1002          let maybeID = asAstNode(declaredDependencyNode);
1003
1004          while (
1005            maybeID.type === 'MemberExpression' ||
1006            maybeID.type === 'OptionalMemberExpression' ||
1007            maybeID.type === 'ChainExpression'
1008          ) {
1009            maybeID = maybeID.object ?? asAstNode(maybeID.expression).object ?? maybeID;
1010          }
1011
1012          const isDeclaredInComponent = !componentScope.through.some(
1013            (ref) => ref.identifier === maybeID,
1014          );
1015
1016          // Add the dependency to our declared dependency map.
1017          declaredDependencies.push({
1018            key: declaredDependency,
1019            node: declaredDependencyNode,
1020          });
1021
1022          if (!isDeclaredInComponent) {
1023            externalDependencies.add(declaredDependency);
1024          }
1025        });
1026      }
1027
1028      const {
1029        suggestedDependencies,
1030        unnecessaryDependencies,
1031        missingDependencies,
1032        duplicateDependencies,
1033      } = collectRecommendations({
1034        dependencies,
1035        declaredDependencies,
1036        stableDependencies,
1037        externalDependencies,
1038        isEffect,
1039      });
1040
1041      let suggestedDeps = suggestedDependencies;
1042
1043      const problemCount =
1044        duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size;
1045
1046      if (problemCount === 0) {
1047        // If nothing else to report, check if some dependencies would
1048        // invalidate on every render.
1049        const constructions = scanForConstructions({
1050          declaredDependencies,
1051          declaredDependenciesNode,
1052          componentScope,
1053          scope,
1054        });
1055
1056        constructions.forEach(({ construction, isUsedOutsideOfHook, depType }) => {
1057          const wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo';
1058
1059          const constructionType = depType === 'function' ? 'definition' : 'initialization';
1060
1061          const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`;
1062
1063          const advice = isUsedOutsideOfHook
1064            ? `To fix this, ${defaultAdvice}`
1065            : `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`;
1066
1067          const causation =
1068            depType === 'conditional' || depType === 'logical expression' ? 'could make' : 'makes';
1069
1070          const message =
1071            `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
1072            `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc?.start.line}) ` +
1073            `change on every render. ${advice}`;
1074
1075          let suggest: Rule.ReportDescriptor['suggest'];
1076
1077          // Only handle the simple case of variable assignments.
1078          // Wrapping function declarations can mess up hoisting.
1079          if (
1080            isUsedOutsideOfHook &&
1081            construction.type === 'Variable' &&
1082            // Objects may be mutated after construction, which would make this
1083            // fix unsafe. Functions _probably_ won't be mutated, so we'll
1084            // allow this fix for them.
1085            depType === 'function'
1086          ) {
1087            suggest = [
1088              {
1089                desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`,
1090                fix(fixer) {
1091                  const [before, after] =
1092                    wrapperHook === 'useMemo'
1093                      ? [`useMemo(() => { return `, '; })']
1094                      : ['useCallback(', ')'];
1095
1096                  return [
1097                    // TODO: also add an import?
1098                    fixer.insertTextBefore(construction.node.init!, before),
1099                    // TODO: ideally we'd gather deps here but it would require
1100                    // restructuring the rule code. This will cause a new lint
1101                    // error to appear immediately for useCallback. Note we're
1102                    // not adding [] because would that changes semantics.
1103                    fixer.insertTextAfter(construction.node.init!, after),
1104                  ];
1105                },
1106              },
1107            ];
1108          }
1109
1110          // TODO: What if the function needs to change on every render anyway?
1111          // Should we suggest removing effect deps as an appropriate fix too?
1112          reportProblem({
1113            // TODO: Why not report this at the dependency site?
1114            node: construction.node,
1115            message,
1116            suggest,
1117          });
1118        });
1119
1120        return;
1121      }
1122
1123      // If we're going to report a missing dependency,
1124      // we might as well recalculate the list ignoring
1125      // the currently specified deps. This can result
1126      // in some extra deduplication. We can't do this
1127      // for effects though because those have legit
1128      // use cases for over-specifying deps.
1129      if (!isEffect && missingDependencies.size > 0) {
1130        suggestedDeps = collectRecommendations({
1131          dependencies,
1132          declaredDependencies: [], // Pretend we don't know
1133          stableDependencies,
1134          externalDependencies,
1135          isEffect,
1136        }).suggestedDependencies;
1137      }
1138
1139      // Alphabetize the suggestions, but only if deps were already alphabetized.
1140      function areDeclaredDepsAlphabetized(): boolean {
1141        if (declaredDependencies.length === 0) {
1142          return true;
1143        }
1144
1145        const declaredDepKeys = declaredDependencies.map((dep) => dep.key);
1146        const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
1147
1148        return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
1149      }
1150
1151      if (areDeclaredDepsAlphabetized()) {
1152        suggestedDeps.sort();
1153      }
1154
1155      // Most of our algorithm deals with dependency paths with optional chaining stripped.
1156      // This function is the last step before printing a dependency, so now is a good time to
1157      // check whether any members in our path are always used as optional-only. In that case,
1158      // we will use ?. instead of . to concatenate those parts of the path.
1159      function formatDependency(path: string): string {
1160        const members = path.split('.');
1161        let finalPath = '';
1162
1163        for (let i = 0; i < members.length; i++) {
1164          if (i !== 0) {
1165            const pathSoFar = members.slice(0, i + 1).join('.');
1166            const isOptional = optionalChains.get(pathSoFar) === true;
1167
1168            finalPath += isOptional ? '?.' : '.';
1169          }
1170
1171          finalPath += members[i];
1172        }
1173
1174        return finalPath;
1175      }
1176
1177      function getWarningMessage(
1178        deps: Set<string>,
1179        singlePrefix: string,
1180        label: string,
1181        fixVerb: string,
1182      ): string | null {
1183        if (deps.size === 0) {
1184          return null;
1185        }
1186
1187        return (
1188          (deps.size > 1 ? '' : singlePrefix + ' ') +
1189          label +
1190          ' ' +
1191          (deps.size > 1 ? 'dependencies' : 'dependency') +
1192          ': ' +
1193          joinEnglish(
1194            Array.from(deps)
1195              .sort()
1196              .map((name) => "'" + formatDependency(name) + "'"),
1197          ) +
1198          `. Either ${fixVerb} ${deps.size > 1 ? 'them' : 'it'} or remove the dependency array.`
1199        );
1200      }
1201
1202      function getTemporaryChineseMessage(
1203        missingDeps: Set<string>,
1204        unnecessaryDeps: Set<string>,
1205        nextDeps: string[],
1206      ): string {
1207        if (!options.temporaryChineseMessage) {
1208          return '';
1209        }
1210
1211        const parts: string[] = [];
1212        const formatDeps = (deps: Set<string>): string =>
1213          Array.from(deps).sort().map(formatDependency).join('、');
1214
1215        if (missingDeps.size > 0) {
1216          parts.push(`缺少依赖:${formatDeps(missingDeps)}`);
1217        }
1218
1219        if (unnecessaryDeps.size > 0) {
1220          parts.push(`多余依赖:${formatDeps(unnecessaryDeps)}`);
1221        }
1222
1223        if (parts.length === 0) {
1224          return '';
1225        }
1226
1227        return ` ⚠️ 提示:${parts.join(';')}。🀄️ 建议改为 [${nextDeps
1228          .map(formatDependency)
1229          .join(', ')}]。`;
1230      }
1231
1232      let extraWarning = '';
1233
1234      if (unnecessaryDependencies.size > 0) {
1235        let badRef: string | null = null;
1236
1237        Array.from(unnecessaryDependencies.keys()).forEach((key) => {
1238          if (badRef !== null) {
1239            return;
1240          }
1241
1242          if (key.endsWith('.current')) {
1243            badRef = key;
1244          }
1245        });
1246
1247        if (badRef !== null) {
1248          extraWarning =
1249            ` Mutable values like '${badRef}' aren't valid dependencies ` +
1250            "because mutating them doesn't re-render the component.";
1251        } else if (externalDependencies.size > 0) {
1252          const dep = Array.from(externalDependencies)[0]!;
1253
1254          // Don't show this warning for things that likely just got moved *inside* the callback
1255          // because in that case they're clearly not referring to globals.
1256          if (!scope.set.has(dep)) {
1257            extraWarning =
1258              ` Outer scope values like '${dep}' aren't valid dependencies ` +
1259              `because mutating them doesn't re-render the component.`;
1260          }
1261        }
1262      }
1263
1264      // `props.foo()` marks `props` as a dependency because it has
1265      // a `this` value. This warning can be confusing.
1266      // So if we're going to show it, append a clarification.
1267      if (!extraWarning && missingDependencies.has('props')) {
1268        const propDep = dependencies.get('props');
1269
1270        if (propDep == null) {
1271          return;
1272        }
1273
1274        const refs = propDep.references;
1275
1276        if (!Array.isArray(refs)) {
1277          return;
1278        }
1279
1280        let isPropsOnlyUsedInMembers = true;
1281
1282        for (const ref of refs) {
1283          const id = fastFindReferenceWithParent(componentScope.block, ref.identifier);
1284
1285          if (!id) {
1286            isPropsOnlyUsedInMembers = false;
1287            break;
1288          }
1289
1290          const parent = id.parent;
1291
1292          if (parent == null) {
1293            isPropsOnlyUsedInMembers = false;
1294            break;
1295          }
1296
1297          if (parent.type !== 'MemberExpression' && parent.type !== 'OptionalMemberExpression') {
1298            isPropsOnlyUsedInMembers = false;
1299            break;
1300          }
1301        }
1302
1303        if (isPropsOnlyUsedInMembers) {
1304          extraWarning =
1305            ` However, 'props' will change when *any* prop changes, so the ` +
1306            `preferred fix is to destructure the 'props' object outside of ` +
1307            `the ${reactiveHookName} call and refer to those specific props ` +
1308            `inside ${sourceCode.getText(reactiveHook)}.`;
1309        }
1310      }
1311
1312      if (!extraWarning && missingDependencies.size > 0) {
1313        // See if the user is trying to avoid specifying a callable prop.
1314        // This usually means they're unaware of useCallback.
1315        let missingCallbackDep: string | null = null;
1316
1317        missingDependencies.forEach((missingDep) => {
1318          if (missingCallbackDep) {
1319            return;
1320          }
1321
1322          // Is this a variable from top scope?
1323          const topScopeRef = componentScope.set.get(missingDep);
1324          const usedDep = dependencies.get(missingDep);
1325
1326          if (!usedDep?.references || usedDep?.references[0]?.resolved !== topScopeRef) {
1327            return;
1328          }
1329
1330          // Is this a destructured prop?
1331          const def = topScopeRef?.defs[0];
1332
1333          if (def == null || def.name == null || def.type !== 'Parameter') {
1334            return;
1335          }
1336
1337          // Was it called in at least one case? Then it's a function.
1338          let isFunctionCall = false;
1339          let id: Identifier | undefined;
1340
1341          for (const reference of usedDep.references) {
1342            id = reference.identifier;
1343            const idNode = asAstNode(id);
1344
1345            if (
1346              id != null &&
1347              idNode.parent != null &&
1348              (idNode.parent.type === 'CallExpression' ||
1349                idNode.parent.type === 'OptionalCallExpression') &&
1350              idNode.parent.callee === idNode
1351            ) {
1352              isFunctionCall = true;
1353              break;
1354            }
1355          }
1356
1357          if (!isFunctionCall) {
1358            return;
1359          }
1360
1361          // If it's missing (i.e. in component scope) *and* it's a parameter
1362          // then it is definitely coming from props destructuring.
1363          // (It could also be props itself but we wouldn't be calling it then.)
1364          missingCallbackDep = missingDep;
1365        });
1366
1367        if (missingCallbackDep !== null) {
1368          extraWarning =
1369            ` If '${missingCallbackDep}' changes too often, ` +
1370            `find the parent component that defines it ` +
1371            `and wrap that definition in useCallback.`;
1372        }
1373      }
1374
1375      if (!extraWarning && missingDependencies.size > 0) {
1376        let setStateRecommendation: {
1377          missingDep: string;
1378          setter: string;
1379          form: 'reducer' | 'updater' | 'inlineReducer';
1380        } | null = null;
1381
1382        for (const missingDep of missingDependencies) {
1383          if (setStateRecommendation !== null) {
1384            break;
1385          }
1386
1387          const usedDep = dependencies.get(missingDep)!;
1388          const references = usedDep.references;
1389          let id;
1390          let maybeCall: AstNode | null | undefined;
1391
1392          for (const reference of references) {
1393            id = reference.identifier;
1394            maybeCall = asAstNode(id).parent;
1395
1396            // Try to see if we have setState(someExpr(missingDep)).
1397            while (maybeCall != null && maybeCall !== componentScope.block) {
1398              if (maybeCall.type === 'CallExpression') {
1399                const maybeCallCallee = maybeCall.callee;
1400
1401                if (maybeCallCallee == null) {
1402                  maybeCall = maybeCall.parent;
1403                  continue;
1404                }
1405
1406                const setterName =
1407                  'name' in maybeCallCallee && typeof maybeCallCallee.name === 'string'
1408                    ? maybeCallCallee.name
1409                    : '';
1410                const correspondingStateVariable = setStateCallSites.get(
1411                  maybeCallCallee as Expression | Super,
1412                );
1413
1414                if (correspondingStateVariable != null) {
1415                  if (
1416                    'name' in correspondingStateVariable &&
1417                    correspondingStateVariable.name === missingDep
1418                  ) {
1419                    // setCount(count + 1)
1420                    setStateRecommendation = {
1421                      missingDep,
1422                      setter: setterName,
1423                      form: 'updater',
1424                    };
1425                  } else if (stateVariables.has(id)) {
1426                    // setCount(count + increment)
1427                    setStateRecommendation = {
1428                      missingDep,
1429                      setter: setterName,
1430                      form: 'reducer',
1431                    };
1432                  } else {
1433                    const resolved = reference.resolved;
1434
1435                    if (resolved != null) {
1436                      // If it's a parameter *and* a missing dep,
1437                      // it must be a prop or something inside a prop.
1438                      // Therefore, recommend an inline reducer.
1439                      const def = resolved.defs[0];
1440
1441                      if (def != null && def.type === 'Parameter') {
1442                        setStateRecommendation = {
1443                          missingDep,
1444                          setter: setterName,
1445                          form: 'inlineReducer',
1446                        };
1447                      }
1448                    }
1449                  }
1450
1451                  break;
1452                }
1453              }
1454
1455              maybeCall = maybeCall.parent;
1456            }
1457
1458            if (setStateRecommendation !== null) {
1459              break;
1460            }
1461          }
1462        }
1463
1464        if (setStateRecommendation !== null) {
1465          switch (setStateRecommendation.form) {
1466            case 'reducer':
1467              extraWarning =
1468                ` You can also replace multiple useState variables with useReducer ` +
1469                `if '${setStateRecommendation.setter}' needs the ` +
1470                `current value of '${setStateRecommendation.missingDep}'.`;
1471              break;
1472
1473            case 'inlineReducer':
1474              extraWarning =
1475                ` If '${setStateRecommendation.setter}' needs the ` +
1476                `current value of '${setStateRecommendation.missingDep}', ` +
1477                `you can also switch to useReducer instead of useState and ` +
1478                `read '${setStateRecommendation.missingDep}' in the reducer.`;
1479              break;
1480
1481            case 'updater':
1482              extraWarning = ` You can also do a functional update '${
1483                setStateRecommendation.setter
1484              }(${setStateRecommendation.missingDep.slice(0, 1)} => ...)' if you only need '${
1485                setStateRecommendation.missingDep
1486              }' in the '${setStateRecommendation.setter}' call.`;
1487              break;
1488
1489            default:
1490              throw new Error('Unknown case.');
1491          }
1492        }
1493      }
1494
1495      reportProblem({
1496        node: declaredDependenciesNode,
1497        message:
1498          `React Hook ${sourceCode.getText(reactiveHook)} has ` +
1499          // To avoid a long message, show the next actionable item.
1500          (getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
1501            getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') ||
1502            getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) +
1503          extraWarning +
1504          getTemporaryChineseMessage(missingDependencies, unnecessaryDependencies, suggestedDeps),
1505        suggest: [
1506          {
1507            desc: `Update the dependencies array to be: [${suggestedDeps
1508              .map(formatDependency)
1509              .join(', ')}]`,
1510            fix(fixer) {
1511              // TODO: consider preserving the comments or formatting?
1512              return fixer.replaceText(
1513                declaredDependenciesNode,
1514                `[${suggestedDeps.map(formatDependency).join(', ')}]`,
1515              );
1516            },
1517          },
1518        ],
1519      });
1520    }
1521
1522    function visitCallExpression(node: CallExpression): void {
1523      const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
1524
1525      if (callbackIndex == null) {
1526        // Not a React Hook call that needs deps.
1527        return;
1528      }
1529
1530      const { callbackIndex: callbackArgumentIndex } = callbackIndex;
1531      let callback = node.arguments[callbackArgumentIndex];
1532      const reactiveHook = node.callee;
1533      const nodeWithoutNamespace = getNodeWithoutReactNamespace(reactiveHook);
1534      const reactiveHookName = 'name' in nodeWithoutNamespace ? nodeWithoutNamespace.name : '';
1535      const declaredDependenciesNode = getDeclaredDependenciesNode(node, callbackIndex);
1536      const isEffect =
1537        callbackIndex.treatAsEffect === true || /Effect($|[^a-z])/g.test(reactiveHookName);
1538
1539      // Check whether a callback is supplied. If there is no callback supplied
1540      // then the hook will not work and React will throw a TypeError.
1541      // So no need to check for dependency inclusion.
1542      if (!callback) {
1543        reportProblem({
1544          node: reactiveHook,
1545          message:
1546            `React Hook ${reactiveHookName} requires an effect callback. ` +
1547            `Did you forget to pass a callback to the hook?`,
1548        });
1549
1550        return;
1551      }
1552
1553      if (declaredDependenciesNode == null && callbackIndex.dependencyRequired === true) {
1554        reportProblem({
1555          node: reactiveHook,
1556          message:
1557            `React Hook ${reactiveHookName} requires a dependency array at ` +
1558            `${formatDependencyPath(callbackIndex.dependencyPath ?? [callbackArgumentIndex + 1])}.`,
1559        });
1560
1561        return;
1562      }
1563
1564      if (
1565        callbackIndex.dependencyPath == null &&
1566        !node.arguments[callbackArgumentIndex + 1] &&
1567        isEffect &&
1568        options.requireExplicitEffectDeps
1569      ) {
1570        reportProblem({
1571          node: reactiveHook,
1572          message:
1573            `React Hook ${reactiveHookName} always requires dependencies. ` +
1574            `Please add a dependency array or an explicit \`undefined\``,
1575        });
1576      }
1577
1578      const isAutoDepsHook = options.experimental_autoDependenciesHooks.includes(reactiveHookName);
1579
1580      // Check the declared dependencies for this reactive hook. If there is no
1581      // second argument then the reactive callback will re-run on every render.
1582      // So no need to check for dependency inclusion.
1583      if (
1584        (!declaredDependenciesNode ||
1585          (isAutoDepsHook &&
1586            declaredDependenciesNode.type === 'Literal' &&
1587            declaredDependenciesNode.value === null)) &&
1588        !isEffect
1589      ) {
1590        // These are only used for optimization.
1591        if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') {
1592          // TODO: Can this have a suggestion?
1593          reportProblem({
1594            node: reactiveHook,
1595            message:
1596              `React Hook ${reactiveHookName} does nothing when called with ` +
1597              `only one argument. Did you forget to pass an array of ` +
1598              `dependencies?`,
1599          });
1600        }
1601
1602        return;
1603      }
1604
1605      let callbackNode = asAstNode(callback);
1606
1607      while (isTypeCastExpression(callbackNode)) {
1608        callbackNode = callbackNode.expression;
1609      }
1610
1611      const callbackName = callbackNode.name;
1612
1613      switch (callbackNode.type) {
1614        case 'FunctionExpression':
1615
1616        case 'ArrowFunctionExpression':
1617          visitFunctionWithDependencies(
1618            callbackNode as unknown as ArrowFunctionExpression | FunctionExpression,
1619            declaredDependenciesNode,
1620            reactiveHook,
1621            reactiveHookName,
1622            isEffect,
1623            isAutoDepsHook,
1624          );
1625
1626          return; // Handled
1627
1628        case 'Identifier':
1629          if (
1630            !declaredDependenciesNode ||
1631            (isAutoDepsHook &&
1632              declaredDependenciesNode.type === 'Literal' &&
1633              declaredDependenciesNode.value === null)
1634          ) {
1635            // Always runs, no problems.
1636            return; // Handled
1637          }
1638
1639          // The function passed as a callback is not written inline.
1640          // But perhaps it's in the dependencies array?
1641          if (
1642            'elements' in declaredDependenciesNode &&
1643            declaredDependenciesNode.elements &&
1644            declaredDependenciesNode.elements.some(
1645              (el) => el && el.type === 'Identifier' && el.name === callbackName,
1646            )
1647          ) {
1648            // If it's already in the list of deps, we don't care because
1649            // this is valid regardless.
1650            return; // Handled
1651          }
1652
1653          // We'll do our best effort to find it, complain otherwise.
1654          if (callbackName == null) {
1655            break;
1656          }
1657
1658          const variable = getScope(callback).set.get(callbackName);
1659
1660          if (variable == null || variable.defs == null) {
1661            // If it's not in scope, we don't care.
1662            return; // Handled
1663          }
1664
1665          // The function passed as a callback is not written inline.
1666          // But it's defined somewhere in the render scope.
1667          // We'll do our best effort to find and check it, complain otherwise.
1668          const def = variable.defs[0];
1669
1670          if (!def || !def.node) {
1671            break; // Unhandled
1672          }
1673
1674          if (def.type === 'Parameter') {
1675            reportProblem({
1676              node: reactiveHook,
1677              message: getUnknownDependenciesMessage(reactiveHookName),
1678            });
1679
1680            return;
1681          }
1682
1683          if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1684            // Parameter or an unusual pattern. Bail out.
1685            break; // Unhandled
1686          }
1687
1688          switch (def.node.type) {
1689            case 'FunctionDeclaration':
1690              // useEffect(() => { ... }, []);
1691              visitFunctionWithDependencies(
1692                def.node,
1693                declaredDependenciesNode,
1694                reactiveHook,
1695                reactiveHookName,
1696                isEffect,
1697                isAutoDepsHook,
1698              );
1699
1700              return; // Handled
1701
1702            case 'VariableDeclarator':
1703              const init = def.node.init;
1704
1705              if (!init) {
1706                break; // Unhandled
1707              }
1708
1709              switch (init.type) {
1710                // const effectBody = () => {...};
1711                // useEffect(effectBody, []);
1712                case 'ArrowFunctionExpression':
1713
1714                case 'FunctionExpression':
1715                  // We can inspect this function as if it were inline.
1716                  visitFunctionWithDependencies(
1717                    init,
1718                    declaredDependenciesNode,
1719                    reactiveHook,
1720                    reactiveHookName,
1721                    isEffect,
1722                    isAutoDepsHook,
1723                  );
1724
1725                  return; // Handled
1726              }
1727
1728              break; // Unhandled
1729          }
1730
1731          break; // Unhandled
1732
1733        default:
1734          // useEffect(generateEffectBody(), []);
1735          reportProblem({
1736            node: reactiveHook,
1737            message: getUnknownDependenciesMessage(reactiveHookName),
1738          });
1739
1740          return; // Handled
1741      }
1742
1743      // Something unusual. Fall back to suggesting to add the body itself as a dep.
1744      reportProblem({
1745        node: reactiveHook,
1746        message:
1747          `React Hook ${reactiveHookName} has a missing dependency: '${callbackName}'. ` +
1748          `Either include it or remove the dependency array.`,
1749        suggest: [
1750          {
1751            desc: `Update the dependencies array to be: [${callbackName}]`,
1752            fix(fixer) {
1753              return fixer.replaceText(declaredDependenciesNode, `[${callbackName}]`);
1754            },
1755          },
1756        ],
1757      });
1758    }
1759
1760    return {
1761      CallExpression: visitCallExpression,
1762    };
1763  },
1764} satisfies Rule.RuleModule;
1765
1766// The meat of the logic.
1767function collectRecommendations({
1768  dependencies,
1769  declaredDependencies,
1770  stableDependencies,
1771  externalDependencies,
1772  isEffect,
1773}: {
1774  dependencies: Map<string, Dependency>;
1775  declaredDependencies: Array<DeclaredDependency>;
1776  stableDependencies: Set<string>;
1777  externalDependencies: Set<string>;
1778  isEffect: boolean;
1779}) {
1780  // Our primary data structure.
1781  // It is a logical representation of property chains:
1782  // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
1783  //         -> `props.lol`
1784  //         -> `props.huh` -> `props.huh.okay`
1785  //         -> `props.wow`
1786  // We'll use it to mark nodes that are *used* by the programmer,
1787  // and the nodes that were *declared* as deps. Then we will
1788  // traverse it to learn which deps are missing or unnecessary.
1789  const depTree = createDepTree();
1790
1791  function createDepTree(): DependencyTreeNode {
1792    return {
1793      isUsed: false, // True if used in code
1794      isSatisfiedRecursively: false, // True if specified in deps
1795      isSubtreeUsed: false, // True if something deeper is used by code
1796      children: new Map(), // Nodes for properties
1797    };
1798  }
1799
1800  // Mark all required nodes first.
1801  // Imagine exclamation marks next to each used deep property.
1802  dependencies.forEach((_, key) => {
1803    const node = getOrCreateNodeByPath(depTree, key);
1804
1805    node.isUsed = true;
1806    markAllParentsByPath(depTree, key, (parent) => {
1807      parent.isSubtreeUsed = true;
1808    });
1809  });
1810
1811  // Mark all satisfied nodes.
1812  // Imagine checkmarks next to each declared dependency.
1813  declaredDependencies.forEach(({ key }) => {
1814    const node = getOrCreateNodeByPath(depTree, key);
1815
1816    node.isSatisfiedRecursively = true;
1817  });
1818  stableDependencies.forEach((key) => {
1819    const node = getOrCreateNodeByPath(depTree, key);
1820
1821    node.isSatisfiedRecursively = true;
1822  });
1823
1824  // Tree manipulation helpers.
1825  function getOrCreateNodeByPath(rootNode: DependencyTreeNode, path: string): DependencyTreeNode {
1826    const keys = path.split('.');
1827    let node = rootNode;
1828
1829    for (const key of keys) {
1830      let child = node.children.get(key);
1831
1832      if (!child) {
1833        child = createDepTree();
1834        node.children.set(key, child);
1835      }
1836
1837      node = child;
1838    }
1839
1840    return node;
1841  }
1842
1843  function markAllParentsByPath(
1844    rootNode: DependencyTreeNode,
1845    path: string,
1846    fn: (node: DependencyTreeNode) => void,
1847  ): void {
1848    const keys = path.split('.');
1849    let node = rootNode;
1850
1851    for (const key of keys) {
1852      const child = node.children.get(key);
1853
1854      if (!child) {
1855        return;
1856      }
1857
1858      fn(child);
1859      node = child;
1860    }
1861  }
1862
1863  // Now we can learn which dependencies are missing or necessary.
1864  const missingDependencies = new Set<string>();
1865  const satisfyingDependencies = new Set<string>();
1866
1867  scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, (key) => key);
1868
1869  function scanTreeRecursively(
1870    node: DependencyTreeNode,
1871    missingPaths: Set<string>,
1872    satisfyingPaths: Set<string>,
1873    keyToPath: (key: string) => string,
1874  ): void {
1875    node.children.forEach((child, key) => {
1876      const path = keyToPath(key);
1877
1878      if (child.isSatisfiedRecursively) {
1879        if (child.isSubtreeUsed) {
1880          // Remember this dep actually satisfied something.
1881          satisfyingPaths.add(path);
1882        }
1883
1884        // It doesn't matter if there's something deeper.
1885        // It would be transitively satisfied since we assume immutability.
1886        // `props.foo` is enough if you read `props.foo.id`.
1887
1888        return;
1889      }
1890
1891      if (child.isUsed) {
1892        // Remember that no declared deps satisfied this node.
1893        missingPaths.add(path);
1894        // If we got here, nothing in its subtree was satisfied.
1895        // No need to search further.
1896
1897        return;
1898      }
1899
1900      scanTreeRecursively(
1901        child,
1902        missingPaths,
1903        satisfyingPaths,
1904        (childKey) => path + '.' + childKey,
1905      );
1906    });
1907  }
1908
1909  // Collect suggestions in the order they were originally specified.
1910  const suggestedDependencies: Array<string> = [];
1911  const unnecessaryDependencies = new Set<string>();
1912  const duplicateDependencies = new Set<string>();
1913
1914  declaredDependencies.forEach(({ key }) => {
1915    // Does this declared dep satisfy a real need?
1916    if (satisfyingDependencies.has(key)) {
1917      if (suggestedDependencies.indexOf(key) === -1) {
1918        // Good one.
1919        suggestedDependencies.push(key);
1920      } else {
1921        // Duplicate.
1922        duplicateDependencies.add(key);
1923      }
1924    } else {
1925      if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
1926        // Effects are allowed extra "unnecessary" deps.
1927        // Such as resetting scroll when ID changes.
1928        // Consider them legit.
1929        // The exception is ref.current which is always wrong.
1930        if (suggestedDependencies.indexOf(key) === -1) {
1931          suggestedDependencies.push(key);
1932        }
1933      } else {
1934        // It's definitely not needed.
1935        unnecessaryDependencies.add(key);
1936      }
1937    }
1938  });
1939
1940  // Then add the missing ones at the end.
1941  missingDependencies.forEach((key) => {
1942    suggestedDependencies.push(key);
1943  });
1944
1945  return {
1946    suggestedDependencies,
1947    unnecessaryDependencies,
1948    duplicateDependencies,
1949    missingDependencies,
1950  };
1951}
1952
1953// If the node will result in constructing a referentially unique value, return
1954// its human readable type name, else return null.
1955function getConstructionExpressionType(node: Node): string | null {
1956  const astNode = asAstNode(node);
1957
1958  switch (astNode.type) {
1959    case 'ObjectExpression':
1960      return 'object';
1961
1962    case 'ArrayExpression':
1963      return 'array';
1964
1965    case 'ArrowFunctionExpression':
1966
1967    case 'FunctionExpression':
1968      return 'function';
1969
1970    case 'ClassExpression':
1971      return 'class';
1972
1973    case 'ConditionalExpression':
1974      if (
1975        getConstructionExpressionType(astNode.consequent as Node) != null ||
1976        getConstructionExpressionType(astNode.alternate as Node) != null
1977      ) {
1978        return 'conditional';
1979      }
1980
1981      return null;
1982
1983    case 'LogicalExpression':
1984      if (
1985        getConstructionExpressionType(astNode.left as Node) != null ||
1986        getConstructionExpressionType(astNode.right as Node) != null
1987      ) {
1988        return 'logical expression';
1989      }
1990
1991      return null;
1992
1993    case 'JSXFragment':
1994      return 'JSX fragment';
1995
1996    case 'JSXElement':
1997      return 'JSX element';
1998
1999    case 'AssignmentExpression':
2000      if (getConstructionExpressionType(astNode.right as Node) != null) {
2001        return 'assignment expression';
2002      }
2003
2004      return null;
2005
2006    case 'NewExpression':
2007      return 'object construction';
2008
2009    case 'Literal':
2010      if (astNode.value instanceof RegExp) {
2011        return 'regular expression';
2012      }
2013
2014      return null;
2015
2016    case 'TypeCastExpression':
2017
2018    case 'AsExpression':
2019
2020    case 'TSAsExpression':
2021      return getConstructionExpressionType(astNode.expression as Node);
2022  }
2023
2024  return null;
2025}
2026
2027// Finds variables declared as dependencies
2028// that would invalidate on every render.
2029function scanForConstructions({
2030  declaredDependencies,
2031  declaredDependenciesNode,
2032  componentScope,
2033  scope,
2034}: {
2035  declaredDependencies: Array<DeclaredDependency>;
2036  declaredDependenciesNode: Node;
2037  componentScope: Scope.Scope;
2038  scope: Scope.Scope;
2039}) {
2040  const constructions = declaredDependencies
2041    .map(({ key }) => {
2042      const ref = componentScope.variables.find((v) => v.name === key);
2043
2044      if (ref == null) {
2045        return null;
2046      }
2047
2048      const node = ref.defs[0];
2049
2050      if (node == null) {
2051        return null;
2052      }
2053
2054      // const handleChange = function () {}
2055      // const handleChange = () => {}
2056      // const foo = {}
2057      // const foo = []
2058      // etc.
2059      if (
2060        node.type === 'Variable' &&
2061        node.node.type === 'VariableDeclarator' &&
2062        node.node.id.type === 'Identifier' && // Ensure this is not destructed assignment
2063        node.node.init != null
2064      ) {
2065        const constantExpressionType = getConstructionExpressionType(node.node.init);
2066
2067        if (constantExpressionType) {
2068          return [ref, constantExpressionType];
2069        }
2070      }
2071
2072      // function handleChange() {}
2073      if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
2074        return [ref, 'function'];
2075      }
2076
2077      // class Foo {}
2078      if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
2079        return [ref, 'class'];
2080      }
2081
2082      return null;
2083    })
2084    .filter(Boolean) as Array<[Scope.Variable, string]>;
2085
2086  function isUsedOutsideOfHook(ref: Scope.Variable): boolean {
2087    let foundWriteExpr = false;
2088
2089    for (const reference of ref.references) {
2090      if (reference.writeExpr) {
2091        if (foundWriteExpr) {
2092          // Two writes to the same function.
2093          return true;
2094        } else {
2095          // Ignore first write as it's not usage.
2096          foundWriteExpr = true;
2097          continue;
2098        }
2099      }
2100
2101      let currentScope: Scope.Scope | null = reference.from;
2102
2103      while (currentScope !== scope && currentScope != null) {
2104        currentScope = currentScope.upper;
2105      }
2106
2107      if (currentScope !== scope) {
2108        // This reference is outside the Hook callback.
2109        // It can only be legit if it's the deps array.
2110        if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
2111          return true;
2112        }
2113      }
2114    }
2115
2116    return false;
2117  }
2118
2119  return constructions.map(([ref, depType]) => ({
2120    construction: ref.defs[0] as Scope.Definition,
2121    depType,
2122    isUsedOutsideOfHook: isUsedOutsideOfHook(ref),
2123  }));
2124}
2125
2126/**
2127 * Assuming () means the passed/returned node:
2128 * (props) => (props)
2129 * props.(foo) => (props.foo)
2130 * props.foo.(bar) => (props).foo.bar
2131 * props.foo.bar.(baz) => (props).foo.bar.baz
2132 */
2133function getDependency(node: AstNode | Node): AstNode {
2134  const astNode = asAstNode(node);
2135
2136  if (
2137    astNode.parent &&
2138    (astNode.parent.type === 'MemberExpression' ||
2139      astNode.parent.type === 'OptionalMemberExpression') &&
2140    astNode.parent.object === astNode &&
2141    astNode.parent.property?.type === 'Identifier' &&
2142    astNode.parent.property.name !== 'current' &&
2143    !astNode.parent.computed &&
2144    !(
2145      astNode.parent.parent != null &&
2146      (astNode.parent.parent.type === 'CallExpression' ||
2147        astNode.parent.parent.type === 'OptionalCallExpression') &&
2148      astNode.parent.parent.callee === astNode.parent
2149    )
2150  ) {
2151    return getDependency(astNode.parent as Node);
2152  } else if (
2153    // Note: we don't check OptionalMemberExpression because it can't be LHS.
2154    astNode.type === 'MemberExpression' &&
2155    astNode.parent &&
2156    astNode.parent.type === 'AssignmentExpression' &&
2157    astNode.parent.left === astNode
2158  ) {
2159    return asAstNode(astNode.object);
2160  } else {
2161    return astNode;
2162  }
2163}
2164
2165/**
2166 * Mark a node as either optional or required.
2167 * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional.
2168 * It just means there is an optional member somewhere inside.
2169 * This particular node might still represent a required member, so check .optional field.
2170 */
2171function markNode(node: Node, optionalChains: Map<string, boolean> | null, result: string): void {
2172  if (optionalChains) {
2173    if ('optional' in node && node.optional) {
2174      // We only want to consider it optional if *all* usages were optional.
2175      if (!optionalChains.has(result)) {
2176        // Mark as (maybe) optional. If there's a required usage, this will be overridden.
2177        optionalChains.set(result, true);
2178      }
2179    } else {
2180      // Mark as required.
2181      optionalChains.set(result, false);
2182    }
2183  }
2184}
2185
2186/**
2187 * Assuming () means the passed node.
2188 * (foo) -> 'foo'
2189 * foo(.)bar -> 'foo.bar'
2190 * foo.bar(.)baz -> 'foo.bar.baz'
2191 * Otherwise throw.
2192 */
2193function analyzePropertyChain(
2194  node: AstNode | Node,
2195  optionalChains: Map<string, boolean> | null,
2196): string {
2197  const astNode = asAstNode(node);
2198
2199  if (astNode.type === 'Identifier' || astNode.type === 'JSXIdentifier') {
2200    const result = astNode.name as string;
2201
2202    if (optionalChains) {
2203      // Mark as required.
2204      optionalChains.set(result, false);
2205    }
2206
2207    return result;
2208  } else if (astNode.type === 'MemberExpression' && !astNode.computed) {
2209    const object = analyzePropertyChain(astNode.object as Node, optionalChains);
2210    const property = analyzePropertyChain(astNode.property as Node, null);
2211    const result = `${object}.${property}`;
2212
2213    markNode(astNode as Node, optionalChains, result);
2214
2215    return result;
2216  } else if (astNode.type === 'OptionalMemberExpression' && !astNode.computed) {
2217    const object = analyzePropertyChain(astNode.object as Node, optionalChains);
2218    const property = analyzePropertyChain(astNode.property as Node, null);
2219    const result = `${object}.${property}`;
2220
2221    markNode(astNode as Node, optionalChains, result);
2222
2223    return result;
2224  } else if (
2225    astNode.type === 'ChainExpression' &&
2226    (!('computed' in astNode) || !astNode.computed)
2227  ) {
2228    const expression = astNode.expression as AstNode;
2229
2230    if (expression.type === 'CallExpression') {
2231      throw new Error(`Unsupported node type: ${expression.type}`);
2232    }
2233
2234    const object = analyzePropertyChain(expression.object as Node, optionalChains);
2235    const property = analyzePropertyChain(expression.property as Node, null);
2236    const result = `${object}.${property}`;
2237
2238    markNode(expression as Node, optionalChains, result);
2239
2240    return result;
2241  } else {
2242    throw new Error(`Unsupported node type: ${astNode.type}`);
2243  }
2244}
2245
2246function getNodeWithoutReactNamespace(node: Expression | Super): Expression | Identifier | Super {
2247  if (
2248    node.type === 'MemberExpression' &&
2249    node.object.type === 'Identifier' &&
2250    node.object.name === 'React' &&
2251    node.property.type === 'Identifier' &&
2252    !node.computed
2253  ) {
2254    return node.property;
2255  }
2256
2257  return node;
2258}
2259
2260// What's the index of callback that needs to be analyzed for a given Hook?
2261// -1 if it's not a Hook we care about (e.g. useState).
2262// 0 for useEffect/useMemo/useCallback(fn).
2263// 1 for useImperativeHandle(ref, fn).
2264// For additionally configured Hooks, assume that they're like useEffect (0).
2265function getReactiveHookCallbackIndex(
2266  calleeNode: Expression | Super,
2267  options?: {
2268    additionalHooks: NormalizedAdditionalHooks;
2269    enableDangerousAutofixThisMayCauseInfiniteLoops?: boolean;
2270    temporaryChineseMessage?: boolean;
2271  },
2272): ReactiveHookConfiguration | undefined {
2273  const node = getNodeWithoutReactNamespace(calleeNode);
2274
2275  if (node.type !== 'Identifier') {
2276    return undefined;
2277  }
2278
2279  switch (node.name) {
2280    case 'useEffect':
2281
2282    case 'useLayoutEffect':
2283
2284    case 'useCallback':
2285
2286    case 'useMemo':
2287      // useEffect(fn)
2288      return { callbackIndex: 0 };
2289
2290    case 'useImperativeHandle':
2291      // useImperativeHandle(ref, fn)
2292      return { callbackIndex: 1 };
2293
2294    default:
2295      if (node === calleeNode && options) {
2296        const configuredHook = options.additionalHooks.configuredHooks.get(node.name);
2297
2298        if (configuredHook) {
2299          return {
2300            callbackIndex: 0,
2301            dependencyPath: configuredHook.dependencyPath,
2302            dependencyRequired: configuredHook.dependencyRequired,
2303            treatAsEffect: configuredHook.treatAsEffect,
2304          };
2305        }
2306      }
2307
2308      if (node === calleeNode && options && options.additionalHooks.regex) {
2309        // Allow the user to provide a regular expression which enables the lint to
2310        // target custom reactive hooks.
2311        let name;
2312
2313        try {
2314          name = analyzePropertyChain(node, null);
2315        } catch (error: unknown) {
2316          if (error instanceof Error && /Unsupported node type/.test(error.message)) {
2317            return { callbackIndex: 0 };
2318          } else {
2319            throw error;
2320          }
2321        }
2322
2323        return options.additionalHooks.regex.test(name) ? { callbackIndex: 0 } : undefined;
2324      } else {
2325        return undefined;
2326      }
2327  }
2328}
2329
2330function normalizeAdditionalHooksOption(
2331  additionalHooksOption: unknown,
2332  settings: Rule.RuleContext['settings'],
2333): NormalizedAdditionalHooks {
2334  const configuredHooks = new Map<string, NormalizedAdditionalHook>();
2335
2336  if (typeof additionalHooksOption === 'string') {
2337    return {
2338      configuredHooks,
2339      regex: new RegExp(additionalHooksOption),
2340    };
2341  }
2342
2343  if (Array.isArray(additionalHooksOption)) {
2344    for (const entry of additionalHooksOption) {
2345      if (typeof entry === 'string') {
2346        configuredHooks.set(entry, {
2347          dependencyRequired: false,
2348          hook: entry,
2349          treatAsEffect: false,
2350        });
2351        continue;
2352      }
2353
2354      if (isAdditionalHookOption(entry)) {
2355        configuredHooks.set(entry.hook, {
2356          dependencyPath: entry.dependencyPath,
2357          dependencyRequired: entry.dependencyRequired ?? true,
2358          hook: entry.hook,
2359          treatAsEffect: false,
2360        });
2361      }
2362    }
2363
2364    return {
2365      configuredHooks,
2366      regex: undefined,
2367    };
2368  }
2369
2370  return {
2371    configuredHooks,
2372    regex: getAdditionalEffectHooksFromSettings(settings),
2373  };
2374}
2375
2376function isAdditionalHookOption(value: unknown): value is AdditionalHookOption {
2377  if (typeof value !== 'object' || value == null || Array.isArray(value)) {
2378    return false;
2379  }
2380
2381  const candidate = value as Record<string, unknown>;
2382
2383  return (
2384    typeof candidate.hook === 'string' &&
2385    Array.isArray(candidate.dependencyPath) &&
2386    candidate.dependencyPath.every(
2387      (segment) => typeof segment === 'string' || Number.isInteger(segment),
2388    ) &&
2389    (candidate.dependencyRequired == null || typeof candidate.dependencyRequired === 'boolean')
2390  );
2391}
2392
2393function getDeclaredDependenciesNode(
2394  node: CallExpression,
2395  configuration: ReactiveHookConfiguration,
2396): Node | undefined {
2397  const dependencyNode = configuration.dependencyPath
2398    ? resolveDependencyPath(
2399        node.arguments.map((argument) => (argument ? asAstNode(argument) : null)),
2400        configuration.dependencyPath,
2401      )
2402    : node.arguments[configuration.callbackIndex + 1];
2403
2404  if (
2405    dependencyNode != null &&
2406    !(dependencyNode.type === 'Identifier' && dependencyNode.name === 'undefined')
2407  ) {
2408    return dependencyNode as Node;
2409  }
2410
2411  return undefined;
2412}
2413
2414function resolveDependencyPath(
2415  root: Array<AstNode | null>,
2416  path: ReadonlyArray<AdditionalHookDependencyPathSegment>,
2417): AstNode | undefined {
2418  let current: AstNode | Array<AstNode | null> | null | undefined = root;
2419
2420  for (const segment of path) {
2421    if (typeof segment === 'number') {
2422      if (Array.isArray(current)) {
2423        current = current[segment] ?? undefined;
2424        continue;
2425      }
2426
2427      if (current?.type === 'ArrayExpression') {
2428        current = current.elements?.[segment] ?? undefined;
2429        continue;
2430      }
2431
2432      return undefined;
2433    }
2434
2435    if (Array.isArray(current) || current == null) {
2436      return undefined;
2437    }
2438
2439    if (current.type === 'ObjectExpression') {
2440      current = getObjectPropertyValue(current, segment);
2441      continue;
2442    }
2443
2444    const nextValue: unknown = (current as Record<string, unknown>)[segment];
2445
2446    if (isNodeLike(nextValue)) {
2447      current = nextValue;
2448      continue;
2449    }
2450
2451    if (Array.isArray(nextValue) && nextValue.every((item) => item == null || isNodeLike(item))) {
2452      current = nextValue as Array<AstNode | null>;
2453      continue;
2454    }
2455
2456    return undefined;
2457  }
2458
2459  return Array.isArray(current) ? undefined : (current ?? undefined);
2460}
2461
2462function getObjectPropertyValue(
2463  objectExpression: AstNode,
2464  propertyName: string,
2465): AstNode | undefined {
2466  for (const property of objectExpression.properties ?? []) {
2467    if (property == null || property.type !== 'Property') {
2468      continue;
2469    }
2470
2471    const key = property.key;
2472
2473    if (key == null) {
2474      continue;
2475    }
2476
2477    if (key.type === 'Identifier' && key.name === propertyName) {
2478      return isNodeLike(property.value) ? property.value : undefined;
2479    }
2480
2481    if (key.type === 'Literal' && key.value === propertyName) {
2482      return isNodeLike(property.value) ? property.value : undefined;
2483    }
2484  }
2485
2486  return undefined;
2487}
2488
2489function formatDependencyPath(path: ReadonlyArray<AdditionalHookDependencyPathSegment>): string {
2490  return path.reduce<string>((result, segment) => {
2491    if (typeof segment === 'number') {
2492      return `${result}[${segment}]`;
2493    }
2494
2495    return `${result}.${segment}`;
2496  }, 'arguments');
2497}
2498
2499/**
2500 * ESLint won't assign node.parent to references from context.getScope()
2501 *
2502 * So instead we search for the node from an ancestor assigning node.parent
2503 * as we go. This mutates the AST.
2504 *
2505 * This traversal is:
2506 * - optimized by only searching nodes with a range surrounding our target node
2507 * - agnostic to AST node types, it looks for `{ type: string, ... }`
2508 */
2509function fastFindReferenceWithParent(start: Node, target: Node): AstNode | null {
2510  const queue = [asAstNode(start)];
2511  let item: AstNode;
2512
2513  while (queue.length) {
2514    item = queue.shift() as AstNode;
2515
2516    if (isSameIdentifier(item, target)) {
2517      return item;
2518    }
2519
2520    if (!isAncestorNodeOf(item, target)) {
2521      continue;
2522    }
2523
2524    for (const [key, value] of Object.entries(item)) {
2525      if (key === 'parent') {
2526        continue;
2527      }
2528
2529      if (isNodeLike(value)) {
2530        value.parent = item;
2531        queue.push(value);
2532      } else if (Array.isArray(value)) {
2533        value.forEach((val) => {
2534          if (isNodeLike(val)) {
2535            val.parent = item;
2536            queue.push(val);
2537          }
2538        });
2539      }
2540    }
2541  }
2542
2543  return null;
2544}
2545
2546function joinEnglish(arr: Array<string>): string {
2547  let s = '';
2548
2549  for (let i = 0; i < arr.length; i++) {
2550    s += arr[i];
2551
2552    if (i === 0 && arr.length === 2) {
2553      s += ' and ';
2554    } else if (i === arr.length - 2 && arr.length > 2) {
2555      s += ', and ';
2556    } else if (i < arr.length - 1) {
2557      s += ', ';
2558    }
2559  }
2560
2561  return s;
2562}
2563
2564function isNodeLike(val: unknown): val is AstNode {
2565  return (
2566    typeof val === 'object' &&
2567    val !== null &&
2568    !Array.isArray(val) &&
2569    'type' in val &&
2570    typeof val.type === 'string'
2571  );
2572}
2573
2574function isSameIdentifier(a: AstNode | Node, b: AstNode | Node): boolean {
2575  const left = asAstNode(a);
2576  const right = asAstNode(b);
2577
2578  return (
2579    (left.type === 'Identifier' || left.type === 'JSXIdentifier') &&
2580    left.type === right.type &&
2581    left.name === right.name &&
2582    !!left.range &&
2583    !!right.range &&
2584    left.range[0] === right.range[0] &&
2585    left.range[1] === right.range[1]
2586  );
2587}
2588
2589function isAncestorNodeOf(a: AstNode | Node, b: AstNode | Node): boolean {
2590  const ancestor = asAstNode(a);
2591  const descendant = asAstNode(b);
2592
2593  return (
2594    !!ancestor.range &&
2595    !!descendant.range &&
2596    ancestor.range[0] <= descendant.range[0] &&
2597    ancestor.range[1] >= descendant.range[1]
2598  );
2599}
2600
2601function isUseEffectEventIdentifier(node: Node): boolean {
2602  return node.type === 'Identifier' && node.name === 'useEffectEvent';
2603}
2604
2605function getUnknownDependenciesMessage(reactiveHookName: string): string {
2606  return (
2607    `React Hook ${reactiveHookName} received a function whose dependencies ` +
2608    `are unknown. Pass an inline function instead.`
2609  );
2610}
2611
2612export default rule;

  1. Footnotes can contain code, formatting, and links. ↩︎

Crafted with by Mashiro

© 2017-2026 樱花庄的白猫 沪ICP备17028213号