@eslint-react/core 3.0.0-next.7 → 3.0.0-next.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +344 -318
  2. package/dist/index.js +607 -595
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1,12 +1,55 @@
1
- import { findImportSource, findProperty, findVariable, getVariableDefinitionNode } from "@eslint-react/var";
2
1
  import * as ast from "@eslint-react/ast";
3
- import { constFalse, constTrue, dual, flip, getOrElseUpdate, identity, unit } from "@eslint-react/eff";
2
+ import { constFalse, dual, flip, getOrElseUpdate, identity } from "@eslint-react/eff";
4
3
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
5
- import { IdGenerator, RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE } from "@eslint-react/shared";
6
- import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
4
+ import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
7
5
  import { P, match } from "ts-pattern";
6
+ import { IdGenerator, RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE } from "@eslint-react/shared";
7
+ import { resolve } from "@eslint-react/var";
8
8
  import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from "@typescript-eslint/utils";
9
9
 
10
+ //#region src/api/find-import-source.ts
11
+ /**
12
+ * Get the arguments of a require expression
13
+ * @param node The node to match
14
+ * @returns The require expression arguments or null if the node is not a require expression
15
+ * @internal
16
+ */
17
+ function getRequireExpressionArguments(node) {
18
+ return match(node).with({
19
+ type: AST_NODE_TYPES.CallExpression,
20
+ arguments: P.select(),
21
+ callee: {
22
+ type: AST_NODE_TYPES.Identifier,
23
+ name: "require"
24
+ }
25
+ }, identity).with({
26
+ type: AST_NODE_TYPES.MemberExpression,
27
+ object: P.select()
28
+ }, getRequireExpressionArguments).otherwise(() => null);
29
+ }
30
+ /**
31
+ * Find the import source of a variable
32
+ * @param name The variable name
33
+ * @param initialScope The initial scope to search
34
+ * @returns The import source or null if not found
35
+ */
36
+ function findImportSource(name, initialScope) {
37
+ const latestDef = findVariable(initialScope, name)?.defs.at(-1);
38
+ if (latestDef == null) return null;
39
+ const { node, parent } = latestDef;
40
+ if (node.type === AST_NODE_TYPES.VariableDeclarator && node.init != null) {
41
+ const { init } = node;
42
+ if (init.type === AST_NODE_TYPES.MemberExpression && init.object.type === AST_NODE_TYPES.Identifier) return findImportSource(init.object.name, initialScope);
43
+ if (init.type === AST_NODE_TYPES.Identifier) return findImportSource(init.name, initialScope);
44
+ const arg0 = getRequireExpressionArguments(init)?.[0];
45
+ if (arg0 == null || !ast.isLiteral(arg0, "string")) return null;
46
+ return arg0.value;
47
+ }
48
+ if (parent?.type === AST_NODE_TYPES.ImportDeclaration) return parent.source.value;
49
+ return null;
50
+ }
51
+
52
+ //#endregion
10
53
  //#region src/api/is-from-react.ts
11
54
  /**
12
55
  * Check if a variable is initialized from React import
@@ -260,7 +303,7 @@ function isHookId(id) {
260
303
 
261
304
  //#endregion
262
305
  //#region src/hook/hook-collector.ts
263
- const idGen$2 = new IdGenerator("hook_");
306
+ const idGen$2 = new IdGenerator("hook:");
264
307
  /**
265
308
  * Get a ctx and visitor object for the rule to collect hooks
266
309
  * @param context The ESLint rule context
@@ -270,7 +313,7 @@ function useHookCollector(context) {
270
313
  const hooks = /* @__PURE__ */ new Map();
271
314
  const functionEntries = [];
272
315
  const getText = (n) => context.sourceCode.getText(n);
273
- const getCurrentEntry = () => functionEntries.at(-1);
316
+ const getCurrentEntry = () => functionEntries.at(-1) ?? null;
274
317
  const onFunctionEnter = (node) => {
275
318
  const id = ast.getFunctionId(node);
276
319
  const key = idGen$2.next();
@@ -284,11 +327,11 @@ function useHookCollector(context) {
284
327
  key,
285
328
  kind: "function",
286
329
  name: ast.getFullyQualifiedName(id, getText),
287
- node,
288
330
  directives: [],
289
331
  flag: 0n,
290
332
  hint: 0n,
291
- hookCalls: []
333
+ hookCalls: [],
334
+ node
292
335
  });
293
336
  };
294
337
  const onFunctionExit = () => {
@@ -315,150 +358,6 @@ function useHookCollector(context) {
315
358
  };
316
359
  }
317
360
 
318
- //#endregion
319
- //#region src/jsx/jsx-stringify.ts
320
- /**
321
- * Incomplete but sufficient stringification of JSX nodes for common use cases
322
- *
323
- * @param node JSX node from TypeScript ESTree
324
- * @returns String representation of the JSX node
325
- */
326
- function stringifyJsx(node) {
327
- switch (node.type) {
328
- case AST_NODE_TYPES.JSXIdentifier: return node.name;
329
- case AST_NODE_TYPES.JSXNamespacedName: return `${node.namespace.name}:${node.name.name}`;
330
- case AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsx(node.object)}.${stringifyJsx(node.property)}`;
331
- case AST_NODE_TYPES.JSXText: return node.value;
332
- case AST_NODE_TYPES.JSXOpeningElement: return `<${stringifyJsx(node.name)}>`;
333
- case AST_NODE_TYPES.JSXClosingElement: return `</${stringifyJsx(node.name)}>`;
334
- case AST_NODE_TYPES.JSXOpeningFragment: return "<>";
335
- case AST_NODE_TYPES.JSXClosingFragment: return "</>";
336
- }
337
- }
338
-
339
- //#endregion
340
- //#region src/jsx/jsx-attribute-name.ts
341
- /**
342
- * Get the stringified name of a JSX attribute
343
- * @param context The ESLint rule context
344
- * @param node The JSX attribute node
345
- * @returns The name of the attribute
346
- */
347
- function getJsxAttributeName(context, node) {
348
- return stringifyJsx(node.name);
349
- }
350
-
351
- //#endregion
352
- //#region src/jsx/jsx-attribute.ts
353
- /**
354
- * Creates a helper function to find a specific JSX attribute by name
355
- * Handles direct attributes and spread attributes (variables or object literals)
356
- * @param context The ESLint rule context
357
- * @param node The JSX element node
358
- * @param initialScope (Optional) The initial scope to use for variable resolution
359
- */
360
- function getJsxAttribute(context, node, initialScope) {
361
- const scope = initialScope ?? context.sourceCode.getScope(node);
362
- const attributes = node.openingElement.attributes;
363
- /**
364
- * Finds the last occurrence of a specific attribute
365
- * @param name The attribute name to search for
366
- */
367
- return (name) => {
368
- return attributes.findLast((attr) => {
369
- if (attr.type === AST_NODE_TYPES.JSXAttribute) return getJsxAttributeName(context, attr) === name;
370
- switch (attr.argument.type) {
371
- case AST_NODE_TYPES.Identifier: {
372
- const variableNode = getVariableDefinitionNode(findVariable(attr.argument.name, scope), 0);
373
- if (variableNode?.type === AST_NODE_TYPES.ObjectExpression) return findProperty(name, variableNode.properties, scope) != null;
374
- return false;
375
- }
376
- case AST_NODE_TYPES.ObjectExpression: return findProperty(name, attr.argument.properties, scope) != null;
377
- }
378
- return false;
379
- });
380
- };
381
- }
382
-
383
- //#endregion
384
- //#region src/jsx/jsx-attribute-value.ts
385
- /**
386
- * Resolve the static value of a JSX attribute or spread attribute
387
- *
388
- * @param context - The ESLint rule context
389
- * @param attribute - The JSX attribute node to resolve
390
- * @returns An object containing the value kind, the node (if applicable), and a `toStatic` helper
391
- */
392
- function resolveJsxAttributeValue(context, attribute) {
393
- const initialScope = context.sourceCode.getScope(attribute);
394
- /**
395
- * Handles standard JSX attributes (e.g., prop="value", prop={value}, prop)
396
- * @param node The JSX attribute node
397
- */
398
- function handleJsxAttribute(node) {
399
- if (node.value == null) return {
400
- kind: "boolean",
401
- toStatic() {
402
- return true;
403
- }
404
- };
405
- switch (node.value.type) {
406
- case AST_NODE_TYPES.Literal: {
407
- const staticValue = node.value.value;
408
- return {
409
- kind: "literal",
410
- node: node.value,
411
- toStatic() {
412
- return staticValue;
413
- }
414
- };
415
- }
416
- case AST_NODE_TYPES.JSXExpressionContainer: {
417
- const expr = node.value.expression;
418
- return {
419
- kind: "expression",
420
- node: expr,
421
- toStatic() {
422
- return getStaticValue(expr, initialScope)?.value;
423
- }
424
- };
425
- }
426
- case AST_NODE_TYPES.JSXElement: return {
427
- kind: "element",
428
- node: node.value,
429
- toStatic() {
430
- return unit;
431
- }
432
- };
433
- case AST_NODE_TYPES.JSXSpreadChild: return {
434
- kind: "spreadChild",
435
- node: node.value.expression,
436
- toStatic() {
437
- return unit;
438
- }
439
- };
440
- }
441
- }
442
- /**
443
- * Handles JSX spread attributes (e.g., {...props})
444
- * @param node The JSX spread attribute node
445
- */
446
- function handleJsxSpreadAttribute(node) {
447
- return {
448
- kind: "spreadProps",
449
- node: node.argument,
450
- toStatic(name) {
451
- if (name == null) return unit;
452
- return match(getStaticValue(node.argument, initialScope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => unit);
453
- }
454
- };
455
- }
456
- switch (attribute.type) {
457
- case AST_NODE_TYPES.JSXAttribute: return handleJsxAttribute(attribute);
458
- case AST_NODE_TYPES.JSXSpreadAttribute: return handleJsxSpreadAttribute(attribute);
459
- }
460
- }
461
-
462
361
  //#endregion
463
362
  //#region src/jsx/jsx-config.ts
464
363
  const JsxEmit = {
@@ -531,25 +430,15 @@ const JsxDetectionHint = {
531
430
  */
532
431
  const DEFAULT_JSX_DETECTION_HINT = 0n | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue;
533
432
  /**
534
- * Check if a node is a `JSXText` or a `Literal` node
535
- * @param node The AST node to check
536
- * @returns `true` if the node is a `JSXText` or a `Literal` node
537
- */
538
- function isJsxText(node) {
539
- if (node == null) return false;
540
- return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
541
- }
542
- /**
543
433
  * Determine if a node represents JSX-like content based on heuristics
544
434
  * Supports configuration through hint flags to customize detection behavior
545
435
  *
546
- * @param code The source code with scope lookup capability
547
- * @param code.getScope The function to get the scope of a node
436
+ * @param context The rule context with scope lookup capability
548
437
  * @param node The AST node to analyze
549
438
  * @param hint The configuration flags to adjust detection behavior
550
439
  * @returns boolean Whether the node is considered JSX-like
551
440
  */
552
- function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
441
+ function isJsxLike(context, node, hint = DEFAULT_JSX_DETECTION_HINT) {
553
442
  if (node == null) return false;
554
443
  if (ast.isJSX(node)) return true;
555
444
  switch (node.type) {
@@ -565,27 +454,27 @@ function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
565
454
  case AST_NODE_TYPES.TemplateLiteral: return !(hint & JsxDetectionHint.DoNotIncludeJsxWithStringValue);
566
455
  case AST_NODE_TYPES.ArrayExpression:
567
456
  if (node.elements.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
568
- if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.elements.every((n) => isJsxLike(code, n, hint));
569
- return node.elements.some((n) => isJsxLike(code, n, hint));
457
+ if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.elements.every((n) => isJsxLike(context, n, hint));
458
+ return node.elements.some((n) => isJsxLike(context, n, hint));
570
459
  case AST_NODE_TYPES.LogicalExpression:
571
- if (hint & JsxDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx) return isJsxLike(code, node.left, hint) && isJsxLike(code, node.right, hint);
572
- return isJsxLike(code, node.left, hint) || isJsxLike(code, node.right, hint);
460
+ if (hint & JsxDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx) return isJsxLike(context, node.left, hint) && isJsxLike(context, node.right, hint);
461
+ return isJsxLike(context, node.left, hint) || isJsxLike(context, node.right, hint);
573
462
  case AST_NODE_TYPES.ConditionalExpression: {
574
463
  function leftHasJSX(node) {
575
464
  if (Array.isArray(node.consequent)) {
576
465
  if (node.consequent.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
577
- if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.consequent.every((n) => isJsxLike(code, n, hint));
578
- return node.consequent.some((n) => isJsxLike(code, n, hint));
466
+ if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.consequent.every((n) => isJsxLike(context, n, hint));
467
+ return node.consequent.some((n) => isJsxLike(context, n, hint));
579
468
  }
580
- return isJsxLike(code, node.consequent, hint);
469
+ return isJsxLike(context, node.consequent, hint);
581
470
  }
582
471
  function rightHasJSX(node) {
583
- return isJsxLike(code, node.alternate, hint);
472
+ return isJsxLike(context, node.alternate, hint);
584
473
  }
585
474
  if (hint & JsxDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx) return leftHasJSX(node) && rightHasJSX(node);
586
475
  return leftHasJSX(node) || rightHasJSX(node);
587
476
  }
588
- case AST_NODE_TYPES.SequenceExpression: return isJsxLike(code, node.expressions.at(-1), hint);
477
+ case AST_NODE_TYPES.SequenceExpression: return isJsxLike(context, node.expressions.at(-1) ?? null, hint);
589
478
  case AST_NODE_TYPES.CallExpression:
590
479
  if (hint & JsxDetectionHint.DoNotIncludeJsxWithCreateElementValue) return false;
591
480
  switch (node.callee.type) {
@@ -593,228 +482,398 @@ function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
593
482
  case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
594
483
  }
595
484
  return false;
596
- case AST_NODE_TYPES.Identifier: {
597
- const { name } = node;
598
- if (name === "undefined") return !(hint & JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue);
485
+ case AST_NODE_TYPES.Identifier:
486
+ if (node.name === "undefined") return !(hint & JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue);
599
487
  if (ast.isJSXTagNameExpression(node)) return true;
600
- return isJsxLike(code, getVariableDefinitionNode(findVariable(name, code.getScope(node)), 0), hint);
601
- }
488
+ return isJsxLike(context, resolve(context, node), hint);
602
489
  }
603
490
  return false;
604
491
  }
605
492
 
606
493
  //#endregion
607
- //#region src/jsx/jsx-element-type.ts
494
+ //#region src/jsx/jsx-stringify.ts
608
495
  /**
609
- * Extracts the element type name from a JSX element or fragment
610
- * For JSX elements, returns the stringified name (e.g., "div", "Button", "React.Fragment")
611
- * For JSX fragments, returns an empty string
496
+ * Incomplete but sufficient stringification of JSX nodes for common use cases
612
497
  *
613
- * @param context ESLint rule context
614
- * @param node JSX element or fragment node
615
- * @returns String representation of the element type
498
+ * @param node JSX node from TypeScript ESTree
499
+ * @returns String representation of the JSX node
616
500
  */
617
- function getJsxElementType(context, node) {
618
- if (node.type === AST_NODE_TYPES.JSXFragment) return "";
619
- return stringifyJsx(node.openingElement.name);
501
+ function stringifyJsx(node) {
502
+ switch (node.type) {
503
+ case AST_NODE_TYPES.JSXIdentifier: return node.name;
504
+ case AST_NODE_TYPES.JSXNamespacedName: return `${node.namespace.name}:${node.name.name}`;
505
+ case AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsx(node.object)}.${stringifyJsx(node.property)}`;
506
+ case AST_NODE_TYPES.JSXText: return node.value;
507
+ case AST_NODE_TYPES.JSXOpeningElement: return `<${stringifyJsx(node.name)}>`;
508
+ case AST_NODE_TYPES.JSXClosingElement: return `</${stringifyJsx(node.name)}>`;
509
+ case AST_NODE_TYPES.JSXOpeningFragment: return "<>";
510
+ case AST_NODE_TYPES.JSXClosingFragment: return "</>";
511
+ }
620
512
  }
621
513
 
622
514
  //#endregion
623
- //#region src/jsx/jsx-element-is.ts
515
+ //#region src/jsx/jsx-inspector.ts
624
516
  /**
625
- * Determine if a JSX element is a host element
626
- * Host elements in React start with lowercase letters (e.g., div, span)
517
+ * A stateful helper that binds an ESLint `RuleContext` once and exposes
518
+ * ergonomic methods for the most common JSX inspection tasks that rules need.
627
519
  *
628
- * @param context ESLint rule context
629
- * @param node AST node to check
630
- * @returns boolean indicating if the element is a host element
631
- */
632
- function isJsxHostElement(context, node) {
633
- return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
634
- }
635
- /**
636
- * Determine if a JSX element is a React Fragment
637
- * Fragments can be imported from React and used like <Fragment> or <React.Fragment>
520
+ * ### Typical usage inside a rule's `create` function
638
521
  *
639
- * @param context ESLint rule context
640
- * @param node AST node to check
641
- * @param jsxConfig Optional JSX configuration
642
- * @param jsxConfig.jsxFragmentFactory Name of the fragment factory (e.g., React.Fragment)
643
- * @returns boolean indicating if the element is a Fragment
644
- */
645
- function isJsxFragmentElement(context, node, jsxConfig) {
646
- if (node.type !== AST_NODE_TYPES.JSXElement) return false;
647
- const fragment = jsxConfig?.jsxFragmentFactory?.split(".").at(-1) ?? "Fragment";
648
- return getJsxElementType(context, node).split(".").at(-1) === fragment;
649
- }
650
-
651
- //#endregion
652
- //#region src/jsx/jsx-hierarchy.ts
653
- /**
654
- * Traverses up the AST to find a parent JSX attribute node that matches a given test
522
+ * ```ts
523
+ * export function create(context: RuleContext) {
524
+ * const jsx = JsxInspector.from(context);
655
525
  *
656
- * @param node The starting AST node
657
- * @param test Optional predicate function to test if the attribute meets criteria
658
- * Defaults to always returning true (matches any attribute)
659
- * @returns The first matching JSX attribute node found when traversing upwards, or undefined
660
- */
661
- function findParentJsxAttribute(node, test = constTrue) {
662
- const guard = (node) => {
663
- return node.type === AST_NODE_TYPES.JSXAttribute && test(node);
664
- };
665
- return ast.findParentNode(node, guard);
666
- }
667
-
668
- //#endregion
669
- //#region src/component/component-detection-hint.ts
670
- /**
671
- * Hints for component collector
672
- */
673
- const ComponentDetectionHint = {
674
- ...JsxDetectionHint,
675
- DoNotIncludeFunctionDefinedOnObjectMethod: 1n << 64n,
676
- DoNotIncludeFunctionDefinedOnClassMethod: 1n << 65n,
677
- DoNotIncludeFunctionDefinedOnClassProperty: 1n << 66n,
678
- DoNotIncludeFunctionDefinedInArrayPattern: 1n << 67n,
679
- DoNotIncludeFunctionDefinedInArrayExpression: 1n << 68n,
680
- DoNotIncludeFunctionDefinedAsArrayMapCallback: 1n << 69n,
681
- DoNotIncludeFunctionDefinedAsArrayFlatMapCallback: 1n << 70n
682
- };
683
- /**
684
- * Default component detection hint
685
- */
686
- const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.DoNotIncludeJsxWithBigIntValue | ComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | ComponentDetectionHint.DoNotIncludeJsxWithNumberValue | ComponentDetectionHint.DoNotIncludeJsxWithStringValue | ComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern | ComponentDetectionHint.RequireAllArrayElementsToBeJsx | ComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | ComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx;
687
-
688
- //#endregion
689
- //#region src/component/component-is.ts
690
- /**
691
- * Check if a node is a React class component
692
- * @param node The AST node to check
693
- * @returns `true` if the node is a class component, `false` otherwise
526
+ * return defineRuleListener({
527
+ * JSXElement(node) {
528
+ * // element type
529
+ * const type = jsx.getElementType(node); // "div" | "React.Fragment" |
530
+ *
531
+ * // attribute lookup + value resolution in one step
532
+ * const val = jsx.getAttributeValue(node, "sandbox");
533
+ * if (typeof val?.getStatic() === "string") { … }
534
+ *
535
+ * // simple boolean checks
536
+ * if (jsx.isHostElement(node)) { … }
537
+ * if (jsx.isFragmentElement(node)) { … }
538
+ * if (jsx.hasAttribute(node, "key")) { … }
539
+ * },
540
+ * });
541
+ * }
542
+ * ```
694
543
  */
695
- function isClassComponent(node) {
696
- if ("superClass" in node && node.superClass != null) {
697
- const re = /^(?:Pure)?Component$/u;
698
- switch (true) {
699
- case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
700
- case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
701
- }
544
+ var JsxInspector = class JsxInspector {
545
+ context;
546
+ /**
547
+ * Merged JSX configuration (tsconfig compiler options + pragma annotations).
548
+ * The result is lazily computed and cached for the lifetime of this inspector.
549
+ */
550
+ get jsxConfig() {
551
+ return this.#jsxConfig ??= {
552
+ ...getJsxConfigFromContext(this.context),
553
+ ...getJsxConfigFromAnnotation(this.context)
554
+ };
702
555
  }
703
- return false;
704
- }
705
- /**
706
- * Check if a node is a React PureComponent
707
- * @param node The AST node to check
708
- * @returns `true` if the node is a PureComponent, `false` otherwise
709
- */
710
- function isPureComponent(node) {
711
- if ("superClass" in node && node.superClass != null) {
712
- const re = /^PureComponent$/u;
713
- switch (true) {
714
- case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
715
- case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
716
- }
556
+ /**
557
+ * Lazily resolved & cached JSX configuration (merged from tsconfig +
558
+ * pragma annotations). Use {@link jsxConfig} to access.
559
+ */
560
+ #jsxConfig;
561
+ constructor(context) {
562
+ this.context = context;
717
563
  }
718
- return false;
719
- }
720
-
721
- //#endregion
722
- //#region src/component/component-wrapper.ts
723
- /**
724
- * Check if the node is a call expression for a component wrapper
725
- * @param context The ESLint rule context
726
- * @param node The node to check
727
- * @returns `true` if the node is a call expression for a component wrapper
728
- */
729
- function isComponentWrapperCall(context, node) {
730
- if (node.type !== AST_NODE_TYPES.CallExpression) return false;
731
- return isMemoCall(context, node) || isForwardRefCall(context, node);
732
- }
733
- /**
734
- * Check if the node is a call expression for a component wrapper loosely
735
- * @param context The ESLint rule context
736
- * @param node The node to check
737
- * @returns `true` if the node is a call expression for a component wrapper loosely
738
- */
739
- function isComponentWrapperCallLoose(context, node) {
740
- if (node.type !== AST_NODE_TYPES.CallExpression) return false;
741
- return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
742
- }
743
- /**
744
- * Check if the node is a callback function passed to a component wrapper
745
- * @param context The ESLint rule context
746
- * @param node The node to check
747
- * @returns `true` if the node is a callback function passed to a component wrapper
748
- */
749
- function isComponentWrapperCallback(context, node) {
750
- if (!ast.isFunction(node)) return false;
751
- const parent = node.parent;
752
- if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
753
- return isComponentWrapperCall(context, parent);
754
- }
755
- /**
756
- * Check if the node is a callback function passed to a component wrapper loosely
757
- * @param context The ESLint rule context
758
- * @param node The node to check
759
- * @returns `true` if the node is a callback function passed to a component wrapper loosely
760
- */
761
- function isComponentWrapperCallbackLoose(context, node) {
762
- if (!ast.isFunction(node)) return false;
763
- const parent = node.parent;
764
- if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
765
- return isComponentWrapperCallLoose(context, parent);
766
- }
767
-
768
- //#endregion
769
- //#region src/component/component-id.ts
770
- /**
771
- * Get function component identifier from `const Component = memo(() => {});`
772
- * @param context The rule context
773
- * @param node The function node to analyze
774
- * @returns The function identifier or `unit` if not found
775
- */
776
- function getFunctionComponentId(context, node) {
777
- const functionId = ast.getFunctionId(node);
778
- if (functionId != null) return functionId;
779
- const { parent } = node;
780
- if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
781
- if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
782
- return unit;
783
- }
564
+ /**
565
+ * Walk **up** the AST from `node` to find the nearest ancestor that is a
566
+ * `JSXAttribute` and passes the optional `test` predicate.
567
+ * @param node The starting node for the search.
568
+ * @param test A predicate function to test each ancestor node.
569
+ */
570
+ static findParentAttribute(node, test = () => true) {
571
+ const guard = (n) => {
572
+ return n.type === AST_NODE_TYPES.JSXAttribute && test(n);
573
+ };
574
+ return ast.findParentNode(node, guard);
575
+ }
576
+ /**
577
+ * Create a new `JsxInspector` bound to the given rule context.
578
+ * @param context The ESLint rule context to bind to this inspector instance.
579
+ */
580
+ static from(context) {
581
+ return new JsxInspector(context);
582
+ }
583
+ /**
584
+ * Whether the node is a `JSXText` or a `Literal` node.
585
+ * @param node The node to check.
586
+ */
587
+ static isJsxText(node) {
588
+ if (node == null) return false;
589
+ return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
590
+ }
591
+ /**
592
+ * Find a JSX attribute (or spread attribute containing the property) by name
593
+ * on a given element.
594
+ *
595
+ * Returns the **last** matching attribute (to mirror React's behaviour where
596
+ * later props win), or `undefined` if not found.
597
+ * @param node The JSX element to search for the attribute.
598
+ * @param name The name of the attribute to find (e.g. `"className"`).
599
+ * @param initialScope An optional scope to use for resolving spread attributes. If not provided,
600
+ */
601
+ findAttribute(node, name, initialScope) {
602
+ initialScope ?? this.context.sourceCode.getScope(node);
603
+ return node.openingElement.attributes.findLast((attr) => {
604
+ if (attr.type === AST_NODE_TYPES.JSXAttribute) return stringifyJsx(attr.name) === name;
605
+ switch (attr.argument.type) {
606
+ case AST_NODE_TYPES.Identifier: {
607
+ const initNode = resolve(this.context, attr.argument);
608
+ if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return ast.findProperty(initNode.properties, name) != null;
609
+ return false;
610
+ }
611
+ case AST_NODE_TYPES.ObjectExpression: return ast.findProperty(attr.argument.properties, name) != null;
612
+ }
613
+ return false;
614
+ });
615
+ }
616
+ /**
617
+ * Get the stringified name of a `JSXAttribute` node
618
+ * (e.g. `"className"`, `"aria-label"`, `"xml:space"`).
619
+ * @param node The `JSXAttribute` node to extract the name from.
620
+ * @returns The stringified name of the attribute.
621
+ */
622
+ getAttributeName(node) {
623
+ return stringifyJsx(node.name);
624
+ }
625
+ /**
626
+ * Resolve the static value of an attribute, automatically handling the
627
+ * `spreadProps` case by extracting the named property.
628
+ *
629
+ * This eliminates the repetitive pattern:
630
+ * ```ts
631
+ * const v = core.resolveJsxAttributeValue(ctx, attr);
632
+ * const s = v.kind === "spreadProps" ? v.getProperty(name) : v.toStatic();
633
+ * ```
634
+ *
635
+ * Returns `undefined` when the attribute is not present or its value
636
+ * cannot be statically determined.
637
+ * @param node The JSX element to search for the attribute.
638
+ * @param name The name of the attribute to resolve (e.g. `"className"`).
639
+ * @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
640
+ * @returns The static value of the attribute, or `undefined` if not found or not statically resolvable.
641
+ */
642
+ getAttributeStaticValue(node, name, initialScope) {
643
+ const attr = this.findAttribute(node, name, initialScope);
644
+ if (attr == null) return void 0;
645
+ const resolved = this.resolveAttributeValue(attr);
646
+ if (resolved.kind === "spreadProps") return resolved.getProperty(name);
647
+ return resolved.toStatic();
648
+ }
649
+ /**
650
+ * **All-in-one helper** – find an attribute by name on an element *and*
651
+ * resolve its value in a single call.
652
+ *
653
+ * Returns `undefined` when the attribute is not present.
654
+ * @param node The JSX element to search for the attribute.
655
+ * @param name The name of the attribute to find and resolve (e.g. `"className"`).
656
+ * @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
657
+ * @returns A descriptor of the attribute's value that can be further inspected, or `undefined` if the attribute is not found.
658
+ */
659
+ getAttributeValue(node, name, initialScope) {
660
+ const attr = this.findAttribute(node, name, initialScope);
661
+ if (attr == null) return void 0;
662
+ return this.resolveAttributeValue(attr);
663
+ }
664
+ /**
665
+ * Get the **self name** (last segment) of a JSX element type.
666
+ *
667
+ * - `<Foo.Bar.Baz>` → `"Baz"`
668
+ * - `<div>` → `"div"`
669
+ * - `<></>` → `""`
670
+ * @param node The JSX element or fragment to extract the self name from.
671
+ */
672
+ getElementSelfName(node) {
673
+ return this.getElementType(node).split(".").at(-1) ?? "";
674
+ }
675
+ /**
676
+ * Get the string representation of a JSX element's type.
677
+ *
678
+ * - `<div>` → `"div"`
679
+ * - `<Foo.Bar>` → `"Foo.Bar"`
680
+ * - `<React.Fragment>` → `"React.Fragment"`
681
+ * - `<></>` (JSXFragment) → `""`
682
+ * @param node The JSX element or fragment to extract the type from.
683
+ */
684
+ getElementType(node) {
685
+ if (node.type === AST_NODE_TYPES.JSXFragment) return "";
686
+ return stringifyJsx(node.openingElement.name);
687
+ }
688
+ /**
689
+ * Shorthand: check whether an attribute exists on the element.
690
+ * @param node The JSX element to check for the attribute.
691
+ * @param name The name of the attribute to check for (e.g. `"className"`).
692
+ * @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
693
+ * @returns `true` if the attribute exists on the element, `false` otherwise.
694
+ */
695
+ hasAttribute(node, name, initialScope) {
696
+ return this.findAttribute(node, name, initialScope) != null;
697
+ }
698
+ /**
699
+ * Whether the node is a React **Fragment** element (either `<Fragment>` /
700
+ * `<React.Fragment>` or the shorthand `<>` syntax).
701
+ *
702
+ * The check honours the configured `jsxFragmentFactory`.
703
+ * @param node The node to check.
704
+ */
705
+ isFragmentElement(node) {
706
+ if (node.type === AST_NODE_TYPES.JSXFragment) return true;
707
+ if (node.type !== AST_NODE_TYPES.JSXElement) return false;
708
+ const fragment = this.jsxConfig.jsxFragmentFactory.split(".").at(-1) ?? "Fragment";
709
+ return this.getElementType(node).split(".").at(-1) === fragment;
710
+ }
711
+ /**
712
+ * Whether the node is a **host** (intrinsic / DOM) element – i.e. its tag
713
+ * name starts with a lowercase letter.
714
+ * @param node The node to check.
715
+ */
716
+ isHostElement(node) {
717
+ return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
718
+ }
719
+ /**
720
+ * Resolve the *value* of a JSX attribute (or spread attribute) into a
721
+ * descriptor that can be inspected further.
722
+ *
723
+ * See {@link JsxAttributeValue} for the full set of `kind` discriminants.
724
+ * @param attribute The attribute node to resolve the value of.
725
+ * @returns A descriptor of the attribute's value that can be further inspected.
726
+ */
727
+ resolveAttributeValue(attribute) {
728
+ const initialScope = this.context.sourceCode.getScope(attribute);
729
+ if (attribute.type === AST_NODE_TYPES.JSXAttribute) return this.#resolveJsxAttribute(attribute, initialScope);
730
+ return this.#resolveJsxSpreadAttribute(attribute, initialScope);
731
+ }
732
+ #resolveJsxAttribute(node, initialScope) {
733
+ if (node.value == null) return {
734
+ kind: "boolean",
735
+ toStatic() {
736
+ return true;
737
+ }
738
+ };
739
+ switch (node.value.type) {
740
+ case AST_NODE_TYPES.Literal: {
741
+ const staticValue = node.value.value;
742
+ return {
743
+ kind: "literal",
744
+ node: node.value,
745
+ toStatic() {
746
+ return staticValue;
747
+ }
748
+ };
749
+ }
750
+ case AST_NODE_TYPES.JSXExpressionContainer: {
751
+ const expr = node.value.expression;
752
+ if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
753
+ kind: "missing",
754
+ node: expr,
755
+ toStatic() {
756
+ return "{}";
757
+ }
758
+ };
759
+ return {
760
+ kind: "expression",
761
+ node: expr,
762
+ toStatic() {
763
+ return getStaticValue(expr, initialScope)?.value;
764
+ }
765
+ };
766
+ }
767
+ case AST_NODE_TYPES.JSXElement: return {
768
+ kind: "element",
769
+ node: node.value,
770
+ toStatic() {
771
+ return null;
772
+ }
773
+ };
774
+ case AST_NODE_TYPES.JSXSpreadChild: {
775
+ const expr = node.value.expression;
776
+ return {
777
+ kind: "spreadChild",
778
+ getChildren(_at) {
779
+ return null;
780
+ },
781
+ node: node.value.expression,
782
+ toStatic() {
783
+ return getStaticValue(expr, initialScope)?.value;
784
+ }
785
+ };
786
+ }
787
+ }
788
+ }
789
+ #resolveJsxSpreadAttribute(node, initialScope) {
790
+ return {
791
+ kind: "spreadProps",
792
+ getProperty(name) {
793
+ return match(getStaticValue(node.argument, initialScope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => null);
794
+ },
795
+ node: node.argument,
796
+ toStatic() {
797
+ return getStaticValue(node.argument, initialScope)?.value;
798
+ }
799
+ };
800
+ }
801
+ };
784
802
 
785
803
  //#endregion
786
- //#region src/component/component-name.ts
804
+ //#region src/component/component-detection-legacy.ts
787
805
  /**
788
- * Check if a string matches the strict component name pattern
789
- * @param name The name to check
806
+ * Check if a node is a React class component
807
+ * @param node The AST node to check
808
+ * @returns `true` if the node is a class component, `false` otherwise
790
809
  */
791
- function isComponentName(name) {
792
- return RE_COMPONENT_NAME.test(name);
810
+ function isClassComponent(node) {
811
+ if ("superClass" in node && node.superClass != null) {
812
+ const re = /^(?:Pure)?Component$/u;
813
+ switch (true) {
814
+ case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
815
+ case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
816
+ }
817
+ }
818
+ return false;
793
819
  }
794
820
  /**
795
- * Check if a string matches the loose component name pattern
796
- * @param name The name to check
821
+ * Check if a node is a React PureComponent
822
+ * @param node The AST node to check
823
+ * @returns `true` if the node is a PureComponent, `false` otherwise
797
824
  */
798
- function isComponentNameLoose(name) {
799
- return RE_COMPONENT_NAME_LOOSE.test(name);
825
+ function isPureComponent(node) {
826
+ if ("superClass" in node && node.superClass != null) {
827
+ const re = /^PureComponent$/u;
828
+ switch (true) {
829
+ case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
830
+ case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
831
+ }
832
+ }
833
+ return false;
800
834
  }
801
835
  /**
802
- * Check if a function has a loose component name
803
- * @param context The rule context
804
- * @param fn The function to check
805
- * @param allowNone Whether to allow no name
806
- * @returns Whether the function has a loose component name
836
+ * Create a lifecycle method checker function
837
+ * @param methodName The lifecycle method name
838
+ * @param isStatic Whether the method is static
807
839
  */
808
- function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
809
- const id = getFunctionComponentId(context, fn);
810
- if (id == null) return allowNone;
811
- if (id.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.name);
812
- if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.property.name);
813
- return false;
840
+ function createLifecycleChecker(methodName, isStatic = false) {
841
+ return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
842
+ }
843
+ const isRender = createLifecycleChecker("render");
844
+ const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
845
+ const isComponentDidMount = createLifecycleChecker("componentDidMount");
846
+ const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
847
+ const isComponentWillMount = createLifecycleChecker("componentWillMount");
848
+ const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
849
+ const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
850
+ const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
851
+ const isGetChildContext = createLifecycleChecker("getChildContext");
852
+ const isGetInitialState = createLifecycleChecker("getInitialState");
853
+ const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
854
+ const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
855
+ const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
856
+ const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
857
+ const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
858
+ const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
859
+ const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
860
+ const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
861
+ /**
862
+ * Check if the given node is a componentDidMount callback
863
+ * @param node The node to check
864
+ * @returns True if the node is a componentDidMount callback, false otherwise
865
+ */
866
+ function isComponentDidMountCallback(node) {
867
+ return ast.isFunction(node) && isComponentDidMount(node.parent) && node.parent.value === node;
868
+ }
869
+ /**
870
+ * Check if the given node is a componentWillUnmount callback
871
+ * @param node The node to check
872
+ * @returns True if the node is a componentWillUnmount callback, false otherwise
873
+ */
874
+ function isComponentWillUnmountCallback(node) {
875
+ return ast.isFunction(node) && isComponentWillUnmount(node.parent) && node.parent.value === node;
814
876
  }
815
-
816
- //#endregion
817
- //#region src/component/component-render-method.ts
818
877
  /**
819
878
  * Check whether given node is a render method of a class component
820
879
  * @example
@@ -830,11 +889,8 @@ function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
830
889
  function isRenderMethodLike(node) {
831
890
  return ast.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render") && node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration;
832
891
  }
833
-
834
- //#endregion
835
- //#region src/component/component-definition.ts
836
892
  /**
837
- * Check if the given node is a function within a render method of a class component.
893
+ * Check if the given node is a function within a render method of a class component
838
894
  *
839
895
  * @param node The AST node to check
840
896
  * @returns `true` if the node is a render function inside a class component
@@ -852,38 +908,139 @@ function isRenderMethodCallback(node) {
852
908
  return greatGrandparent != null && isRenderMethodLike(parent) && isClassComponent(greatGrandparent);
853
909
  }
854
910
  /**
855
- * Check if a function node should be excluded based on provided detection hints
856
- *
857
- * @param node The function node to check
858
- * @param hint Component detection hints as bit flags
859
- * @returns `true` if the function matches an exclusion hint
911
+ * Check whether the given node is a this.setState() call
912
+ * @param node The node to check
913
+ * @internal
860
914
  */
861
- function shouldExcludeBasedOnHint(node, hint) {
862
- switch (true) {
863
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnObjectMethod && ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property && node.parent.parent.type === AST_NODE_TYPES.ObjectExpression: return true;
864
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassMethod && ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.MethodDefinition: return true;
865
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassProperty && ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property: return true;
866
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern && node.parent.type === AST_NODE_TYPES.ArrayPattern: return true;
867
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression && node.parent.type === AST_NODE_TYPES.ArrayExpression: return true;
868
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "map": return true;
869
- case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "flatMap": return true;
870
- }
871
- return false;
915
+ function isThisSetState(node) {
916
+ const { callee } = node;
917
+ return callee.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "setState";
872
918
  }
873
919
  /**
874
- * Determine if the node is an argument within `createElement`'s children list (3rd argument onwards)
875
- *
876
- * @param context The rule context
877
- * @param node The AST node to check
878
- * @returns `true` if the node is passed as a child to `createElement`
920
+ * Check whether the given node is an assignment to this.state
921
+ * @param node The node to check
922
+ * @internal
923
+ */
924
+ function isAssignmentToThisState(node) {
925
+ const { left } = node;
926
+ return left.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
927
+ }
928
+
929
+ //#endregion
930
+ //#region src/component/component-wrapper.ts
931
+ /**
932
+ * Check if the node is a call expression for a component wrapper
933
+ * @param context The ESLint rule context
934
+ * @param node The node to check
935
+ * @returns `true` if the node is a call expression for a component wrapper
936
+ */
937
+ function isComponentWrapperCall(context, node) {
938
+ if (node.type !== AST_NODE_TYPES.CallExpression) return false;
939
+ return isMemoCall(context, node) || isForwardRefCall(context, node);
940
+ }
941
+ /**
942
+ * Check if the node is a call expression for a component wrapper loosely
943
+ * @param context The ESLint rule context
944
+ * @param node The node to check
945
+ * @returns `true` if the node is a call expression for a component wrapper loosely
946
+ */
947
+ function isComponentWrapperCallLoose(context, node) {
948
+ if (node.type !== AST_NODE_TYPES.CallExpression) return false;
949
+ return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
950
+ }
951
+ /**
952
+ * Check if the node is a callback function passed to a component wrapper
953
+ * @param context The ESLint rule context
954
+ * @param node The node to check
955
+ * @returns `true` if the node is a callback function passed to a component wrapper
879
956
  */
880
- function isChildrenOfCreateElement(context, node) {
957
+ function isComponentWrapperCallback(context, node) {
958
+ if (!ast.isFunction(node)) return false;
881
959
  const parent = node.parent;
882
- if (parent?.type !== AST_NODE_TYPES.CallExpression) return false;
883
- if (!isCreateElementCall(context, parent)) return false;
884
- return parent.arguments.slice(2).some((arg) => arg === node);
960
+ if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
961
+ return isComponentWrapperCall(context, parent);
962
+ }
963
+ /**
964
+ * Check if the node is a callback function passed to a component wrapper loosely
965
+ * @param context The ESLint rule context
966
+ * @param node The node to check
967
+ * @returns `true` if the node is a callback function passed to a component wrapper loosely
968
+ */
969
+ function isComponentWrapperCallbackLoose(context, node) {
970
+ if (!ast.isFunction(node)) return false;
971
+ const parent = node.parent;
972
+ if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
973
+ return isComponentWrapperCallLoose(context, parent);
974
+ }
975
+
976
+ //#endregion
977
+ //#region src/component/component-id.ts
978
+ /**
979
+ * Get function component identifier from `const Component = memo(() => {});`
980
+ * @param context The rule context
981
+ * @param node The function node to analyze
982
+ * @returns The function identifier or `null` if not found
983
+ */
984
+ function getFunctionComponentId(context, node) {
985
+ const functionId = ast.getFunctionId(node);
986
+ if (functionId != null) return functionId;
987
+ const { parent } = node;
988
+ if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
989
+ if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
990
+ return null;
991
+ }
992
+
993
+ //#endregion
994
+ //#region src/component/component-name.ts
995
+ /**
996
+ * Check if a string matches the strict component name pattern
997
+ * @param name The name to check
998
+ */
999
+ function isComponentName(name) {
1000
+ return RE_COMPONENT_NAME.test(name);
1001
+ }
1002
+ /**
1003
+ * Check if a string matches the loose component name pattern
1004
+ * @param name The name to check
1005
+ */
1006
+ function isComponentNameLoose(name) {
1007
+ return RE_COMPONENT_NAME_LOOSE.test(name);
885
1008
  }
886
1009
  /**
1010
+ * Check if a function has a loose component name
1011
+ * @param context The rule context
1012
+ * @param fn The function to check
1013
+ * @param allowNone Whether to allow no name
1014
+ * @returns Whether the function has a loose component name
1015
+ */
1016
+ function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
1017
+ const id = getFunctionComponentId(context, fn);
1018
+ if (id == null) return allowNone;
1019
+ if (id.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.name);
1020
+ if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.property.name);
1021
+ return false;
1022
+ }
1023
+
1024
+ //#endregion
1025
+ //#region src/component/component-detection.ts
1026
+ /**
1027
+ * Hints for component collector
1028
+ */
1029
+ const ComponentDetectionHint = {
1030
+ ...JsxDetectionHint,
1031
+ DoNotIncludeFunctionDefinedAsArrayFlatMapCallback: 1n << 17n,
1032
+ DoNotIncludeFunctionDefinedAsArrayMapCallback: 1n << 16n,
1033
+ DoNotIncludeFunctionDefinedInArrayExpression: 1n << 15n,
1034
+ DoNotIncludeFunctionDefinedInArrayPattern: 1n << 14n,
1035
+ DoNotIncludeFunctionDefinedOnClassMethod: 1n << 12n,
1036
+ DoNotIncludeFunctionDefinedOnClassProperty: 1n << 13n,
1037
+ DoNotIncludeFunctionDefinedOnObjectMethod: 1n << 11n
1038
+ };
1039
+ /**
1040
+ * Default component detection hint
1041
+ */
1042
+ const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.DoNotIncludeJsxWithBigIntValue | ComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | ComponentDetectionHint.DoNotIncludeJsxWithNumberValue | ComponentDetectionHint.DoNotIncludeJsxWithStringValue | ComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern | ComponentDetectionHint.RequireAllArrayElementsToBeJsx | ComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | ComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx;
1043
+ /**
887
1044
  * Determine if a function node represents a valid React component definition
888
1045
  *
889
1046
  * @param context The rule context
@@ -893,8 +1050,33 @@ function isChildrenOfCreateElement(context, node) {
893
1050
  */
894
1051
  function isComponentDefinition(context, node, hint) {
895
1052
  if (!isFunctionWithLooseComponentName(context, node, true)) return false;
896
- if (isChildrenOfCreateElement(context, node) || isRenderMethodCallback(node)) return false;
897
- if (shouldExcludeBasedOnHint(node, hint)) return false;
1053
+ switch (true) {
1054
+ case node.parent.type === AST_NODE_TYPES.CallExpression && isCreateElementCall(context, node.parent) && node.parent.arguments.slice(2).some((arg) => arg === node): return false;
1055
+ case isRenderMethodCallback(node): return false;
1056
+ }
1057
+ switch (true) {
1058
+ case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property && node.parent.parent.type === AST_NODE_TYPES.ObjectExpression:
1059
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnObjectMethod) return false;
1060
+ break;
1061
+ case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.MethodDefinition:
1062
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassMethod) return false;
1063
+ break;
1064
+ case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property:
1065
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassProperty) return false;
1066
+ break;
1067
+ case node.parent.type === AST_NODE_TYPES.ArrayPattern:
1068
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern) return false;
1069
+ break;
1070
+ case node.parent.type === AST_NODE_TYPES.ArrayExpression:
1071
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression) return false;
1072
+ break;
1073
+ case node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "map":
1074
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback) return false;
1075
+ break;
1076
+ case node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "flatMap":
1077
+ if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback) return false;
1078
+ break;
1079
+ }
898
1080
  const significantParent = ast.findParentNode(node, ast.isOneOf([
899
1081
  AST_NODE_TYPES.JSXExpressionContainer,
900
1082
  AST_NODE_TYPES.ArrowFunctionExpression,
@@ -913,15 +1095,12 @@ function isComponentDefinition(context, node, hint) {
913
1095
  * Component flag constants
914
1096
  */
915
1097
  const ComponentFlag = {
916
- None: 0n,
917
- PureComponent: 1n << 0n,
918
1098
  CreateElement: 1n << 1n,
1099
+ ForwardRef: 1n << 3n,
919
1100
  Memo: 1n << 2n,
920
- ForwardRef: 1n << 3n
1101
+ None: 0n,
1102
+ PureComponent: 1n << 0n
921
1103
  };
922
-
923
- //#endregion
924
- //#region src/component/component-init-path.ts
925
1104
  /**
926
1105
  * Get component flag from init path
927
1106
  * @param initPath The init path of the function component
@@ -936,7 +1115,7 @@ function getComponentFlagFromInitPath(initPath) {
936
1115
 
937
1116
  //#endregion
938
1117
  //#region src/component/component-collector.ts
939
- const idGen$1 = new IdGenerator("function_component_");
1118
+ const idGen$1 = new IdGenerator("function-component:");
940
1119
  /**
941
1120
  * Get a ctx and visitor object for the rule to collect function components
942
1121
  * @param context The ESLint rule context
@@ -948,14 +1127,14 @@ function useComponentCollector(context, options = {}) {
948
1127
  const functionEntries = [];
949
1128
  const components = /* @__PURE__ */ new Map();
950
1129
  const getText = (n) => context.sourceCode.getText(n);
951
- const getCurrentEntry = () => functionEntries.at(-1);
1130
+ const getCurrentEntry = () => functionEntries.at(-1) ?? null;
952
1131
  const onFunctionEnter = (node) => {
953
1132
  const key = idGen$1.next();
954
1133
  const exp = ast.findParentNode(node, (n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
955
1134
  const isExportDefault = exp != null;
956
1135
  const isExportDefaultDeclaration = exp != null && ast.getUnderlyingExpression(exp.declaration) === node;
957
1136
  const id = getFunctionComponentId(context, node);
958
- const name = id == null ? unit : ast.getFullyQualifiedName(id, getText);
1137
+ const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
959
1138
  const initPath = ast.getFunctionInitPath(node);
960
1139
  const directives = ast.getFunctionDirectives(node);
961
1140
  const entry = {
@@ -963,9 +1142,8 @@ function useComponentCollector(context, options = {}) {
963
1142
  key,
964
1143
  kind: "function-component",
965
1144
  name,
966
- node,
967
1145
  directives,
968
- displayName: unit,
1146
+ displayName: null,
969
1147
  flag: getComponentFlagFromInitPath(initPath),
970
1148
  hint,
971
1149
  hookCalls: [],
@@ -973,6 +1151,7 @@ function useComponentCollector(context, options = {}) {
973
1151
  isComponentDefinition: isComponentDefinition(context, node, hint),
974
1152
  isExportDefault,
975
1153
  isExportDefaultDeclaration,
1154
+ node,
976
1155
  rets: []
977
1156
  };
978
1157
  functionEntries.push(entry);
@@ -1002,13 +1181,13 @@ function useComponentCollector(context, options = {}) {
1002
1181
  if (body.type === AST_NODE_TYPES.BlockStatement) return;
1003
1182
  entry.rets.push(body);
1004
1183
  if (!entry.isComponentDefinition) return;
1005
- if (!components.has(entry.key) && !isJsxLike(context.sourceCode, body, hint)) return;
1184
+ if (!components.has(entry.key) && !isJsxLike(context, body, hint)) return;
1006
1185
  components.set(entry.key, entry);
1007
1186
  },
1008
1187
  ...collectDisplayName ? { [ast.SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node) {
1009
1188
  const { left, right } = node;
1010
1189
  if (left.type !== AST_NODE_TYPES.MemberExpression) return;
1011
- const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name : unit;
1190
+ const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name : null;
1012
1191
  const component = [...components.values()].findLast(({ name }) => name != null && name === componentName);
1013
1192
  if (component == null) return;
1014
1193
  component.displayName = right;
@@ -1027,7 +1206,7 @@ function useComponentCollector(context, options = {}) {
1027
1206
  entry.rets.push(node.argument);
1028
1207
  if (!entry.isComponentDefinition) return;
1029
1208
  const { argument } = node;
1030
- if (!components.has(entry.key) && !isJsxLike(context.sourceCode, argument, hint)) return;
1209
+ if (!components.has(entry.key) && !isJsxLike(context, argument, hint)) return;
1031
1210
  components.set(entry.key, entry);
1032
1211
  }
1033
1212
  }
@@ -1036,7 +1215,7 @@ function useComponentCollector(context, options = {}) {
1036
1215
 
1037
1216
  //#endregion
1038
1217
  //#region src/component/component-collector-legacy.ts
1039
- const idGen = new IdGenerator("class_component_");
1218
+ const idGen = new IdGenerator("class-component:");
1040
1219
  /**
1041
1220
  * Get a ctx and visitor object for the rule to collect class componentss
1042
1221
  * @param context The ESLint rule context
@@ -1052,18 +1231,18 @@ function useComponentCollectorLegacy(context) {
1052
1231
  if (!isClassComponent(node)) return;
1053
1232
  const id = ast.getClassId(node);
1054
1233
  const key = idGen.next();
1055
- const name = id == null ? unit : ast.getFullyQualifiedName(id, getText);
1234
+ const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
1056
1235
  const flag = isPureComponent(node) ? ComponentFlag.PureComponent : ComponentFlag.None;
1057
1236
  components.set(key, {
1058
1237
  id,
1059
1238
  key,
1060
1239
  kind: "class-component",
1061
1240
  name,
1062
- node,
1063
- displayName: unit,
1241
+ displayName: null,
1064
1242
  flag,
1065
1243
  hint: 0n,
1066
- methods: []
1244
+ methods: [],
1245
+ node
1067
1246
  });
1068
1247
  };
1069
1248
  return {
@@ -1074,179 +1253,6 @@ function useComponentCollectorLegacy(context) {
1074
1253
  }
1075
1254
  };
1076
1255
  }
1077
- /**
1078
- * Check whether the given node is a this.setState() call
1079
- * @param node The node to check
1080
- * @internal
1081
- */
1082
- function isThisSetState(node) {
1083
- const { callee } = node;
1084
- return callee.type === AST_NODE_TYPES$1.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES$1.Identifier && callee.property.name === "setState";
1085
- }
1086
- /**
1087
- * Check whether the given node is an assignment to this.state
1088
- * @param node The node to check
1089
- * @internal
1090
- */
1091
- function isAssignmentToThisState(node) {
1092
- const { left } = node;
1093
- return left.type === AST_NODE_TYPES$1.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
1094
- }
1095
-
1096
- //#endregion
1097
- //#region src/component/component-method-is.ts
1098
- /**
1099
- * Create a lifecycle method checker function
1100
- * @param methodName The lifecycle method name
1101
- * @param isStatic Whether the method is static
1102
- */
1103
- function createLifecycleChecker(methodName, isStatic = false) {
1104
- return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
1105
- }
1106
- const isRender = createLifecycleChecker("render");
1107
- const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
1108
- const isComponentDidMount = createLifecycleChecker("componentDidMount");
1109
- const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
1110
- const isComponentWillMount = createLifecycleChecker("componentWillMount");
1111
- const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
1112
- const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
1113
- const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
1114
- const isGetChildContext = createLifecycleChecker("getChildContext");
1115
- const isGetInitialState = createLifecycleChecker("getInitialState");
1116
- const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
1117
- const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
1118
- const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
1119
- const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
1120
- const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
1121
- const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
1122
- const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
1123
- const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
1124
-
1125
- //#endregion
1126
- //#region src/component/component-method-callback.ts
1127
- /**
1128
- * Check if the given node is a componentDidMount callback
1129
- * @param node The node to check
1130
- * @returns True if the node is a componentDidMount callback, false otherwise
1131
- */
1132
- function isComponentDidMountCallback(node) {
1133
- return ast.isFunction(node) && isComponentDidMount(node.parent) && node.parent.value === node;
1134
- }
1135
- /**
1136
- * Check if the given node is a componentWillUnmount callback
1137
- * @param node The node to check
1138
- * @returns True if the node is a componentWillUnmount callback, false otherwise
1139
- */
1140
- function isComponentWillUnmountCallback(node) {
1141
- return ast.isFunction(node) && isComponentWillUnmount(node.parent) && node.parent.value === node;
1142
- }
1143
-
1144
- //#endregion
1145
- //#region src/component/component-render-prop.ts
1146
- /**
1147
- * Unsafe check whether given node is a render function
1148
- * ```tsx
1149
- * const renderRow = () => <div />
1150
- * ` ^^^^^^^^^^^^`
1151
- * _ = <Component renderRow={() => <div />} />
1152
- * ` ^^^^^^^^^^^^^ `
1153
- * ```
1154
- * @param context The rule context
1155
- * @param node The AST node to check
1156
- * @returns `true` if node is a render function, `false` if not
1157
- */
1158
- function isRenderFunctionLoose(context, node) {
1159
- if (!ast.isFunction(node)) return false;
1160
- const id = ast.getFunctionId(node);
1161
- switch (true) {
1162
- case id?.type === AST_NODE_TYPES.Identifier: return id.name.startsWith("render");
1163
- case id?.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier: return id.property.name.startsWith("render");
1164
- case node.parent.type === AST_NODE_TYPES.JSXExpressionContainer && node.parent.parent.type === AST_NODE_TYPES.JSXAttribute && node.parent.parent.name.type === AST_NODE_TYPES.JSXIdentifier: return node.parent.parent.name.name.startsWith("render");
1165
- }
1166
- return false;
1167
- }
1168
- /**
1169
- * Unsafe check whether given JSXAttribute is a render prop
1170
- * ```tsx
1171
- * _ = <Component renderRow={() => <div />} />
1172
- * ` ^^^^^^^^^^^^^^^^^^^^^^^^^ `
1173
- * ```
1174
- * @param context The rule context
1175
- * @param node The AST node to check
1176
- * @returns `true` if node is a render prop, `false` if not
1177
- */
1178
- function isRenderPropLoose(context, node) {
1179
- if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) return false;
1180
- return node.name.name.startsWith("render") && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && isRenderFunctionLoose(context, node.value.expression);
1181
- }
1182
- /**
1183
- * Unsafe check whether given node is declared directly inside a render property
1184
- * ```tsx
1185
- * const rows = { render: () => <div /> }
1186
- * ` ^^^^^^^^^^^^^ `
1187
- * _ = <Component rows={ [{ render: () => <div /> }] } />
1188
- * ` ^^^^^^^^^^^^^ `
1189
- * ```
1190
- * @internal
1191
- * @param node The AST node to check
1192
- * @returns `true` if component is declared inside a render property, `false` if not
1193
- */
1194
- function isDirectValueOfRenderPropertyLoose(node) {
1195
- const matching = (node) => {
1196
- return node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render");
1197
- };
1198
- return matching(node) || node.parent != null && matching(node.parent);
1199
- }
1200
- /**
1201
- * Unsafe check whether given node is declared inside a render prop
1202
- * ```tsx
1203
- * _ = <Component renderRow={"node"} />
1204
- * ` ^^^^^^ `
1205
- * _ = <Component rows={ [{ render: "node" }] } />
1206
- * ` ^^^^^^ `
1207
- * ```
1208
- * @param node The AST node to check
1209
- * @returns `true` if component is declared inside a render prop, `false` if not
1210
- */
1211
- function isDeclaredInRenderPropLoose(node) {
1212
- if (isDirectValueOfRenderPropertyLoose(node)) return true;
1213
- const parent = ast.findParentNode(node, ast.is(AST_NODE_TYPES.JSXExpressionContainer))?.parent;
1214
- if (parent?.type !== AST_NODE_TYPES.JSXAttribute) return false;
1215
- return parent.name.type === AST_NODE_TYPES.JSXIdentifier && parent.name.name.startsWith("render");
1216
- }
1217
-
1218
- //#endregion
1219
- //#region src/hierarchy/find-enclosing-component-or-hook.ts
1220
- /**
1221
- * Find the enclosing React component or hook for a given AST node
1222
- * @param node The AST node to start the search from
1223
- * @param test Optional test function to customize component or hook identification
1224
- * @returns The enclosing component or hook node, or `null` if none is ASAST.
1225
- */
1226
- function findEnclosingComponentOrHook(node, test = (n, name) => {
1227
- if (name == null) return false;
1228
- return isComponentNameLoose(name) || isHookName(name);
1229
- }) {
1230
- const enclosingNode = ast.findParentNode(node, (n) => {
1231
- if (!ast.isFunction(n)) return false;
1232
- return test(n, match(ast.getFunctionId(n)).with({ type: AST_NODE_TYPES.Identifier }, (id) => id.name).with({
1233
- type: AST_NODE_TYPES.MemberExpression,
1234
- property: { type: AST_NODE_TYPES.Identifier }
1235
- }, (me) => me.property.name).otherwise(() => null));
1236
- });
1237
- return ast.isFunction(enclosingNode) ? enclosingNode : unit;
1238
- }
1239
-
1240
- //#endregion
1241
- //#region src/hierarchy/is-inside-component-or-hook.ts
1242
- /**
1243
- * Check if a given AST node is inside a React component or hook
1244
- * @param node The AST node to check
1245
- * @returns True if the node is inside a component or hook, false otherwise
1246
- */
1247
- function isInsideComponentOrHook(node) {
1248
- return findEnclosingComponentOrHook(node) != null;
1249
- }
1250
1256
 
1251
1257
  //#endregion
1252
1258
  //#region src/ref/ref-name.ts
@@ -1255,12 +1261,18 @@ function isInsideComponentOrHook(node) {
1255
1261
  * @param name The name to check
1256
1262
  * @returns True if the name is "ref" or ends with "Ref"
1257
1263
  */
1258
- function isRefName(name) {
1264
+ function isRefLikeName(name) {
1259
1265
  return name === "ref" || name.endsWith("Ref");
1260
1266
  }
1261
1267
 
1262
1268
  //#endregion
1263
- //#region src/ref/is-from-ref.ts
1269
+ //#region src/ref/ref-id.ts
1270
+ function isRefId(node) {
1271
+ return node.type === AST_NODE_TYPES.Identifier && isRefLikeName(node.name);
1272
+ }
1273
+
1274
+ //#endregion
1275
+ //#region src/ref/ref-init.ts
1264
1276
  /**
1265
1277
  * Check if the variable with the given name is initialized or derived from a ref
1266
1278
  * @param name The variable name
@@ -1274,20 +1286,20 @@ function isInitializedFromRef(name, initialScope) {
1274
1286
  * Get the init expression of a ref variable
1275
1287
  * @param name The variable name
1276
1288
  * @param initialScope The initial scope
1277
- * @returns The init expression node if the variable is derived from a ref, or undefined otherwise
1289
+ * @returns The init expression node if the variable is derived from a ref, or null otherwise
1278
1290
  */
1279
1291
  function getRefInit(name, initialScope) {
1280
- for (const { node } of findVariable(initialScope)(name)?.defs ?? []) {
1292
+ for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
1281
1293
  if (node.type !== AST_NODE_TYPES$1.VariableDeclarator) continue;
1282
1294
  const init = node.init;
1283
1295
  if (init == null) continue;
1284
1296
  switch (true) {
1285
- case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier && isRefName(init.object.name): return init;
1297
+ case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier && isRefLikeName(init.object.name): return init;
1286
1298
  case init.type === AST_NODE_TYPES$1.CallExpression && isUseRefCall(init): return init;
1287
1299
  }
1288
1300
  }
1289
- return unit;
1301
+ return null;
1290
1302
  }
1291
1303
 
1292
1304
  //#endregion
1293
- export { ComponentDetectionHint, ComponentFlag, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit, REACT_BUILTIN_HOOK_NAMES, findEnclosingComponentOrHook, findParentJsxAttribute, getComponentFlagFromInitPath, getFunctionComponentId, getJsxAttribute, getJsxAttributeName, getJsxConfigFromAnnotation, getJsxConfigFromContext, getJsxElementType, getRefInit, isAssignmentToThisState, isCaptureOwnerStack, isCaptureOwnerStackCall, isChildrenCount, isChildrenCountCall, isChildrenForEach, isChildrenForEachCall, isChildrenMap, isChildrenMapCall, isChildrenOnly, isChildrenOnlyCall, isChildrenToArray, isChildrenToArrayCall, isClassComponent, isCloneElement, isCloneElementCall, isComponentDefinition, isComponentDidCatch, isComponentDidMount, isComponentDidMountCallback, isComponentDidUpdate, isComponentName, isComponentNameLoose, isComponentWillMount, isComponentWillReceiveProps, isComponentWillUnmount, isComponentWillUnmountCallback, isComponentWillUpdate, isComponentWrapperCall, isComponentWrapperCallLoose, isComponentWrapperCallback, isComponentWrapperCallbackLoose, isCreateContext, isCreateContextCall, isCreateElement, isCreateElementCall, isCreateRef, isCreateRefCall, isDeclaredInRenderPropLoose, isDirectValueOfRenderPropertyLoose, isForwardRef, isForwardRefCall, isFunctionWithLooseComponentName, isGetChildContext, isGetDefaultProps, isGetDerivedStateFromError, isGetDerivedStateFromProps, isGetInitialState, isGetSnapshotBeforeUpdate, isHook, isHookCall, isHookCallWithName, isHookId, isHookName, isInitializedFromReact, isInitializedFromReactNative, isInitializedFromRef, isInsideComponentOrHook, isJsxFragmentElement, isJsxHostElement, isJsxLike, isJsxText, isLazy, isLazyCall, isMemo, isMemoCall, isPureComponent, isReactAPI, isReactAPICall, isRefName, isRender, isRenderFunctionLoose, isRenderMethodLike, isRenderPropLoose, isShouldComponentUpdate, isThisSetState, isUnsafeComponentWillMount, isUnsafeComponentWillReceiveProps, isUnsafeComponentWillUpdate, isUseActionStateCall, isUseCall, isUseCallbackCall, isUseContextCall, isUseDebugValueCall, isUseDeferredValueCall, isUseEffectCall, isUseEffectCleanupCallback, isUseEffectLikeCall, isUseEffectSetupCallback, isUseFormStatusCall, isUseIdCall, isUseImperativeHandleCall, isUseInsertionEffectCall, isUseLayoutEffectCall, isUseMemoCall, isUseOptimisticCall, isUseReducerCall, isUseRefCall, isUseStateCall, isUseStateLikeCall, isUseSyncExternalStoreCall, isUseTransitionCall, resolveJsxAttributeValue, stringifyJsx, useComponentCollector, useComponentCollectorLegacy, useHookCollector };
1305
+ export { ComponentDetectionHint, ComponentFlag, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit, JsxInspector, REACT_BUILTIN_HOOK_NAMES, findImportSource, getComponentFlagFromInitPath, getFunctionComponentId, getJsxConfigFromAnnotation, getJsxConfigFromContext, getRefInit, isAssignmentToThisState, isCaptureOwnerStack, isCaptureOwnerStackCall, isChildrenCount, isChildrenCountCall, isChildrenForEach, isChildrenForEachCall, isChildrenMap, isChildrenMapCall, isChildrenOnly, isChildrenOnlyCall, isChildrenToArray, isChildrenToArrayCall, isClassComponent, isCloneElement, isCloneElementCall, isComponentDefinition, isComponentDidCatch, isComponentDidMount, isComponentDidMountCallback, isComponentDidUpdate, isComponentName, isComponentNameLoose, isComponentWillMount, isComponentWillReceiveProps, isComponentWillUnmount, isComponentWillUnmountCallback, isComponentWillUpdate, isComponentWrapperCall, isComponentWrapperCallLoose, isComponentWrapperCallback, isComponentWrapperCallbackLoose, isCreateContext, isCreateContextCall, isCreateElement, isCreateElementCall, isCreateRef, isCreateRefCall, isForwardRef, isForwardRefCall, isFunctionWithLooseComponentName, isGetChildContext, isGetDefaultProps, isGetDerivedStateFromError, isGetDerivedStateFromProps, isGetInitialState, isGetSnapshotBeforeUpdate, isHook, isHookCall, isHookCallWithName, isHookId, isHookName, isInitializedFromReact, isInitializedFromReactNative, isInitializedFromRef, isJsxLike, isLazy, isLazyCall, isMemo, isMemoCall, isPureComponent, isReactAPI, isReactAPICall, isRefId, isRefLikeName, isRender, isRenderMethodCallback, isRenderMethodLike, isShouldComponentUpdate, isThisSetState, isUnsafeComponentWillMount, isUnsafeComponentWillReceiveProps, isUnsafeComponentWillUpdate, isUseActionStateCall, isUseCall, isUseCallbackCall, isUseContextCall, isUseDebugValueCall, isUseDeferredValueCall, isUseEffectCall, isUseEffectCleanupCallback, isUseEffectLikeCall, isUseEffectSetupCallback, isUseFormStatusCall, isUseIdCall, isUseImperativeHandleCall, isUseInsertionEffectCall, isUseLayoutEffectCall, isUseMemoCall, isUseOptimisticCall, isUseReducerCall, isUseRefCall, isUseStateCall, isUseStateLikeCall, isUseSyncExternalStoreCall, isUseTransitionCall, useComponentCollector, useComponentCollectorLegacy, useHookCollector };