oxlint-plugin-react-doctor 0.4.0-dev.ee980d9 → 0.4.0-dev.ee9ab33

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 (2) hide show
  1. package/dist/index.js +1920 -1639
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as path from "node:path";
2
- import { analyze } from "eslint-scope";
3
- import * as eslintVisitorKeys from "eslint-visitor-keys";
4
2
  import * as fs from "node:fs";
5
3
  import { parseSync } from "oxc-parser";
4
+ import { analyze } from "eslint-scope";
5
+ import * as eslintVisitorKeys from "eslint-visitor-keys";
6
6
  //#region src/plugin/utils/is-testlike-filename.ts
7
7
  const NON_PRODUCTION_PATH_SEGMENTS = [
8
8
  "/test/",
@@ -13367,1572 +13367,2071 @@ const nextjsNoSideEffectInGetHandler = defineRule({
13367
13367
  }
13368
13368
  });
13369
13369
  //#endregion
13370
- //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
13371
- const fileMentionsSuspense = (programNode) => {
13372
- let didSee = false;
13373
- walkAst(programNode, (child) => {
13374
- if (didSee) return false;
13375
- if (isNodeOfType(child, "JSXOpeningElement") && isNodeOfType(child.name, "JSXIdentifier") && child.name.name === "Suspense") {
13376
- didSee = true;
13377
- return false;
13370
+ //#region src/plugin/utils/find-exported-function-body.ts
13371
+ const isFunctionLike = (node) => {
13372
+ if (!node) return false;
13373
+ return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
13374
+ };
13375
+ const findExportedFunctionBody = (programRoot, exportedName) => {
13376
+ if (!isNodeOfType(programRoot, "Program")) return null;
13377
+ const localBindings = /* @__PURE__ */ new Map();
13378
+ const namedExports = /* @__PURE__ */ new Map();
13379
+ let defaultExport = null;
13380
+ let defaultExportIdentifierName = null;
13381
+ const recordVariableDeclaration = (declaration) => {
13382
+ for (const declarator of declaration.declarations ?? []) {
13383
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
13384
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
13385
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
13386
+ if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
13378
13387
  }
13379
- if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
13380
- if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
13381
- didSee = true;
13382
- return false;
13388
+ };
13389
+ for (const statement of programRoot.body ?? []) {
13390
+ if (isNodeOfType(statement, "VariableDeclaration")) {
13391
+ recordVariableDeclaration(statement);
13392
+ continue;
13393
+ }
13394
+ if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
13395
+ localBindings.set(statement.id.name, statement);
13396
+ continue;
13397
+ }
13398
+ if (isNodeOfType(statement, "ExportNamedDeclaration")) {
13399
+ const declaration = statement.declaration;
13400
+ if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
13401
+ recordVariableDeclaration(declaration);
13402
+ for (const declarator of declaration.declarations ?? []) {
13403
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
13404
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
13405
+ namedExports.set(declarator.id.name, declarator.id.name);
13406
+ }
13407
+ } else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
13408
+ localBindings.set(declaration.id.name, declaration);
13409
+ namedExports.set(declaration.id.name, declaration.id.name);
13410
+ }
13411
+ for (const specifier of statement.specifiers ?? []) {
13412
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
13413
+ const local = specifier.local;
13414
+ const exported = specifier.exported;
13415
+ if (!isNodeOfType(local, "Identifier")) continue;
13416
+ const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
13417
+ if (!exportedNameSpec) continue;
13418
+ namedExports.set(exportedNameSpec, local.name);
13383
13419
  }
13420
+ continue;
13384
13421
  }
13385
- });
13386
- return didSee;
13387
- };
13388
- const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
13389
- id: "nextjs-no-use-search-params-without-suspense",
13390
- title: "useSearchParams without Suspense",
13391
- tags: ["test-noise"],
13392
- requires: ["nextjs"],
13393
- severity: "warn",
13394
- recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
13395
- create: (context) => {
13396
- let hasSuspenseInFile = false;
13397
- return {
13398
- Program(programNode) {
13399
- hasSuspenseInFile = fileMentionsSuspense(programNode);
13400
- },
13401
- CallExpression(node) {
13402
- if (hasSuspenseInFile) return;
13403
- if (!isHookCall$1(node, "useSearchParams")) return;
13404
- context.report({
13405
- node,
13406
- message: "useSearchParams() without a <Suspense> boundary forces the whole page into client-side rendering."
13407
- });
13422
+ if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
13423
+ const declaration = statement.declaration;
13424
+ if (!declaration) continue;
13425
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
13426
+ localBindings.set(declaration.id.name, declaration);
13427
+ defaultExport = declaration;
13428
+ continue;
13429
+ }
13430
+ if (isFunctionLike(declaration)) {
13431
+ defaultExport = declaration;
13432
+ continue;
13433
+ }
13434
+ if (isNodeOfType(declaration, "Identifier")) {
13435
+ defaultExportIdentifierName = declaration.name;
13436
+ continue;
13408
13437
  }
13409
- };
13410
- }
13411
- });
13412
- //#endregion
13413
- //#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
13414
- const nextjsNoVercelOgImport = defineRule({
13415
- id: "nextjs-no-vercel-og-import",
13416
- title: "@vercel/og import instead of next/og",
13417
- tags: ["test-noise"],
13418
- requires: ["nextjs"],
13419
- severity: "warn",
13420
- recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
13421
- create: (context) => ({ ImportDeclaration(node) {
13422
- if (node.source?.value !== "@vercel/og") return;
13423
- context.report({
13424
- node,
13425
- message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
13426
- });
13427
- } })
13428
- });
13429
- //#endregion
13430
- //#region src/plugin/rules/a11y/no-access-key.ts
13431
- const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
13432
- const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
13433
- const noAccessKey = defineRule({
13434
- id: "no-access-key",
13435
- title: "accessKey attribute used",
13436
- tags: ["react-jsx-only"],
13437
- severity: "warn",
13438
- recommendation: "Do not use `accessKey`. It conflicts with assistive tech shortcuts.",
13439
- category: "Accessibility",
13440
- create: (context) => ({ JSXOpeningElement(node) {
13441
- const accessKey = hasJsxPropIgnoreCase(node.attributes, "accessKey");
13442
- if (!accessKey) return;
13443
- const attributeValue = accessKey.value;
13444
- if (!attributeValue) return;
13445
- if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
13446
- context.report({
13447
- node: accessKey,
13448
- message: MESSAGE$31
13449
- });
13450
- return;
13451
13438
  }
13452
- if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
13453
- const expression = attributeValue.expression;
13454
- if (!expression || expression.type === "JSXEmptyExpression") return;
13455
- if (isUndefinedIdentifier(expression)) return;
13456
- context.report({
13457
- node: accessKey,
13458
- message: MESSAGE$31
13459
- });
13439
+ }
13440
+ if (exportedName === "default") {
13441
+ if (defaultExport) return defaultExport;
13442
+ if (defaultExportIdentifierName) {
13443
+ const binding = localBindings.get(defaultExportIdentifierName);
13444
+ if (binding) return binding;
13460
13445
  }
13461
- } })
13462
- });
13463
- //#endregion
13464
- //#region src/plugin/rules/state-and-effects/utils/effect/constants.ts
13465
- const TYPESCRIPT_VISITOR_KEYS = {
13466
- TSAsExpression: ["expression", "typeAnnotation"],
13467
- TSNonNullExpression: ["expression"],
13468
- TSSatisfiesExpression: ["expression", "typeAnnotation"],
13469
- TSTypeAssertion: ["typeAnnotation", "expression"],
13470
- TSInstantiationExpression: ["expression", "typeArguments"]
13446
+ }
13447
+ const localName = namedExports.get(exportedName);
13448
+ if (!localName) return null;
13449
+ return localBindings.get(localName) ?? null;
13471
13450
  };
13472
- const VISITOR_KEYS = {
13473
- ...eslintVisitorKeys.KEYS,
13474
- ...TYPESCRIPT_VISITOR_KEYS
13451
+ const resolveImportedExportName = (importSpecifier) => {
13452
+ if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
13453
+ const imported = importSpecifier.imported;
13454
+ if (isNodeOfType(imported, "Identifier")) return imported.name;
13455
+ if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
13456
+ return null;
13457
+ }
13458
+ if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
13459
+ return null;
13475
13460
  };
13476
- //#endregion
13477
- //#region src/plugin/rules/state-and-effects/utils/effect/get-program-analysis.ts
13478
- const programToAnalysis = /* @__PURE__ */ new WeakMap();
13479
- const stripAndRecordParents = (root) => {
13480
- const restorations = [];
13481
- const seen = /* @__PURE__ */ new WeakSet();
13482
- const visit = (value) => {
13483
- if (!value || typeof value !== "object") return;
13484
- if (seen.has(value)) return;
13485
- seen.add(value);
13486
- if (Array.isArray(value)) {
13487
- for (const item of value) visit(item);
13488
- return;
13461
+ const findReExportSourcesForName = (programRoot, exportedName) => {
13462
+ if (!isNodeOfType(programRoot, "Program")) return [];
13463
+ const exportAllSources = [];
13464
+ for (const statement of programRoot.body ?? []) {
13465
+ if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
13466
+ const sourceValue = statement.source.value;
13467
+ if (typeof sourceValue !== "string") continue;
13468
+ for (const specifier of statement.specifiers ?? []) {
13469
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
13470
+ const exported = specifier.exported;
13471
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
13472
+ }
13489
13473
  }
13490
- const record = value;
13491
- if (!("type" in record)) return;
13492
- if ("parent" in record) {
13493
- restorations.push({
13494
- node: record,
13495
- originalParent: record.parent
13496
- });
13497
- record.parent = null;
13474
+ if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
13475
+ const sourceValue = statement.source.value;
13476
+ if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
13498
13477
  }
13499
- for (const key of Object.keys(record)) {
13478
+ }
13479
+ return exportAllSources;
13480
+ };
13481
+ //#endregion
13482
+ //#region src/plugin/utils/attach-parent-references.ts
13483
+ const attachParentReferences = (root) => {
13484
+ const visit = (node, parent) => {
13485
+ const writableNode = node;
13486
+ writableNode.parent = parent;
13487
+ const nodeRecord = node;
13488
+ for (const key of Object.keys(nodeRecord)) {
13500
13489
  if (key === "parent") continue;
13501
- visit(record[key]);
13490
+ const child = nodeRecord[key];
13491
+ if (Array.isArray(child)) {
13492
+ for (const item of child) if (isAstNode(item)) visit(item, node);
13493
+ } else if (isAstNode(child)) visit(child, node);
13502
13494
  }
13503
13495
  };
13504
- visit(root);
13505
- return restorations;
13496
+ visit(root, null);
13506
13497
  };
13507
- const restoreParents = (restorations) => {
13508
- for (const restoration of restorations) restoration.node.parent = restoration.originalParent;
13498
+ //#endregion
13499
+ //#region src/plugin/utils/parse-source-file.ts
13500
+ const FILENAME_TO_LANG = {
13501
+ ".ts": "ts",
13502
+ ".tsx": "tsx",
13503
+ ".js": "js",
13504
+ ".jsx": "jsx",
13505
+ ".mjs": "js",
13506
+ ".cjs": "js",
13507
+ ".mts": "ts",
13508
+ ".cts": "ts"
13509
13509
  };
13510
- const getProgramAnalysis = (anyNode) => {
13511
- const programNode = findProgramRoot(anyNode);
13512
- if (!programNode) return null;
13513
- const cached = programToAnalysis.get(programNode);
13514
- if (cached) return cached;
13515
- const restorations = stripAndRecordParents(programNode);
13516
- let scopeManager;
13517
- try {
13518
- scopeManager = analyze(programNode, {
13519
- ecmaVersion: 2024,
13520
- sourceType: "module",
13521
- childVisitorKeys: VISITOR_KEYS,
13522
- fallback: "iteration"
13510
+ const resolveLang = (filename) => {
13511
+ return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
13512
+ };
13513
+ const parseCache = /* @__PURE__ */ new Map();
13514
+ const parseSourceFile = (absoluteFilePath) => {
13515
+ let fileStat;
13516
+ try {
13517
+ fileStat = fs.statSync(absoluteFilePath);
13518
+ } catch {
13519
+ return null;
13520
+ }
13521
+ if (!fileStat.isFile()) return null;
13522
+ if (fileStat.size > 2e6) return null;
13523
+ const cached = parseCache.get(absoluteFilePath);
13524
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
13525
+ if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
13526
+ parseCache.set(absoluteFilePath, {
13527
+ mtimeMs: fileStat.mtimeMs,
13528
+ size: fileStat.size,
13529
+ program: null
13523
13530
  });
13524
- } finally {
13525
- restoreParents(restorations);
13531
+ return null;
13526
13532
  }
13527
- const analysis = {
13528
- programNode,
13529
- scopeManager
13530
- };
13531
- programToAnalysis.set(programNode, analysis);
13532
- return analysis;
13533
- };
13534
- const getScopeForNode = (node, manager) => {
13535
- if (!node.range) return null;
13536
- let bestScope = null;
13537
- let bestSize = Infinity;
13538
- for (const scope of manager.scopes) {
13539
- const block = scope.block;
13540
- if (!block?.range) continue;
13541
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
13542
- const size = block.range[1] - block.range[0];
13543
- if (size <= bestSize) {
13544
- bestSize = size;
13545
- bestScope = scope;
13533
+ let sourceText;
13534
+ try {
13535
+ sourceText = fs.readFileSync(absoluteFilePath, "utf8");
13536
+ } catch {
13537
+ parseCache.set(absoluteFilePath, {
13538
+ mtimeMs: fileStat.mtimeMs,
13539
+ size: fileStat.size,
13540
+ program: null
13541
+ });
13542
+ return null;
13543
+ }
13544
+ let parsedProgram = null;
13545
+ try {
13546
+ const result = parseSync(absoluteFilePath, sourceText, {
13547
+ astType: "ts",
13548
+ lang: resolveLang(absoluteFilePath)
13549
+ });
13550
+ if (!result.errors.some((parseError) => parseError.severity === "Error")) {
13551
+ parsedProgram = result.program;
13552
+ attachParentReferences(parsedProgram);
13546
13553
  }
13554
+ } catch {
13555
+ parsedProgram = null;
13547
13556
  }
13548
- return bestScope;
13557
+ parseCache.set(absoluteFilePath, {
13558
+ mtimeMs: fileStat.mtimeMs,
13559
+ size: fileStat.size,
13560
+ program: parsedProgram
13561
+ });
13562
+ return parsedProgram;
13549
13563
  };
13550
13564
  //#endregion
13551
- //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
13552
- const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
13553
- const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
13554
- if (visited.has(ref)) return;
13555
- const result = visit(ref);
13556
- visited.add(ref);
13557
- if (result === false) return;
13558
- const defs = ref.resolved?.defs ?? [];
13559
- for (const def of defs) {
13560
- if (def.type === "ImportBinding") continue;
13561
- if (def.type === "Parameter") continue;
13562
- const defNode = def.node;
13563
- const next = defNode.init ?? defNode.body;
13564
- if (!next) continue;
13565
- for (const innerRef of getDownstreamRefs(analysis, next)) ascend(analysis, innerRef, visit, visited);
13566
- }
13565
+ //#region src/plugin/utils/parse-export-specifiers.ts
13566
+ const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
13567
+ const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
13568
+ const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
13569
+ const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
13570
+ const localName = getSpecifierName(rawLocalName ?? "");
13571
+ return {
13572
+ localName,
13573
+ exportedName: getSpecifierName(rawExportedName ?? localName),
13574
+ isTypeOnly
13575
+ };
13576
+ });
13577
+ //#endregion
13578
+ //#region src/plugin/utils/strip-js-comments.ts
13579
+ const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
13580
+ const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
13581
+ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
13582
+ //#endregion
13583
+ //#region src/plugin/utils/does-module-export-name.ts
13584
+ const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
13585
+ const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
13586
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13587
+ const doesSourceTextExportName = (sourceText, exportedName) => {
13588
+ const strippedSource = stripJsComments(sourceText);
13589
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
13590
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
13591
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
13592
+ return false;
13567
13593
  };
13568
- const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
13569
- if (visited.has(node)) return;
13570
- visit(node);
13571
- visited.add(node);
13572
- const keys = getChildKeys(node);
13573
- const record = node;
13574
- for (const key of keys) {
13575
- const child = record[key];
13576
- if (!child) continue;
13577
- if (Array.isArray(child)) {
13578
- for (const item of child) if (item && isAstNode(item)) descend(item, visit, visited);
13579
- } else if (isAstNode(child)) descend(child, visit, visited);
13594
+ const doesModuleExportName = (filePath, exportedName) => {
13595
+ try {
13596
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
13597
+ } catch {
13598
+ return false;
13580
13599
  }
13581
13600
  };
13582
- const getUpstreamRefs = (analysis, ref) => {
13583
- const refs = [];
13584
- ascend(analysis, ref, (upRef) => {
13585
- refs.push(upRef);
13601
+ //#endregion
13602
+ //#region src/plugin/utils/is-barrel-index-module.ts
13603
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
13604
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13605
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13606
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13607
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
13608
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
13609
+ const createNonBarrelInfo = () => ({
13610
+ isBarrel: false,
13611
+ exportsByName: /* @__PURE__ */ new Map(),
13612
+ starExportSources: []
13613
+ });
13614
+ const addImportedBinding = (importedBindings, binding) => {
13615
+ importedBindings.set(binding.localName, {
13616
+ ...binding,
13617
+ didExport: false
13586
13618
  });
13587
- return refs;
13588
13619
  };
13589
- const findDownstreamNodes = (topNode, type) => {
13590
- const nodes = [];
13591
- descend(topNode, (node) => {
13592
- if (node.type === type) nodes.push(node);
13620
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
13621
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
13622
+ localName: specifier.exportedName,
13623
+ importedName: specifier.localName,
13624
+ source,
13625
+ isTypeOnly: specifier.isTypeOnly
13593
13626
  });
13594
- return nodes;
13595
- };
13596
- const getRef = (analysis, identifier) => {
13597
- const scope = getScopeForNode(identifier, analysis.scopeManager);
13598
- if (!scope) return null;
13599
- for (const reference of scope.references) if (reference.identifier === identifier) return reference;
13600
- return null;
13601
- };
13602
- const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
13603
- const getDownstreamRefs = (analysis, node) => {
13604
- let perNode = downstreamRefsCache.get(analysis);
13605
- if (!perNode) {
13606
- perNode = /* @__PURE__ */ new WeakMap();
13607
- downstreamRefsCache.set(analysis, perNode);
13608
- }
13609
- const cached = perNode.get(node);
13610
- if (cached) return cached;
13611
- const refs = [];
13612
- for (const identifier of findDownstreamNodes(node, "Identifier")) {
13613
- const ref = getRef(analysis, identifier);
13614
- if (ref) refs.push(ref);
13615
- }
13616
- perNode.set(node, refs);
13617
- return refs;
13618
- };
13619
- const getCallExpr = (ref, current = ref.identifier.parent) => {
13620
- if (!current) return null;
13621
- if (isNodeOfType(current, "CallExpression")) {
13622
- let node = ref.identifier;
13623
- let parent = node.parent;
13624
- while (parent && isNodeOfType(parent, "MemberExpression")) {
13625
- node = parent;
13626
- parent = node.parent;
13627
- }
13628
- if (current.callee === node) return current;
13629
- }
13630
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
13631
- return null;
13632
- };
13633
- const getArgsUpstreamRefs = (analysis, ref) => {
13634
- const result = [];
13635
- for (const upRef of getUpstreamRefs(analysis, ref)) {
13636
- const callExpr = getCallExpr(upRef);
13637
- if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
13638
- for (const argument of callExpr.arguments ?? []) for (const argRef of getDownstreamRefs(analysis, argument)) for (const innerRef of getUpstreamRefs(analysis, argRef)) result.push(innerRef);
13639
- }
13640
- return result;
13641
- };
13642
- const isSynchronous = (node, within) => {
13643
- if (!node) return false;
13644
- if (node === within) return true;
13645
- if (node.async === true) return false;
13646
- if (isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "void" || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) return false;
13647
- return isSynchronous(node.parent, within);
13648
13627
  };
13649
- const isEventualCallTo = (analysis, ref, predicate) => {
13650
- const callExprRefs = [];
13651
- ascend(analysis, ref, (upRef) => {
13652
- if (getCallExpr(upRef)) callExprRefs.push(upRef);
13653
- else return false;
13628
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
13629
+ const trimmedImportClause = importClause.trim();
13630
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
13631
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
13632
+ localName: namespaceMatch[1],
13633
+ importedName: "*",
13634
+ source,
13635
+ isTypeOnly: declarationIsTypeOnly
13636
+ });
13637
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
13638
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
13639
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
13640
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
13641
+ localName: defaultImportName,
13642
+ importedName: "default",
13643
+ source,
13644
+ isTypeOnly: declarationIsTypeOnly
13654
13645
  });
13655
- return callExprRefs.some(predicate);
13656
13646
  };
13657
- //#endregion
13658
- //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
13659
- const getOuterScopeContaining = (analysis, node) => {
13660
- if (!node.range) return null;
13661
- let best = null;
13662
- let bestSize = Infinity;
13663
- for (const scope of analysis.scopeManager.scopes) {
13664
- const block = scope.block;
13665
- if (!block?.range) continue;
13666
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
13667
- const size = block.range[1] - block.range[0];
13668
- if (size <= bestSize) {
13669
- bestSize = size;
13670
- best = scope;
13671
- }
13672
- }
13673
- return best;
13647
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
13648
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
13649
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
13650
+ return "";
13651
+ });
13652
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
13653
+ const isTypeOnly = Boolean(typeKeyword);
13654
+ if (namespaceExportName) {
13655
+ exportsByName.set(namespaceExportName, {
13656
+ exportedName: namespaceExportName,
13657
+ importedName: "*",
13658
+ source,
13659
+ isTypeOnly
13660
+ });
13661
+ return "";
13662
+ }
13663
+ if (specifiersText) {
13664
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
13665
+ exportedName: specifier.exportedName,
13666
+ importedName: specifier.localName,
13667
+ source,
13668
+ isTypeOnly: specifier.isTypeOnly
13669
+ });
13670
+ return "";
13671
+ }
13672
+ starExportSources.push(source);
13673
+ return "";
13674
+ });
13675
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
13676
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
13677
+ const importedBinding = importedBindings.get(specifier.localName);
13678
+ if (!importedBinding) return _match;
13679
+ importedBinding.didExport = true;
13680
+ exportsByName.set(specifier.exportedName, {
13681
+ exportedName: specifier.exportedName,
13682
+ importedName: importedBinding.importedName,
13683
+ source: importedBinding.source,
13684
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
13685
+ });
13686
+ }
13687
+ return "";
13688
+ });
13689
+ return withoutKnownDeclarations;
13674
13690
  };
13675
- const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
13676
- const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
13677
- const isReactFunctionalComponent = (node) => {
13678
- if (!node) return false;
13679
- if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id && startsWithUppercase(node.id.name));
13680
- if (isNodeOfType(node, "VariableDeclarator")) {
13681
- if (!isNodeOfType(node.id, "Identifier")) return false;
13682
- if (!startsWithUppercase(node.id.name)) return false;
13683
- const init = node.init;
13684
- if (!init) return false;
13685
- return isNodeOfType(init, "ArrowFunctionExpression") || isNodeOfType(init, "CallExpression");
13686
- }
13691
+ const hasUnexportedRuntimeImport = (importedBindings) => {
13692
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
13687
13693
  return false;
13688
13694
  };
13689
- const isReactFunctionalHOC = (analysis, node) => {
13690
- if (!isReactFunctionalComponent(node)) return false;
13691
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13692
- const init = node.init;
13693
- if (!init) return false;
13694
- const isWrappedInline = () => {
13695
- if (!isNodeOfType(init, "CallExpression")) return false;
13696
- if (!isNodeOfType(init.callee, "Identifier")) return false;
13697
- if (KNOWN_PURE_HOC_NAMES.has(init.callee.name)) return false;
13698
- const firstArg = init.arguments?.[0];
13699
- if (!firstArg) return false;
13700
- return isNodeOfType(firstArg, "ArrowFunctionExpression") || isNodeOfType(firstArg, "FunctionExpression");
13701
- };
13702
- const isWrappedSeparately = () => {
13703
- if (!isNodeOfType(node.id, "Identifier")) return false;
13704
- const bindingName = node.id.name;
13705
- const containingScope = getOuterScopeContaining(analysis, node);
13706
- if (!containingScope) return false;
13707
- const variable = containingScope.variables.find((v) => v.name === bindingName);
13708
- if (!variable) return false;
13709
- for (const reference of variable.references) {
13710
- const parent = reference.identifier.parent;
13711
- if (!parent || !isNodeOfType(parent, "CallExpression")) continue;
13712
- const args = parent.arguments ?? [];
13713
- const refId = reference.identifier;
13714
- if (!args.includes(refId)) continue;
13715
- const callee = parent.callee;
13716
- const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "CallExpression") && isNodeOfType(callee.callee, "Identifier") ? callee.callee.name : null;
13717
- if (calleeName != null && !KNOWN_PURE_HOC_NAMES.has(calleeName)) return true;
13718
- }
13719
- return false;
13695
+ const classifyBarrelModule = (sourceText) => {
13696
+ const strippedSource = stripJsComments(sourceText).trim();
13697
+ if (!strippedSource) return createNonBarrelInfo();
13698
+ const importedBindings = /* @__PURE__ */ new Map();
13699
+ const exportsByName = /* @__PURE__ */ new Map();
13700
+ const starExportSources = [];
13701
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
13702
+ return {
13703
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
13704
+ exportsByName,
13705
+ starExportSources
13720
13706
  };
13721
- return isWrappedInline() || isWrappedSeparately();
13722
13707
  };
13723
- const isCustomHook = (node) => {
13724
- if (!node) return false;
13725
- if (isNodeOfType(node, "FunctionDeclaration")) {
13726
- const name = node.id?.name;
13727
- if (!name) return false;
13728
- return name.startsWith("use") && name.length > 3 && name[3] >= "A" && name[3] <= "Z";
13708
+ const getBarrelIndexModuleInfo = (filePath) => {
13709
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
13710
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
13711
+ if (cachedResult !== void 0) return cachedResult;
13712
+ let moduleInfo = createNonBarrelInfo();
13713
+ try {
13714
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
13715
+ } catch {
13716
+ moduleInfo = createNonBarrelInfo();
13729
13717
  }
13730
- if (isNodeOfType(node, "VariableDeclarator")) {
13731
- if (!isNodeOfType(node.id, "Identifier")) return false;
13732
- const name = node.id.name;
13733
- const init = node.init;
13734
- if (!init) return false;
13735
- if (!isNodeOfType(init, "ArrowFunctionExpression") && !isNodeOfType(init, "FunctionExpression")) return false;
13736
- return name.startsWith("use") && name.length > 3 && name[3] >= "A" && name[3] <= "Z";
13718
+ barrelIndexModuleInfoCache.set(filePath, moduleInfo);
13719
+ return moduleInfo;
13720
+ };
13721
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
13722
+ //#endregion
13723
+ //#region src/plugin/utils/resolve-relative-import-path.ts
13724
+ const MODULE_FILE_EXTENSIONS = [
13725
+ ".ts",
13726
+ ".tsx",
13727
+ ".js",
13728
+ ".jsx",
13729
+ ".mjs",
13730
+ ".cjs",
13731
+ ".mts",
13732
+ ".cts"
13733
+ ];
13734
+ const PACKAGE_EXPORT_CONDITIONS = [
13735
+ "import",
13736
+ "default",
13737
+ "module",
13738
+ "browser",
13739
+ "require"
13740
+ ];
13741
+ const PACKAGE_ENTRY_FIELDS = [
13742
+ "module",
13743
+ "main",
13744
+ "browser"
13745
+ ];
13746
+ const getExistingFilePath = (filePath) => {
13747
+ try {
13748
+ return fs.statSync(filePath).isFile() ? filePath : null;
13749
+ } catch {
13750
+ return null;
13737
13751
  }
13738
- return false;
13739
13752
  };
13740
- const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved?.defs.some((def) => {
13741
- if (def.type !== "ImportBinding") return false;
13742
- const declarationNode = def.node;
13743
- if (!isNodeOfType(declarationNode, "ImportSpecifier")) return false;
13744
- const imported = declarationNode.imported;
13745
- if (!isNodeOfType(imported, "Identifier")) return false;
13746
- if (imported.name !== importedName) return false;
13747
- const importDeclaration = declarationNode.parent;
13748
- return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
13749
- }));
13750
- const isHookCallee = (analysis, node, hookName) => {
13751
- if (!node) return false;
13752
- if (isNodeOfType(node, "Identifier")) {
13753
- if (node.name === hookName) return true;
13754
- if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
13755
- const parent = node.parent;
13756
- if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
13757
- return false;
13753
+ const getExistingDirectoryPath = (directoryPath) => {
13754
+ try {
13755
+ return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
13756
+ } catch {
13757
+ return null;
13758
13758
  }
13759
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
13760
- return false;
13761
13759
  };
13762
- const isUseEffect = (node) => {
13763
- if (!node || !isNodeOfType(node, "CallExpression")) return false;
13764
- const callee = node.callee;
13765
- if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
13766
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
13767
- return false;
13760
+ const getModuleFilePathCandidates = (modulePath) => {
13761
+ const extension = path.extname(modulePath);
13762
+ if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
13763
+ const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
13764
+ if (extension === ".js") return [
13765
+ modulePath,
13766
+ `${modulePathWithoutExtension}.ts`,
13767
+ `${modulePathWithoutExtension}.tsx`,
13768
+ `${modulePathWithoutExtension}.jsx`
13769
+ ];
13770
+ if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
13771
+ if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
13772
+ if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
13773
+ return [modulePath];
13768
13774
  };
13769
- const getEffectFn = (analysis, node) => {
13770
- if (!isNodeOfType(node, "CallExpression")) return null;
13771
- const fn = node.arguments?.[0];
13772
- if (!fn) return null;
13773
- if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
13774
- if (isNodeOfType(fn, "Identifier")) {
13775
- const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
13776
- if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
13777
- if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13778
- const initializer = definitionNode.init;
13779
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
13775
+ const isObjectRecord$1 = (value) => typeof value === "object" && value !== null;
13776
+ const getConditionalExportEntry = (exportEntry) => {
13777
+ if (typeof exportEntry === "string") return exportEntry;
13778
+ if (Array.isArray(exportEntry)) {
13779
+ for (const fallbackEntry of exportEntry) {
13780
+ const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
13781
+ if (resolvedFallbackEntry) return resolvedFallbackEntry;
13780
13782
  }
13783
+ return null;
13784
+ }
13785
+ if (!isObjectRecord$1(exportEntry)) return null;
13786
+ for (const condition of PACKAGE_EXPORT_CONDITIONS) {
13787
+ const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
13788
+ if (nestedEntry) return nestedEntry;
13781
13789
  }
13782
13790
  return null;
13783
13791
  };
13784
- const getEffectFnRefs = (analysis, node) => {
13785
- const fn = getEffectFn(analysis, node);
13786
- if (!fn) return null;
13787
- return getDownstreamRefs(analysis, fn);
13792
+ const getPackageExportEntry = (packageJson) => {
13793
+ const exportsField = packageJson.exports;
13794
+ if (!exportsField) return null;
13795
+ const directExportEntry = getConditionalExportEntry(exportsField);
13796
+ if (directExportEntry) return directExportEntry;
13797
+ if (!isObjectRecord$1(exportsField)) return null;
13798
+ return getConditionalExportEntry(exportsField["."]);
13788
13799
  };
13789
- const getEffectDepsRefs = (analysis, node) => {
13790
- if (!isNodeOfType(node, "CallExpression")) return null;
13791
- const deps = node.arguments?.[1];
13792
- if (!deps || !isNodeOfType(deps, "ArrayExpression")) return null;
13793
- return getDownstreamRefs(analysis, deps);
13800
+ const resolveModulePathWithIndexFallback = (modulePath) => {
13801
+ const filePath = resolveModuleFilePath(modulePath);
13802
+ if (filePath) return filePath;
13803
+ return resolveModuleFilePath(path.join(modulePath, "index"));
13794
13804
  };
13795
- const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
13796
- const node = def.node;
13797
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13798
- if (!isNodeOfType(node.init, "CallExpression")) return false;
13799
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
13800
- if (!isNodeOfType(node.id, "ArrayPattern")) return false;
13801
- const elements = node.id.elements ?? [];
13802
- if (elements.length !== 1 && elements.length !== 2) return false;
13803
- const first = elements[0];
13804
- return Boolean(first && isNodeOfType(first, "Identifier") && first.name === ref.identifier.name);
13805
- }));
13806
- const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
13807
- const node = def.node;
13808
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13809
- if (!isNodeOfType(node.init, "CallExpression")) return false;
13810
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
13811
- if (!isNodeOfType(node.id, "ArrayPattern")) return false;
13812
- const elements = node.id.elements ?? [];
13813
- if (elements.length !== 2) return false;
13814
- const second = elements[1];
13815
- return Boolean(second && isNodeOfType(second, "Identifier") && second.name === ref.identifier.name);
13816
- }));
13817
- const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
13818
- if (def.type !== "Parameter") return false;
13819
- const defNode = def.node;
13820
- let declaringNode = defNode;
13821
- if (isNodeOfType(defNode, "ArrowFunctionExpression")) {
13822
- const parent = defNode.parent;
13823
- if (parent && isNodeOfType(parent, "CallExpression")) declaringNode = parent.parent;
13824
- else declaringNode = parent;
13805
+ const resolvePackageDirectoryEntry = (directoryPath) => {
13806
+ const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
13807
+ if (!existingDirectoryPath) return null;
13808
+ const packageJsonPath = path.join(existingDirectoryPath, "package.json");
13809
+ try {
13810
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
13811
+ const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
13812
+ if (!packageEntry) return null;
13813
+ return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
13814
+ } catch {
13815
+ return null;
13825
13816
  }
13826
- if (!declaringNode) return false;
13827
- return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
13828
- }));
13829
- const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
13830
- const isPropAlias = (analysis, ref) => {
13831
- if (isProp(analysis, ref)) return true;
13832
- return Boolean(ref.resolved?.defs.some((def) => {
13833
- const node = def.node;
13834
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13835
- const initializer = node.init;
13836
- if (!initializer) return false;
13837
- if (!isNodeOfType(node.id, "ObjectPattern") && !isIdentifierOrMemberExpression(initializer)) return false;
13838
- return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
13839
- }));
13840
13817
  };
13841
- const isConstant = (ref) => Boolean((ref.resolved?.defs ?? []).some((def) => {
13842
- const node = def.node;
13843
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13844
- const init = node.init;
13845
- if (!init) return false;
13846
- return isNodeOfType(init, "Literal") || isNodeOfType(init, "TemplateLiteral") || isNodeOfType(init, "ArrayExpression") || isNodeOfType(init, "ObjectExpression");
13847
- }));
13848
- const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
13849
- const node = def.node;
13850
- if (!isNodeOfType(node, "VariableDeclarator")) return false;
13851
- if (!isNodeOfType(node.init, "CallExpression")) return false;
13852
- return isHookCallee(analysis, node.init.callee, "useRef");
13853
- }));
13854
- const isRefCurrent = (ref) => {
13855
- const parent = ref.identifier.parent;
13856
- if (!parent || !isNodeOfType(parent, "MemberExpression")) return false;
13857
- if (!isNodeOfType(parent.property, "Identifier")) return false;
13858
- return parent.property.name === "current";
13818
+ const resolveModuleFilePath = (modulePath) => {
13819
+ const exactFilePath = getExistingFilePath(modulePath);
13820
+ if (exactFilePath) return exactFilePath;
13821
+ for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
13822
+ const filePath = getExistingFilePath(candidateFilePath);
13823
+ if (filePath) return filePath;
13824
+ }
13825
+ return null;
13859
13826
  };
13860
- const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
13861
- const isPropCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isPropAlias(analysis, innerRef));
13862
- const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
13863
- const getUseStateDecl = (analysis, ref) => {
13864
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
13865
- while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
13866
- return node ?? null;
13827
+ const resolveModuleFileFromAbsolutePath = (importPath) => {
13828
+ const directFilePath = resolveModuleFilePath(importPath);
13829
+ if (directFilePath) return directFilePath;
13830
+ const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
13831
+ if (packageEntryFilePath) return packageEntryFilePath;
13832
+ return resolveModuleFilePath(path.join(importPath, "index"));
13867
13833
  };
13868
- const isCleanupReturnArgument = (analysis, node) => {
13869
- if (isFunctionLike$2(node)) return true;
13870
- if (isNodeOfType(node, "MemberExpression")) return true;
13871
- if (isNodeOfType(node, "Identifier")) {
13872
- const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
13873
- if (definitionNode && isFunctionLike$2(definitionNode)) return true;
13874
- if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13875
- const initializer = definitionNode.init;
13876
- return isFunctionLike$2(initializer);
13834
+ const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
13835
+ //#endregion
13836
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
13837
+ const getUniqueFilePath = (filePaths) => {
13838
+ const uniqueFilePaths = new Set(filePaths);
13839
+ if (uniqueFilePaths.size !== 1) return null;
13840
+ const [filePath] = uniqueFilePaths;
13841
+ return filePath ?? null;
13842
+ };
13843
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
13844
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
13845
+ if (!resolvedTargetPath) return null;
13846
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
13847
+ if (nestedTargetPath) return nestedTargetPath;
13848
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
13849
+ };
13850
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
13851
+ if (visitedFilePaths.has(barrelFilePath)) return null;
13852
+ visitedFilePaths.add(barrelFilePath);
13853
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
13854
+ if (!moduleInfo.isBarrel) return null;
13855
+ const target = moduleInfo.exportsByName.get(exportedName);
13856
+ if (target) {
13857
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
13858
+ if (!resolvedTargetPath) return null;
13859
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
13860
+ }
13861
+ if (exportedName === "default") return null;
13862
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
13863
+ };
13864
+ //#endregion
13865
+ //#region src/plugin/utils/resolve-tsconfig-alias.ts
13866
+ const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
13867
+ const isObjectRecord = (value) => typeof value === "object" && value !== null;
13868
+ const stripJsonComments = (text) => {
13869
+ let output = "";
13870
+ let inString = false;
13871
+ let inLineComment = false;
13872
+ let inBlockComment = false;
13873
+ for (let index = 0; index < text.length; index++) {
13874
+ const character = text[index];
13875
+ const nextCharacter = text[index + 1];
13876
+ if (inLineComment) {
13877
+ if (character === "\n") {
13878
+ inLineComment = false;
13879
+ output += character;
13880
+ }
13881
+ continue;
13882
+ }
13883
+ if (inBlockComment) {
13884
+ if (character === "*" && nextCharacter === "/") {
13885
+ inBlockComment = false;
13886
+ index++;
13887
+ }
13888
+ continue;
13889
+ }
13890
+ if (inString) {
13891
+ output += character;
13892
+ if (character === "\\") {
13893
+ output += nextCharacter ?? "";
13894
+ index++;
13895
+ } else if (character === "\"") inString = false;
13896
+ continue;
13897
+ }
13898
+ if (character === "\"") {
13899
+ inString = true;
13900
+ output += character;
13901
+ continue;
13877
13902
  }
13903
+ if (character === "/" && nextCharacter === "/") {
13904
+ inLineComment = true;
13905
+ index++;
13906
+ continue;
13907
+ }
13908
+ if (character === "/" && nextCharacter === "*") {
13909
+ inBlockComment = true;
13910
+ index++;
13911
+ continue;
13912
+ }
13913
+ output += character;
13878
13914
  }
13879
- if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
13880
- return false;
13915
+ return output.replace(/,(\s*[}\]])/g, "$1");
13881
13916
  };
13882
- const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet()) => {
13883
- if (visited.has(node)) return false;
13884
- visited.add(node);
13885
- if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
13886
- if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
13887
- const record = node;
13888
- for (const [key, value] of Object.entries(record)) {
13889
- if (key === "parent") continue;
13890
- if (Array.isArray(value)) {
13891
- if (value.some((item) => isAstNode(item) && hasCleanupReturn(analysis, item, visited))) return true;
13892
- } else if (isAstNode(value) && hasCleanupReturn(analysis, value, visited)) return true;
13917
+ const parseTsconfigFile = (configFilePath) => {
13918
+ let sourceText;
13919
+ try {
13920
+ sourceText = fs.readFileSync(configFilePath, "utf8");
13921
+ } catch {
13922
+ return null;
13923
+ }
13924
+ try {
13925
+ const parsed = JSON.parse(stripJsonComments(sourceText));
13926
+ return isObjectRecord(parsed) ? parsed : null;
13927
+ } catch {
13928
+ return null;
13893
13929
  }
13894
- return false;
13895
13930
  };
13896
- const hasCleanup = (analysis, node) => {
13897
- const fn = getEffectFn(analysis, node);
13898
- if (!isFunctionLike$2(fn)) return false;
13899
- if (!isNodeOfType(fn.body, "BlockStatement")) return false;
13900
- return hasCleanupReturn(analysis, fn.body);
13931
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
13932
+ const withExtension = extendsValue.endsWith(".json") ? extendsValue : `${extendsValue}.json`;
13933
+ if (extendsValue.startsWith("./") || extendsValue.startsWith("../")) return path.resolve(fromConfigDirectory, withExtension);
13934
+ return path.join(fromConfigDirectory, "node_modules", withExtension);
13935
+ };
13936
+ const parsePathsField = (pathsField) => {
13937
+ const paths = /* @__PURE__ */ new Map();
13938
+ if (!isObjectRecord(pathsField)) return paths;
13939
+ for (const [pattern, targets] of Object.entries(pathsField)) {
13940
+ if (!Array.isArray(targets)) continue;
13941
+ const stringTargets = targets.filter((target) => typeof target === "string");
13942
+ if (stringTargets.length > 0) paths.set(pattern, stringTargets);
13943
+ }
13944
+ return paths;
13945
+ };
13946
+ const readResolvedTsconfig = (configFilePath, extendsDepth) => {
13947
+ const parsed = parseTsconfigFile(configFilePath);
13948
+ if (!parsed) return null;
13949
+ const configDirectory = path.dirname(configFilePath);
13950
+ const compilerOptions = isObjectRecord(parsed.compilerOptions) ? parsed.compilerOptions : {};
13951
+ const baseUrlValue = typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : null;
13952
+ const hasExplicitBaseUrl = baseUrlValue !== null;
13953
+ const baseAbsolutePath = baseUrlValue !== null ? path.resolve(configDirectory, baseUrlValue) : configDirectory;
13954
+ if (isObjectRecord(compilerOptions.paths)) return {
13955
+ baseAbsolutePath,
13956
+ hasExplicitBaseUrl,
13957
+ paths: parsePathsField(compilerOptions.paths)
13958
+ };
13959
+ if (typeof parsed.extends === "string" && extendsDepth < 8) {
13960
+ const parentPath = resolveExtendsPath(parsed.extends, configDirectory);
13961
+ const inherited = parentPath ? readResolvedTsconfig(parentPath, extendsDepth + 1) : null;
13962
+ if (inherited) return inherited;
13963
+ }
13964
+ return hasExplicitBaseUrl ? {
13965
+ baseAbsolutePath,
13966
+ hasExplicitBaseUrl,
13967
+ paths: /* @__PURE__ */ new Map()
13968
+ } : null;
13969
+ };
13970
+ const configByFilePath = /* @__PURE__ */ new Map();
13971
+ const loadTsconfigCached = (configFilePath) => {
13972
+ let fileStat;
13973
+ try {
13974
+ fileStat = fs.statSync(configFilePath);
13975
+ } catch {
13976
+ return null;
13977
+ }
13978
+ const cached = configByFilePath.get(configFilePath);
13979
+ if (cached && cached.mtimeMs === fileStat.mtimeMs) return cached.config;
13980
+ const config = readResolvedTsconfig(configFilePath, 0);
13981
+ configByFilePath.set(configFilePath, {
13982
+ mtimeMs: fileStat.mtimeMs,
13983
+ config
13984
+ });
13985
+ return config;
13901
13986
  };
13902
- const findContainingNode = (analysis, node) => {
13903
- if (!node) return null;
13904
- if (isReactFunctionalComponent(node) || isReactFunctionalHOC(analysis, node) || isCustomHook(node)) return node;
13905
- const parent = node.parent;
13906
- return findContainingNode(analysis, parent);
13987
+ const findNearestTsconfig = (fromDirectory) => {
13988
+ let currentDirectory = fromDirectory;
13989
+ for (let level = 0; level < 30; level++) {
13990
+ for (const fileName of TSCONFIG_FILE_NAMES) {
13991
+ const candidate = loadTsconfigCached(path.join(currentDirectory, fileName));
13992
+ if (candidate) return candidate;
13993
+ }
13994
+ const parentDirectory = path.dirname(currentDirectory);
13995
+ if (parentDirectory === currentDirectory) break;
13996
+ currentDirectory = parentDirectory;
13997
+ }
13998
+ return null;
13999
+ };
14000
+ const matchPathPattern = (source, pattern) => {
14001
+ const starIndex = pattern.indexOf("*");
14002
+ if (starIndex === -1) return source === pattern ? "" : null;
14003
+ const prefix = pattern.slice(0, starIndex);
14004
+ const suffix = pattern.slice(starIndex + 1);
14005
+ if (source.length >= prefix.length + suffix.length && source.startsWith(prefix) && source.endsWith(suffix)) return source.slice(prefix.length, source.length - suffix.length);
14006
+ return null;
14007
+ };
14008
+ const resolveTsconfigAliasPath = (fromFilename, source) => {
14009
+ const config = findNearestTsconfig(path.dirname(fromFilename));
14010
+ if (!config) return null;
14011
+ let bestPattern = null;
14012
+ let bestCapture = "";
14013
+ let bestPrefixLength = -1;
14014
+ for (const pattern of config.paths.keys()) {
14015
+ const capture = matchPathPattern(source, pattern);
14016
+ if (capture === null) continue;
14017
+ const starIndex = pattern.indexOf("*");
14018
+ const prefixLength = starIndex === -1 ? pattern.length : starIndex;
14019
+ if (prefixLength > bestPrefixLength) {
14020
+ bestPattern = pattern;
14021
+ bestCapture = capture;
14022
+ bestPrefixLength = prefixLength;
14023
+ }
14024
+ }
14025
+ if (bestPattern) for (const target of config.paths.get(bestPattern) ?? []) {
14026
+ const substituted = target.replaceAll("*", bestCapture);
14027
+ const resolved = resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, substituted));
14028
+ if (resolved) return resolved;
14029
+ }
14030
+ if (config.hasExplicitBaseUrl) return resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, source));
14031
+ return null;
13907
14032
  };
13908
14033
  //#endregion
13909
- //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
13910
- const noAdjustStateOnPropChange = defineRule({
13911
- id: "no-adjust-state-on-prop-change",
13912
- title: "State synced to a prop inside an effect",
13913
- severity: "error",
13914
- tags: ["test-noise"],
13915
- recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
13916
- create: (context) => ({ CallExpression(node) {
13917
- if (!isUseEffect(node)) return;
13918
- const analysis = getProgramAnalysis(node);
13919
- if (!analysis) return;
13920
- const effectFnRefs = getEffectFnRefs(analysis, node);
13921
- const depsRefs = getEffectDepsRefs(analysis, node);
13922
- if (!effectFnRefs || !depsRefs) return;
13923
- const effectFn = getEffectFn(analysis, node);
13924
- if (!effectFn) return;
13925
- if (!depsRefs.flatMap((ref) => getUpstreamRefs(analysis, ref)).some((ref) => isProp(analysis, ref))) return;
13926
- for (const ref of effectFnRefs) {
13927
- if (!isStateSetterCall(analysis, ref)) continue;
13928
- if (!isSynchronous(ref.identifier, effectFn)) continue;
13929
- const callExpr = getCallExpr(ref);
13930
- if (!callExpr) continue;
13931
- if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
13932
- context.report({
13933
- node: callExpr,
13934
- message: "Your users briefly see the wrong value when the prop changes."
13935
- });
13936
- }
13937
- } })
13938
- });
14034
+ //#region src/plugin/utils/resolve-module-path.ts
14035
+ const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
13939
14036
  //#endregion
13940
- //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
13941
- const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
13942
- const noAriaHiddenOnFocusable = defineRule({
13943
- id: "no-aria-hidden-on-focusable",
13944
- title: "aria-hidden on focusable element",
13945
- tags: ["react-jsx-only"],
13946
- severity: "warn",
13947
- recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable.",
13948
- category: "Accessibility",
13949
- create: (context) => ({ JSXOpeningElement(node) {
13950
- const ariaHidden = hasJsxPropIgnoreCase(node.attributes, "aria-hidden");
13951
- if (!ariaHidden) return;
13952
- const value = ariaHidden.value;
13953
- if (value) {
13954
- if (isNodeOfType(value, "Literal") && value.value !== "true") return;
13955
- if (isNodeOfType(value, "JSXExpressionContainer")) {
13956
- const expression = value.expression;
13957
- if (isNodeOfType(expression, "Literal") && !expression.value) return;
13958
- }
14037
+ //#region src/plugin/utils/resolve-cross-file-function-export.ts
14038
+ const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
14039
+ if (visitedFilePaths.size >= 4) return null;
14040
+ if (visitedFilePaths.has(filePath)) return null;
14041
+ visitedFilePaths.add(filePath);
14042
+ const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
14043
+ const programRoot = parseSourceFile(actualFilePath);
14044
+ if (!programRoot) return null;
14045
+ const exported = findExportedFunctionBody(programRoot, exportedName);
14046
+ if (exported) return exported;
14047
+ for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
14048
+ const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
14049
+ if (!nextFilePath) continue;
14050
+ const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
14051
+ if (resolved) return resolved;
14052
+ }
14053
+ return null;
14054
+ };
14055
+ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
14056
+ const resolvedFilePath = resolveModulePath(fromFilename, source);
14057
+ if (!resolvedFilePath) return null;
14058
+ return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
14059
+ };
14060
+ //#endregion
14061
+ //#region src/plugin/utils/ast-mentions-suspense.ts
14062
+ const isSuspenseJsxOpeningName = (name) => {
14063
+ if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense";
14064
+ return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
14065
+ };
14066
+ const astMentionsSuspense = (programNode) => {
14067
+ let didDetect = false;
14068
+ walkAst(programNode, (child) => {
14069
+ if (didDetect) return false;
14070
+ if (isNodeOfType(child, "JSXOpeningElement") && isSuspenseJsxOpeningName(child.name)) {
14071
+ didDetect = true;
14072
+ return false;
13959
14073
  }
13960
- const tag = getElementType(node, context.settings);
13961
- const tabIndex = hasJsxPropIgnoreCase(node.attributes, "tabIndex");
13962
- const tabIndexValue = tabIndex ? parseJsxValue(tabIndex.value ?? null) : null;
13963
- if (tabIndexValue !== null && tabIndexValue < 0) return;
13964
- const isExplicitlyFocusable = tabIndexValue !== null && tabIndexValue >= 0;
13965
- const isImplicitlyFocusable = isInteractiveElement(tag, node);
13966
- if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
13967
- node: ariaHidden,
13968
- message: MESSAGE$30
13969
- });
13970
- } })
13971
- });
13972
- //#endregion
13973
- //#region src/plugin/utils/is-all-literal-array-expression.ts
13974
- /**
13975
- * True when the node is an `ArrayExpression` whose elements are all
13976
- * primitive literals (string / number / boolean). Used by the two
13977
- * "no array-index key" rule variants to detect a `keys={[1, 2, 3]}`
13978
- * style stable-id array.
13979
- */
13980
- const isAllLiteralArrayExpression = (node) => {
13981
- if (!isNodeOfType(node, "ArrayExpression")) return false;
13982
- const elements = node.elements ?? [];
13983
- if (elements.length < 1) return false;
13984
- for (const element of elements) {
13985
- if (!element) return false;
13986
- if (!isNodeOfType(element, "Literal")) return false;
13987
- const value = element.value;
13988
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
13989
- }
13990
- return true;
13991
- };
13992
- //#endregion
13993
- //#region src/plugin/utils/jsx-stateless-leaf.ts
13994
- const PURE_SVG_PRIMITIVE_TAGS = new Set([
13995
- "circle",
13996
- "ellipse",
13997
- "g",
13998
- "line",
13999
- "path",
14000
- "polygon",
14001
- "polyline",
14002
- "rect",
14003
- "stop",
14004
- "text",
14005
- "tspan",
14006
- "defs",
14007
- "use",
14008
- "mask",
14009
- "marker",
14010
- "linearGradient",
14011
- "radialGradient",
14012
- "clipPath",
14013
- "filter",
14014
- "feGaussianBlur",
14015
- "feOffset",
14016
- "feMerge",
14017
- "feMergeNode",
14018
- "feColorMatrix",
14019
- "feFlood",
14020
- "feComposite",
14021
- "title",
14022
- "desc"
14023
- ]);
14024
- const STATELESS_HTML_LEAF_TAGS = new Set([
14025
- "div",
14026
- "span",
14027
- "p",
14028
- "h1",
14029
- "h2",
14030
- "h3",
14031
- "h4",
14032
- "h5",
14033
- "h6",
14034
- "header",
14035
- "footer",
14036
- "section",
14037
- "article",
14038
- "aside",
14039
- "main",
14040
- "nav",
14041
- "li",
14042
- "ul",
14043
- "ol",
14044
- "dl",
14045
- "dt",
14046
- "dd",
14047
- "tr",
14048
- "td",
14049
- "th",
14050
- "tbody",
14051
- "thead",
14052
- "tfoot",
14053
- "table",
14054
- "caption",
14055
- "colgroup",
14056
- "col",
14057
- "strong",
14058
- "em",
14059
- "small",
14060
- "b",
14061
- "i",
14062
- "u",
14063
- "s",
14064
- "mark",
14065
- "del",
14066
- "ins",
14067
- "sub",
14068
- "sup",
14069
- "abbr",
14070
- "cite",
14071
- "code",
14072
- "kbd",
14073
- "samp",
14074
- "pre",
14075
- "blockquote",
14076
- "q",
14077
- "br",
14078
- "hr",
14079
- "wbr",
14080
- "img",
14081
- "picture",
14082
- "figure",
14083
- "figcaption",
14084
- "label",
14085
- "legend",
14086
- "fieldset",
14087
- "address",
14088
- "time",
14089
- "data",
14090
- "var",
14091
- "ruby",
14092
- "rt",
14093
- "rp",
14094
- "bdi",
14095
- "bdo"
14096
- ]);
14097
- const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
14098
- "input",
14099
- "textarea",
14100
- "select",
14101
- "option",
14102
- "optgroup",
14103
- "button",
14104
- "form",
14105
- "output",
14106
- "progress",
14107
- "meter",
14108
- "video",
14109
- "audio",
14110
- "source",
14111
- "track",
14112
- "iframe",
14113
- "embed",
14114
- "object",
14115
- "a",
14116
- "details",
14117
- "summary",
14118
- "dialog",
14119
- "canvas"
14120
- ]);
14121
- const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
14122
- const containsStatefulDescendant = (jsxElement) => {
14123
- let budget = STATEFUL_DESCENDANT_SCAN_BUDGET;
14124
- const stack = [jsxElement];
14125
- while (stack.length > 0) {
14126
- if (budget <= 0) return true;
14127
- budget -= 1;
14128
- const node = stack.pop();
14129
- if (isNodeOfType(node, "JSXElement")) {
14130
- const name = node.openingElement.name;
14131
- if (name && isNodeOfType(name, "JSXIdentifier")) {
14132
- const tagName = name.name;
14133
- const firstChar = tagName.charCodeAt(0);
14134
- if (firstChar >= 65 && firstChar <= 90) return true;
14135
- if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
14074
+ if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
14075
+ if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
14076
+ didDetect = true;
14077
+ return false;
14136
14078
  }
14137
- if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
14138
- const children = node.children ?? [];
14139
- for (const child of children) stack.push(child);
14140
- continue;
14141
- }
14142
- if (isNodeOfType(node, "JSXFragment")) {
14143
- const children = node.children ?? [];
14144
- for (const child of children) stack.push(child);
14145
- continue;
14146
- }
14147
- if (isNodeOfType(node, "JSXExpressionContainer")) {
14148
- const expression = node.expression;
14149
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "Identifier") || isNodeOfType(expression, "MemberExpression")) return true;
14150
- stack.push(expression);
14151
- continue;
14152
- }
14153
- if (isNodeOfType(node, "ConditionalExpression")) {
14154
- stack.push(node.consequent, node.alternate);
14155
- continue;
14156
- }
14157
- if (isNodeOfType(node, "LogicalExpression")) {
14158
- stack.push(node.left, node.right);
14159
- continue;
14160
14079
  }
14080
+ });
14081
+ return didDetect;
14082
+ };
14083
+ //#endregion
14084
+ //#region src/plugin/utils/find-ancestor-suspense-layout.ts
14085
+ const LAYOUT_FILE_NAMES = [
14086
+ "layout.tsx",
14087
+ "layout.jsx",
14088
+ "layout.ts",
14089
+ "layout.js"
14090
+ ];
14091
+ const hasAncestorSuspenseLayout = (pageFilename) => {
14092
+ const normalizedPage = pageFilename.replaceAll("\\", "/");
14093
+ let currentDirectory = path.dirname(normalizedPage);
14094
+ for (let level = 0; level < 30; level++) {
14095
+ for (const layoutFileName of LAYOUT_FILE_NAMES) {
14096
+ const layoutPath = path.join(currentDirectory, layoutFileName);
14097
+ if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
14098
+ const programRoot = parseSourceFile(layoutPath);
14099
+ if (programRoot && astMentionsSuspense(programRoot)) return true;
14100
+ }
14101
+ if (path.basename(currentDirectory) === "app") break;
14102
+ const parentDirectory = path.dirname(currentDirectory);
14103
+ if (parentDirectory === currentDirectory) break;
14104
+ currentDirectory = parentDirectory;
14161
14105
  }
14162
14106
  return false;
14163
14107
  };
14164
14108
  //#endregion
14165
- //#region src/plugin/rules/correctness/no-array-index-as-key.ts
14166
- const STRING_COERCION_FUNCTIONS = new Set(["String", "Number"]);
14167
- const extractIndexName = (node) => {
14168
- if (isNodeOfType(node, "Identifier") && INDEX_PARAMETER_NAMES.has(node.name)) return node.name;
14169
- if (isNodeOfType(node, "TemplateLiteral")) {
14170
- for (const expression of node.expressions ?? []) if (isNodeOfType(expression, "Identifier") && INDEX_PARAMETER_NAMES.has(expression.name)) return expression.name;
14171
- }
14172
- if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && INDEX_PARAMETER_NAMES.has(node.callee.object.name) && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "toString") return node.callee.object.name;
14173
- if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && STRING_COERCION_FUNCTIONS.has(node.callee.name) && isNodeOfType(node.arguments?.[0], "Identifier") && INDEX_PARAMETER_NAMES.has(node.arguments[0].name)) return node.arguments[0].name;
14174
- if (isNodeOfType(node, "BinaryExpression") && node.operator === "+" && (isNodeOfType(node.left, "Identifier") && INDEX_PARAMETER_NAMES.has(node.left.name) && isNodeOfType(node.right, "Literal") && node.right.value === "" || isNodeOfType(node.right, "Identifier") && INDEX_PARAMETER_NAMES.has(node.right.name) && isNodeOfType(node.left, "Literal") && node.left.value === "")) {
14175
- if (isNodeOfType(node.left, "Identifier")) return node.left.name;
14176
- if (isNodeOfType(node.right, "Identifier")) return node.right.name;
14177
- return null;
14178
- }
14179
- return null;
14109
+ //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
14110
+ const astContainsUseSearchParams = (root) => {
14111
+ let didFind = false;
14112
+ walkAst(root, (child) => {
14113
+ if (didFind) return false;
14114
+ if (isHookCall$1(child, "useSearchParams")) {
14115
+ didFind = true;
14116
+ return false;
14117
+ }
14118
+ });
14119
+ return didFind;
14180
14120
  };
14181
- const isNumericLiteralOrUndefined = (node) => {
14182
- if (!node) return false;
14183
- if (isNodeOfType(node, "Literal") && typeof node.value === "number") return true;
14184
- if (isNodeOfType(node, "Identifier") && node.name === "undefined") return true;
14185
- return false;
14121
+ const isSuspenseJsxName = (name, suspenseLocalNames) => {
14122
+ if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense" || suspenseLocalNames.has(name.name);
14123
+ return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
14186
14124
  };
14187
- const isArrayConstructorCallWithNumericLength = (node) => {
14188
- if (!node) return false;
14189
- if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Array" && isNumericLiteralOrUndefined(node.arguments?.[0])) return true;
14190
- if (isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Array" && isNumericLiteralOrUndefined(node.arguments?.[0])) return true;
14125
+ const isInsideSuspenseBoundary = (node, suspenseLocalNames) => {
14126
+ let ancestor = node.parent;
14127
+ while (ancestor) {
14128
+ if (isNodeOfType(ancestor, "JSXElement") && isSuspenseJsxName(ancestor.openingElement?.name, suspenseLocalNames)) return true;
14129
+ ancestor = ancestor.parent ?? null;
14130
+ }
14191
14131
  return false;
14192
14132
  };
14193
- const isArrayFromCall = (node) => {
14194
- if (!node) return false;
14195
- if (!isNodeOfType(node, "CallExpression")) return false;
14196
- const callee = node.callee;
14197
- return Boolean(isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from");
14133
+ const collectSuspenseLocalNames = (programNode) => {
14134
+ const names = /* @__PURE__ */ new Set();
14135
+ for (const statement of programNode.body ?? []) {
14136
+ if (!isNodeOfType(statement, "ImportDeclaration")) continue;
14137
+ if (statement.source?.value !== "react") continue;
14138
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
14139
+ }
14140
+ return names;
14198
14141
  };
14199
- /**
14200
- * True if every element of an ArrayExpression is a primitive constant
14201
- * (number/string/boolean literal) `[1, 2, 3]`, `['a', 'b']`. Such arrays
14202
- * have a fixed order at every render, so an index key is stable.
14203
- */
14204
- /**
14205
- * True if the call expression looks like a placeholder constructor whose
14206
- * elements have no identity beyond their position — i.e. `Array.from(...)`,
14207
- * `Array(N)`, `new Array(N)`, `Array(N).fill(...)`, `[...Array(N)]`, or
14208
- * a literal-only array `[1, 2, 3]` / `['a', 'b']`.
14209
- *
14210
- * Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
14211
- */
14212
- const isStaticPlaceholderReceiver = (receiver) => {
14213
- if (isArrayFromCall(receiver)) return true;
14214
- if (isArrayConstructorCallWithNumericLength(receiver)) return true;
14215
- if (isAllLiteralArrayExpression(receiver)) return true;
14216
- if (isNodeOfType(receiver, "CallExpression")) {
14217
- const callee = receiver.callee;
14218
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
14219
- }
14220
- if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
14221
- const only = receiver.elements[0];
14222
- if (only && isNodeOfType(only, "SpreadElement")) {
14223
- const arg = only.argument;
14224
- if (isArrayConstructorCallWithNumericLength(arg)) return true;
14225
- if (isArrayFromCall(arg)) return true;
14226
- }
14227
- }
14228
- return false;
14229
- };
14230
- const isArrayFromLengthObjectCall = (node) => {
14231
- if (!isArrayFromCall(node)) return false;
14232
- if (!isNodeOfType(node, "CallExpression")) return false;
14233
- const first = node.arguments?.[0];
14234
- if (!first || !isNodeOfType(first, "ObjectExpression")) return false;
14235
- for (const prop of first.properties ?? []) {
14236
- if (!isNodeOfType(prop, "Property")) continue;
14237
- const key = prop.key;
14238
- if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
14239
- if (isNumericLiteralOrUndefined(prop.value)) return true;
14240
- if (isNodeOfType(prop.value, "Identifier")) return true;
14241
- }
14242
- return false;
14243
- };
14244
- const isInsideStaticPlaceholderMap = (node) => {
14245
- let current = node;
14246
- while (current.parent) {
14247
- const parent = current.parent;
14248
- if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14249
- const callee = parent.callee;
14250
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
14251
- if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
14142
+ const collectImportedComponents = (programNode) => {
14143
+ const entries = /* @__PURE__ */ new Map();
14144
+ for (const statement of programNode.body ?? []) {
14145
+ if (!isNodeOfType(statement, "ImportDeclaration")) continue;
14146
+ if (typeof statement.source?.value !== "string") continue;
14147
+ const source = statement.source.value;
14148
+ for (const specifier of statement.specifiers ?? []) {
14149
+ const localName = specifier.local?.name;
14150
+ if (!localName) continue;
14151
+ const exportedName = resolveImportedExportName(specifier);
14152
+ if (!exportedName) continue;
14153
+ entries.set(localName, {
14154
+ source,
14155
+ exportedName
14156
+ });
14252
14157
  }
14253
- current = parent;
14254
14158
  }
14255
- return false;
14159
+ return entries;
14256
14160
  };
14257
- /**
14258
- * Walk up from a JSXAttribute node looking for the enclosing iterator
14259
- * callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
14260
- * and return the first parameter's name. The first param is the per-item
14261
- * value, e.g. `item` in `arr.map((item, index) => …)`.
14262
- */
14263
- const findIteratorItemName$1 = (node) => {
14264
- let current = node;
14265
- while (current.parent) {
14266
- const parent = current.parent;
14267
- if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14268
- const callee = parent.callee;
14269
- const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
14270
- const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
14271
- if (isIteratorMethodCall || isArrayFromCallback) {
14272
- const first = (current.params ?? [])[0];
14273
- if (first && isNodeOfType(first, "Identifier")) return first.name;
14274
- return null;
14161
+ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
14162
+ id: "nextjs-no-use-search-params-without-suspense",
14163
+ title: "useSearchParams without Suspense",
14164
+ tags: ["test-noise"],
14165
+ requires: ["nextjs"],
14166
+ severity: "warn",
14167
+ recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
14168
+ create: (context) => {
14169
+ let isPageOrLayoutFile = false;
14170
+ let hasAncestorLayoutSuspense = false;
14171
+ let hasSuspenseInFile = false;
14172
+ let importedComponents = /* @__PURE__ */ new Map();
14173
+ let suspenseLocalNames = /* @__PURE__ */ new Set();
14174
+ return {
14175
+ Program(programNode) {
14176
+ const filename = normalizeFilename$1(context.filename ?? "");
14177
+ isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
14178
+ if (!isPageOrLayoutFile) return;
14179
+ hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
14180
+ if (hasAncestorLayoutSuspense) return;
14181
+ hasSuspenseInFile = astMentionsSuspense(programNode);
14182
+ importedComponents = collectImportedComponents(programNode);
14183
+ suspenseLocalNames = collectSuspenseLocalNames(programNode);
14184
+ },
14185
+ CallExpression(node) {
14186
+ if (!isPageOrLayoutFile || hasAncestorLayoutSuspense || hasSuspenseInFile) return;
14187
+ if (!isHookCall$1(node, "useSearchParams")) return;
14188
+ context.report({
14189
+ node,
14190
+ message: "useSearchParams() without a <Suspense> boundary forces the whole page into client-side rendering."
14191
+ });
14192
+ },
14193
+ JSXOpeningElement(node) {
14194
+ if (!isPageOrLayoutFile || hasAncestorLayoutSuspense) return;
14195
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
14196
+ const importEntry = importedComponents.get(node.name.name);
14197
+ if (!importEntry) return;
14198
+ const jsxElement = node.parent;
14199
+ if (!jsxElement) return;
14200
+ if (isInsideSuspenseBoundary(jsxElement, suspenseLocalNames)) return;
14201
+ const componentBody = resolveCrossFileFunctionExport(context.filename ?? "", importEntry.source, importEntry.exportedName);
14202
+ if (!componentBody || !astContainsUseSearchParams(componentBody)) return;
14203
+ context.report({
14204
+ node,
14205
+ message: `<${node.name.name}> uses useSearchParams() but is not wrapped in a <Suspense> boundary.`
14206
+ });
14275
14207
  }
14276
- }
14277
- current = parent;
14278
- }
14279
- return null;
14280
- };
14281
- const templateLiteralHasIteratorIdentity = (template, itemName) => {
14282
- for (const expression of template.expressions ?? []) {
14283
- if (isNodeOfType(expression, "Identifier") && expression.name === itemName) return true;
14284
- if (isNodeOfType(expression, "MemberExpression") && isNodeOfType(expression.object, "Identifier") && expression.object.name === itemName) return true;
14208
+ };
14285
14209
  }
14286
- return false;
14287
- };
14288
- /**
14289
- * True when the JSX key value is a template literal mixing an index with at
14290
- * least one stable per-item identifier (e.g. `${item.id}-${index}`). Common
14291
- * defensive pattern in user code — the index is just a uniqueness fallback,
14292
- * the real identity is `item.id`.
14293
- */
14294
- const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
14295
- if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
14296
- if ((keyExpression.expressions ?? []).length < 2) return false;
14297
- const itemName = findIteratorItemName$1(attributeNode);
14298
- if (!itemName) return false;
14299
- return templateLiteralHasIteratorIdentity(keyExpression, itemName);
14300
- };
14301
- const noArrayIndexAsKey = defineRule({
14302
- id: "no-array-index-as-key",
14303
- title: "Array index used as a key",
14210
+ });
14211
+ //#endregion
14212
+ //#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
14213
+ const nextjsNoVercelOgImport = defineRule({
14214
+ id: "nextjs-no-vercel-og-import",
14215
+ title: "@vercel/og import instead of next/og",
14216
+ tags: ["test-noise"],
14217
+ requires: ["nextjs"],
14304
14218
  severity: "warn",
14305
- recommendation: "Use a stable id from the item, like `key={item.id}` or `key={item.slug}`. Index keys break when the list reorders or filters.",
14306
- create: (context) => ({ JSXAttribute(node) {
14307
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "key") return;
14308
- if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
14309
- const indexName = extractIndexName(node.value.expression);
14310
- if (!indexName) return;
14311
- if (isInsideStaticPlaceholderMap(node)) return;
14312
- if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
14313
- const openingElement = node.parent;
14314
- if (openingElement && isNodeOfType(openingElement, "JSXOpeningElement")) {
14315
- const elementName = openingElement.name;
14316
- if (isNodeOfType(elementName, "JSXIdentifier")) {
14317
- if (elementName.name === "Fragment") return;
14318
- if (PURE_SVG_PRIMITIVE_TAGS.has(elementName.name)) return;
14319
- if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
14320
- const jsxElement = openingElement.parent;
14321
- if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
14322
- if (!containsStatefulDescendant(jsxElement)) return;
14323
- }
14324
- }
14325
- }
14326
- if (isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && isNodeOfType(elementName.property, "JSXIdentifier") && elementName.object.name === "React" && elementName.property.name === "Fragment") return;
14327
- }
14219
+ recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
14220
+ create: (context) => ({ ImportDeclaration(node) {
14221
+ if (node.source?.value !== "@vercel/og") return;
14328
14222
  context.report({
14329
14223
  node,
14330
- message: `Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like \`key={item.id}\`, not the array index "${indexName}".`
14224
+ message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
14331
14225
  });
14332
14226
  } })
14333
14227
  });
14334
14228
  //#endregion
14335
- //#region src/plugin/rules/react-builtins/no-array-index-key.ts
14336
- const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
14337
- const SECOND_INDEX_METHODS = new Set([
14338
- "every",
14339
- "filter",
14340
- "find",
14341
- "findIndex",
14342
- "flatMap",
14343
- "forEach",
14344
- "map",
14345
- "some"
14346
- ]);
14347
- const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
14348
- const isPositionallyStableIterationReceiver = (receiver) => {
14349
- if (isAllLiteralArrayExpression(receiver)) return true;
14350
- if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
14351
- const only = receiver.elements[0];
14352
- if (only && isNodeOfType(only, "SpreadElement")) {
14353
- const arg = only.argument;
14354
- if (arg && isPositionallyStableIterationReceiver(arg)) return true;
14229
+ //#region src/plugin/rules/a11y/no-access-key.ts
14230
+ const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
14231
+ const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
14232
+ const noAccessKey = defineRule({
14233
+ id: "no-access-key",
14234
+ title: "accessKey attribute used",
14235
+ tags: ["react-jsx-only"],
14236
+ severity: "warn",
14237
+ recommendation: "Do not use `accessKey`. It conflicts with assistive tech shortcuts.",
14238
+ category: "Accessibility",
14239
+ create: (context) => ({ JSXOpeningElement(node) {
14240
+ const accessKey = hasJsxPropIgnoreCase(node.attributes, "accessKey");
14241
+ if (!accessKey) return;
14242
+ const attributeValue = accessKey.value;
14243
+ if (!attributeValue) return;
14244
+ if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
14245
+ context.report({
14246
+ node: accessKey,
14247
+ message: MESSAGE$31
14248
+ });
14249
+ return;
14355
14250
  }
14356
- }
14357
- if (!isNodeOfType(receiver, "CallExpression")) return false;
14358
- const callee = receiver.callee;
14359
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
14360
- if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
14361
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
14362
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
14363
- return false;
14364
- };
14365
- const templateHasIteratorMember = (templateLiteral, iteratorName) => {
14366
- for (const expression of templateLiteral.expressions ?? []) {
14367
- if (isNodeOfType(expression, "Identifier") && expression.name === iteratorName) return true;
14368
- if (isNodeOfType(expression, "MemberExpression") && isNodeOfType(expression.object, "Identifier") && expression.object.name === iteratorName) return true;
14369
- }
14370
- return false;
14371
- };
14372
- const isArrayFromMapperCallback = (parentCall, callback) => {
14373
- if (parentCall.arguments[1] !== callback) return false;
14374
- const callee = parentCall.callee;
14375
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
14376
- };
14377
- const isArrayFromSourcePositionallyStable = (source) => {
14378
- if (isNodeOfType(source, "ObjectExpression")) {
14379
- for (const property of source.properties ?? []) {
14380
- if (!isNodeOfType(property, "Property")) continue;
14381
- const key = property.key;
14382
- if (isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length") return true;
14251
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
14252
+ const expression = attributeValue.expression;
14253
+ if (!expression || expression.type === "JSXEmptyExpression") return;
14254
+ if (isUndefinedIdentifier(expression)) return;
14255
+ context.report({
14256
+ node: accessKey,
14257
+ message: MESSAGE$31
14258
+ });
14383
14259
  }
14384
- return false;
14385
- }
14386
- return isPositionallyStableIterationReceiver(source);
14260
+ } })
14261
+ });
14262
+ //#endregion
14263
+ //#region src/plugin/rules/state-and-effects/utils/effect/constants.ts
14264
+ const TYPESCRIPT_VISITOR_KEYS = {
14265
+ TSAsExpression: ["expression", "typeAnnotation"],
14266
+ TSNonNullExpression: ["expression"],
14267
+ TSSatisfiesExpression: ["expression", "typeAnnotation"],
14268
+ TSTypeAssertion: ["typeAnnotation", "expression"],
14269
+ TSInstantiationExpression: ["expression", "typeArguments"]
14387
14270
  };
14388
- const findIteratorItemName = (node) => {
14389
- let current = node;
14390
- while (current) {
14391
- if (isNodeOfType(current, "ArrowFunctionExpression") || isNodeOfType(current, "FunctionExpression")) {
14392
- const parent = current.parent;
14393
- const callbackItemName = readIteratorItemFromCallback(current, parent);
14394
- if (callbackItemName !== void 0) return callbackItemName;
14395
- if (current.params.length > 0) return null;
14271
+ const VISITOR_KEYS = {
14272
+ ...eslintVisitorKeys.KEYS,
14273
+ ...TYPESCRIPT_VISITOR_KEYS
14274
+ };
14275
+ //#endregion
14276
+ //#region src/plugin/rules/state-and-effects/utils/effect/get-program-analysis.ts
14277
+ const programToAnalysis = /* @__PURE__ */ new WeakMap();
14278
+ const stripAndRecordParents = (root) => {
14279
+ const restorations = [];
14280
+ const seen = /* @__PURE__ */ new WeakSet();
14281
+ const visit = (value) => {
14282
+ if (!value || typeof value !== "object") return;
14283
+ if (seen.has(value)) return;
14284
+ seen.add(value);
14285
+ if (Array.isArray(value)) {
14286
+ for (const item of value) visit(item);
14287
+ return;
14396
14288
  }
14397
- current = current.parent ?? null;
14289
+ const record = value;
14290
+ if (!("type" in record)) return;
14291
+ if ("parent" in record) {
14292
+ restorations.push({
14293
+ node: record,
14294
+ originalParent: record.parent
14295
+ });
14296
+ record.parent = null;
14297
+ }
14298
+ for (const key of Object.keys(record)) {
14299
+ if (key === "parent") continue;
14300
+ visit(record[key]);
14301
+ }
14302
+ };
14303
+ visit(root);
14304
+ return restorations;
14305
+ };
14306
+ const restoreParents = (restorations) => {
14307
+ for (const restoration of restorations) restoration.node.parent = restoration.originalParent;
14308
+ };
14309
+ const getProgramAnalysis = (anyNode) => {
14310
+ const programNode = findProgramRoot(anyNode);
14311
+ if (!programNode) return null;
14312
+ const cached = programToAnalysis.get(programNode);
14313
+ if (cached) return cached;
14314
+ const restorations = stripAndRecordParents(programNode);
14315
+ let scopeManager;
14316
+ try {
14317
+ scopeManager = analyze(programNode, {
14318
+ ecmaVersion: 2024,
14319
+ sourceType: "module",
14320
+ childVisitorKeys: VISITOR_KEYS,
14321
+ fallback: "iteration"
14322
+ });
14323
+ } finally {
14324
+ restoreParents(restorations);
14398
14325
  }
14399
- return null;
14326
+ const analysis = {
14327
+ programNode,
14328
+ scopeManager
14329
+ };
14330
+ programToAnalysis.set(programNode, analysis);
14331
+ return analysis;
14400
14332
  };
14401
- const readIteratorItemFromCallback = (callback, parent) => {
14402
- if (!parent || !isNodeOfType(parent, "CallExpression")) return void 0;
14403
- const callee = parent.callee;
14404
- if (parent.arguments[0] === callback && isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
14405
- const methodName = callee.property.name;
14406
- if (SECOND_INDEX_METHODS.has(methodName)) {
14407
- const item = callback.params[0];
14408
- return item && isNodeOfType(item, "Identifier") ? item.name : null;
14409
- }
14410
- if (THIRD_INDEX_METHODS.has(methodName)) {
14411
- const item = callback.params[1];
14412
- return item && isNodeOfType(item, "Identifier") ? item.name : null;
14333
+ const getScopeForNode = (node, manager) => {
14334
+ if (!node.range) return null;
14335
+ let bestScope = null;
14336
+ let bestSize = Infinity;
14337
+ for (const scope of manager.scopes) {
14338
+ const block = scope.block;
14339
+ if (!block?.range) continue;
14340
+ if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
14341
+ const size = block.range[1] - block.range[0];
14342
+ if (size <= bestSize) {
14343
+ bestSize = size;
14344
+ bestScope = scope;
14413
14345
  }
14414
14346
  }
14415
- if (isArrayFromMapperCallback(parent, callback)) {
14416
- const item = callback.params[0];
14417
- return item && isNodeOfType(item, "Identifier") ? item.name : null;
14347
+ return bestScope;
14348
+ };
14349
+ //#endregion
14350
+ //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
14351
+ const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
14352
+ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
14353
+ if (visited.has(ref)) return;
14354
+ const result = visit(ref);
14355
+ visited.add(ref);
14356
+ if (result === false) return;
14357
+ const defs = ref.resolved?.defs ?? [];
14358
+ for (const def of defs) {
14359
+ if (def.type === "ImportBinding") continue;
14360
+ if (def.type === "Parameter") continue;
14361
+ const defNode = def.node;
14362
+ const next = defNode.init ?? defNode.body;
14363
+ if (!next) continue;
14364
+ for (const innerRef of getDownstreamRefs(analysis, next)) ascend(analysis, innerRef, visit, visited);
14418
14365
  }
14419
14366
  };
14420
- const findIndexParameterBinding = (node) => {
14421
- let current = node.parent;
14422
- while (current) {
14423
- if (isNodeOfType(current, "ArrowFunctionExpression") || isNodeOfType(current, "FunctionExpression")) {
14424
- const indexParam = readIteratorIndexFromCallback(current, current.parent);
14425
- if (indexParam !== void 0) return indexParam;
14426
- if (current.params.length > 0) return null;
14427
- }
14428
- current = current.parent ?? null;
14367
+ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
14368
+ if (visited.has(node)) return;
14369
+ visit(node);
14370
+ visited.add(node);
14371
+ const keys = getChildKeys(node);
14372
+ const record = node;
14373
+ for (const key of keys) {
14374
+ const child = record[key];
14375
+ if (!child) continue;
14376
+ if (Array.isArray(child)) {
14377
+ for (const item of child) if (item && isAstNode(item)) descend(item, visit, visited);
14378
+ } else if (isAstNode(child)) descend(child, visit, visited);
14429
14379
  }
14380
+ };
14381
+ const getUpstreamRefs = (analysis, ref) => {
14382
+ const refs = [];
14383
+ ascend(analysis, ref, (upRef) => {
14384
+ refs.push(upRef);
14385
+ });
14386
+ return refs;
14387
+ };
14388
+ const findDownstreamNodes = (topNode, type) => {
14389
+ const nodes = [];
14390
+ descend(topNode, (node) => {
14391
+ if (node.type === type) nodes.push(node);
14392
+ });
14393
+ return nodes;
14394
+ };
14395
+ const getRef = (analysis, identifier) => {
14396
+ const scope = getScopeForNode(identifier, analysis.scopeManager);
14397
+ if (!scope) return null;
14398
+ for (const reference of scope.references) if (reference.identifier === identifier) return reference;
14430
14399
  return null;
14431
14400
  };
14432
- const readIteratorIndexFromCallback = (callback, parent) => {
14433
- if (!parent || !isNodeOfType(parent, "CallExpression")) return void 0;
14434
- const callee = parent.callee;
14435
- if (parent.arguments[0] === callback && isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
14436
- const methodName = callee.property.name;
14437
- let indexParamPosition = null;
14438
- if (SECOND_INDEX_METHODS.has(methodName)) indexParamPosition = 1;
14439
- else if (THIRD_INDEX_METHODS.has(methodName)) indexParamPosition = 2;
14440
- if (indexParamPosition !== null) {
14441
- const receiver = callee.object;
14442
- if (isPositionallyStableIterationReceiver(receiver)) return null;
14443
- const indexParam = callback.params[indexParamPosition];
14444
- return indexParam && isNodeOfType(indexParam, "Identifier") ? indexParam : null;
14445
- }
14401
+ const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
14402
+ const getDownstreamRefs = (analysis, node) => {
14403
+ let perNode = downstreamRefsCache.get(analysis);
14404
+ if (!perNode) {
14405
+ perNode = /* @__PURE__ */ new WeakMap();
14406
+ downstreamRefsCache.set(analysis, perNode);
14446
14407
  }
14447
- if (isArrayFromMapperCallback(parent, callback)) {
14448
- const source = parent.arguments[0];
14449
- if (source && isArrayFromSourcePositionallyStable(source)) return null;
14450
- const indexParam = callback.params[1];
14451
- return indexParam && isNodeOfType(indexParam, "Identifier") ? indexParam : null;
14408
+ const cached = perNode.get(node);
14409
+ if (cached) return cached;
14410
+ const refs = [];
14411
+ for (const identifier of findDownstreamNodes(node, "Identifier")) {
14412
+ const ref = getRef(analysis, identifier);
14413
+ if (ref) refs.push(ref);
14452
14414
  }
14415
+ perNode.set(node, refs);
14416
+ return refs;
14453
14417
  };
14454
- const isIndexReference = (expression, paramName) => isNodeOfType(expression, "Identifier") && expression.name === paramName;
14455
- const expressionUsesIndex = (expression, paramName) => {
14456
- if (isIndexReference(expression, paramName)) return true;
14457
- if (isNodeOfType(expression, "TemplateLiteral")) return expression.expressions.some((innerExpression) => isIndexReference(innerExpression, paramName));
14458
- if (isNodeOfType(expression, "BinaryExpression")) {
14459
- const usesInLeft = isIndexReference(expression.left, paramName);
14460
- const usesInRight = isIndexReference(expression.right, paramName);
14461
- if (usesInLeft || usesInRight) return true;
14462
- if (isNodeOfType(expression.left, "BinaryExpression") && expressionUsesIndex(expression.left, paramName)) return true;
14463
- if (isNodeOfType(expression.right, "BinaryExpression") && expressionUsesIndex(expression.right, paramName)) return true;
14464
- return false;
14418
+ const getCallExpr = (ref, current = ref.identifier.parent) => {
14419
+ if (!current) return null;
14420
+ if (isNodeOfType(current, "CallExpression")) {
14421
+ let node = ref.identifier;
14422
+ let parent = node.parent;
14423
+ while (parent && isNodeOfType(parent, "MemberExpression")) {
14424
+ node = parent;
14425
+ parent = node.parent;
14426
+ }
14427
+ if (current.callee === node) return current;
14465
14428
  }
14466
- if (isNodeOfType(expression, "CallExpression")) {
14467
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
14468
- if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
14429
+ if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
14430
+ return null;
14431
+ };
14432
+ const getArgsUpstreamRefs = (analysis, ref) => {
14433
+ const result = [];
14434
+ for (const upRef of getUpstreamRefs(analysis, ref)) {
14435
+ const callExpr = getCallExpr(upRef);
14436
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
14437
+ for (const argument of callExpr.arguments ?? []) for (const argRef of getDownstreamRefs(analysis, argument)) for (const innerRef of getUpstreamRefs(analysis, argRef)) result.push(innerRef);
14469
14438
  }
14470
- return false;
14439
+ return result;
14471
14440
  };
14472
- const isReactCloneElement = (callExpression) => {
14473
- const callee = callExpression.callee;
14474
- if (!isNodeOfType(callee, "MemberExpression")) return false;
14475
- if (!isNodeOfType(callee.property, "Identifier")) return false;
14476
- if (callee.property.name !== "cloneElement") return false;
14477
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
14441
+ const isSynchronous = (node, within) => {
14442
+ if (!node) return false;
14443
+ if (node === within) return true;
14444
+ if (node.async === true) return false;
14445
+ if (isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "void" || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) return false;
14446
+ return isSynchronous(node.parent, within);
14478
14447
  };
14479
- const isPureSvgPrimitiveJsxName = (jsxOpeningName) => isNodeOfType(jsxOpeningName, "JSXIdentifier") && PURE_SVG_PRIMITIVE_TAGS.has(jsxOpeningName.name);
14480
- const isStatelessLeafJsxName = (jsxOpeningName) => isNodeOfType(jsxOpeningName, "JSXIdentifier") && STATELESS_HTML_LEAF_TAGS.has(jsxOpeningName.name);
14481
- const isFragmentJsxName = (jsxOpeningName) => {
14482
- if (isNodeOfType(jsxOpeningName, "JSXIdentifier")) return jsxOpeningName.name === "Fragment";
14483
- if (isNodeOfType(jsxOpeningName, "JSXMemberExpression") && isNodeOfType(jsxOpeningName.object, "JSXIdentifier") && isNodeOfType(jsxOpeningName.property, "JSXIdentifier") && jsxOpeningName.object.name === "React" && jsxOpeningName.property.name === "Fragment") return true;
14484
- return false;
14448
+ const isEventualCallTo = (analysis, ref, predicate) => {
14449
+ const callExprRefs = [];
14450
+ ascend(analysis, ref, (upRef) => {
14451
+ if (getCallExpr(upRef)) callExprRefs.push(upRef);
14452
+ else return false;
14453
+ });
14454
+ return callExprRefs.some(predicate);
14485
14455
  };
14486
- const noArrayIndexKey = defineRule({
14487
- id: "no-array-index-key",
14488
- title: "Array index used as a key",
14489
- severity: "warn",
14490
- defaultEnabled: false,
14491
- recommendation: "Use a stable `key` from your data instead of the array index.",
14492
- category: "Performance",
14493
- create: (context) => ({
14494
- JSXOpeningElement(node) {
14495
- const keyAttribute = hasJsxPropIgnoreCase(node.attributes, "key");
14496
- if (!keyAttribute) return;
14497
- if (!keyAttribute.value || !isNodeOfType(keyAttribute.value, "JSXExpressionContainer")) return;
14498
- const expression = keyAttribute.value.expression;
14499
- if (expression.type === "JSXEmptyExpression") return;
14500
- if (isFragmentJsxName(node.name)) return;
14501
- if (isPureSvgPrimitiveJsxName(node.name)) return;
14502
- const indexBinding = findIndexParameterBinding(node);
14503
- if (!indexBinding) return;
14504
- if (!expressionUsesIndex(expression, indexBinding.name)) return;
14505
- if (isNodeOfType(expression, "TemplateLiteral")) {
14506
- const itemName = findIteratorItemName(node);
14507
- if (itemName && templateHasIteratorMember(expression, itemName)) return;
14508
- const interpolations = expression.expressions ?? [];
14509
- let interpolationsBeyondIndex = 0;
14510
- for (const interpolation of interpolations) {
14511
- if (isIndexReference(interpolation, indexBinding.name)) continue;
14512
- interpolationsBeyondIndex += 1;
14513
- if (interpolationsBeyondIndex >= 1) break;
14514
- }
14515
- if (interpolationsBeyondIndex >= 1) return;
14516
- }
14517
- if (isNodeOfType(expression, "BinaryExpression") && expression.operator === "+") {
14518
- let interpolationsBeyondIndex = 0;
14519
- const walkOperand = (operand) => {
14520
- if (isNodeOfType(operand, "BinaryExpression") && operand.operator === "+") {
14521
- walkOperand(operand.left);
14522
- walkOperand(operand.right);
14523
- return;
14524
- }
14525
- if (isIndexReference(operand, indexBinding.name)) return;
14526
- if (isNodeOfType(operand, "Literal") && typeof operand.value === "string") return;
14527
- interpolationsBeyondIndex += 1;
14528
- };
14529
- walkOperand(expression);
14530
- if (interpolationsBeyondIndex >= 1) return;
14531
- }
14532
- if (isStatelessLeafJsxName(node.name)) {
14533
- const jsxElement = node.parent;
14534
- if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
14535
- if (!containsStatefulDescendant(jsxElement)) return;
14536
- }
14537
- }
14456
+ //#endregion
14457
+ //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
14458
+ const getOuterScopeContaining = (analysis, node) => {
14459
+ if (!node.range) return null;
14460
+ let best = null;
14461
+ let bestSize = Infinity;
14462
+ for (const scope of analysis.scopeManager.scopes) {
14463
+ const block = scope.block;
14464
+ if (!block?.range) continue;
14465
+ if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
14466
+ const size = block.range[1] - block.range[0];
14467
+ if (size <= bestSize) {
14468
+ bestSize = size;
14469
+ best = scope;
14470
+ }
14471
+ }
14472
+ return best;
14473
+ };
14474
+ const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
14475
+ const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
14476
+ const isReactFunctionalComponent = (node) => {
14477
+ if (!node) return false;
14478
+ if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id && startsWithUppercase(node.id.name));
14479
+ if (isNodeOfType(node, "VariableDeclarator")) {
14480
+ if (!isNodeOfType(node.id, "Identifier")) return false;
14481
+ if (!startsWithUppercase(node.id.name)) return false;
14482
+ const init = node.init;
14483
+ if (!init) return false;
14484
+ return isNodeOfType(init, "ArrowFunctionExpression") || isNodeOfType(init, "CallExpression");
14485
+ }
14486
+ return false;
14487
+ };
14488
+ const isReactFunctionalHOC = (analysis, node) => {
14489
+ if (!isReactFunctionalComponent(node)) return false;
14490
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14491
+ const init = node.init;
14492
+ if (!init) return false;
14493
+ const isWrappedInline = () => {
14494
+ if (!isNodeOfType(init, "CallExpression")) return false;
14495
+ if (!isNodeOfType(init.callee, "Identifier")) return false;
14496
+ if (KNOWN_PURE_HOC_NAMES.has(init.callee.name)) return false;
14497
+ const firstArg = init.arguments?.[0];
14498
+ if (!firstArg) return false;
14499
+ return isNodeOfType(firstArg, "ArrowFunctionExpression") || isNodeOfType(firstArg, "FunctionExpression");
14500
+ };
14501
+ const isWrappedSeparately = () => {
14502
+ if (!isNodeOfType(node.id, "Identifier")) return false;
14503
+ const bindingName = node.id.name;
14504
+ const containingScope = getOuterScopeContaining(analysis, node);
14505
+ if (!containingScope) return false;
14506
+ const variable = containingScope.variables.find((v) => v.name === bindingName);
14507
+ if (!variable) return false;
14508
+ for (const reference of variable.references) {
14509
+ const parent = reference.identifier.parent;
14510
+ if (!parent || !isNodeOfType(parent, "CallExpression")) continue;
14511
+ const args = parent.arguments ?? [];
14512
+ const refId = reference.identifier;
14513
+ if (!args.includes(refId)) continue;
14514
+ const callee = parent.callee;
14515
+ const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "CallExpression") && isNodeOfType(callee.callee, "Identifier") ? callee.callee.name : null;
14516
+ if (calleeName != null && !KNOWN_PURE_HOC_NAMES.has(calleeName)) return true;
14517
+ }
14518
+ return false;
14519
+ };
14520
+ return isWrappedInline() || isWrappedSeparately();
14521
+ };
14522
+ const isCustomHook = (node) => {
14523
+ if (!node) return false;
14524
+ if (isNodeOfType(node, "FunctionDeclaration")) {
14525
+ const name = node.id?.name;
14526
+ if (!name) return false;
14527
+ return name.startsWith("use") && name.length > 3 && name[3] >= "A" && name[3] <= "Z";
14528
+ }
14529
+ if (isNodeOfType(node, "VariableDeclarator")) {
14530
+ if (!isNodeOfType(node.id, "Identifier")) return false;
14531
+ const name = node.id.name;
14532
+ const init = node.init;
14533
+ if (!init) return false;
14534
+ if (!isNodeOfType(init, "ArrowFunctionExpression") && !isNodeOfType(init, "FunctionExpression")) return false;
14535
+ return name.startsWith("use") && name.length > 3 && name[3] >= "A" && name[3] <= "Z";
14536
+ }
14537
+ return false;
14538
+ };
14539
+ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved?.defs.some((def) => {
14540
+ if (def.type !== "ImportBinding") return false;
14541
+ const declarationNode = def.node;
14542
+ if (!isNodeOfType(declarationNode, "ImportSpecifier")) return false;
14543
+ const imported = declarationNode.imported;
14544
+ if (!isNodeOfType(imported, "Identifier")) return false;
14545
+ if (imported.name !== importedName) return false;
14546
+ const importDeclaration = declarationNode.parent;
14547
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
14548
+ }));
14549
+ const isHookCallee = (analysis, node, hookName) => {
14550
+ if (!node) return false;
14551
+ if (isNodeOfType(node, "Identifier")) {
14552
+ if (node.name === hookName) return true;
14553
+ if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
14554
+ const parent = node.parent;
14555
+ if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
14556
+ return false;
14557
+ }
14558
+ if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
14559
+ return false;
14560
+ };
14561
+ const isUseEffect = (node) => {
14562
+ if (!node || !isNodeOfType(node, "CallExpression")) return false;
14563
+ const callee = node.callee;
14564
+ if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
14565
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
14566
+ return false;
14567
+ };
14568
+ const getEffectFn = (analysis, node) => {
14569
+ if (!isNodeOfType(node, "CallExpression")) return null;
14570
+ const fn = node.arguments?.[0];
14571
+ if (!fn) return null;
14572
+ if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
14573
+ if (isNodeOfType(fn, "Identifier")) {
14574
+ const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
14575
+ if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
14576
+ if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
14577
+ const initializer = definitionNode.init;
14578
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
14579
+ }
14580
+ }
14581
+ return null;
14582
+ };
14583
+ const getEffectFnRefs = (analysis, node) => {
14584
+ const fn = getEffectFn(analysis, node);
14585
+ if (!fn) return null;
14586
+ return getDownstreamRefs(analysis, fn);
14587
+ };
14588
+ const getEffectDepsRefs = (analysis, node) => {
14589
+ if (!isNodeOfType(node, "CallExpression")) return null;
14590
+ const deps = node.arguments?.[1];
14591
+ if (!deps || !isNodeOfType(deps, "ArrayExpression")) return null;
14592
+ return getDownstreamRefs(analysis, deps);
14593
+ };
14594
+ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
14595
+ const node = def.node;
14596
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14597
+ if (!isNodeOfType(node.init, "CallExpression")) return false;
14598
+ if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
14599
+ if (!isNodeOfType(node.id, "ArrayPattern")) return false;
14600
+ const elements = node.id.elements ?? [];
14601
+ if (elements.length !== 1 && elements.length !== 2) return false;
14602
+ const first = elements[0];
14603
+ return Boolean(first && isNodeOfType(first, "Identifier") && first.name === ref.identifier.name);
14604
+ }));
14605
+ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
14606
+ const node = def.node;
14607
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14608
+ if (!isNodeOfType(node.init, "CallExpression")) return false;
14609
+ if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
14610
+ if (!isNodeOfType(node.id, "ArrayPattern")) return false;
14611
+ const elements = node.id.elements ?? [];
14612
+ if (elements.length !== 2) return false;
14613
+ const second = elements[1];
14614
+ return Boolean(second && isNodeOfType(second, "Identifier") && second.name === ref.identifier.name);
14615
+ }));
14616
+ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
14617
+ if (def.type !== "Parameter") return false;
14618
+ const defNode = def.node;
14619
+ let declaringNode = defNode;
14620
+ if (isNodeOfType(defNode, "ArrowFunctionExpression")) {
14621
+ const parent = defNode.parent;
14622
+ if (parent && isNodeOfType(parent, "CallExpression")) declaringNode = parent.parent;
14623
+ else declaringNode = parent;
14624
+ }
14625
+ if (!declaringNode) return false;
14626
+ return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
14627
+ }));
14628
+ const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
14629
+ const isPropAlias = (analysis, ref) => {
14630
+ if (isProp(analysis, ref)) return true;
14631
+ return Boolean(ref.resolved?.defs.some((def) => {
14632
+ const node = def.node;
14633
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14634
+ const initializer = node.init;
14635
+ if (!initializer) return false;
14636
+ if (!isNodeOfType(node.id, "ObjectPattern") && !isIdentifierOrMemberExpression(initializer)) return false;
14637
+ return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
14638
+ }));
14639
+ };
14640
+ const isConstant = (ref) => Boolean((ref.resolved?.defs ?? []).some((def) => {
14641
+ const node = def.node;
14642
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14643
+ const init = node.init;
14644
+ if (!init) return false;
14645
+ return isNodeOfType(init, "Literal") || isNodeOfType(init, "TemplateLiteral") || isNodeOfType(init, "ArrayExpression") || isNodeOfType(init, "ObjectExpression");
14646
+ }));
14647
+ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
14648
+ const node = def.node;
14649
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
14650
+ if (!isNodeOfType(node.init, "CallExpression")) return false;
14651
+ return isHookCallee(analysis, node.init.callee, "useRef");
14652
+ }));
14653
+ const isRefCurrent = (ref) => {
14654
+ const parent = ref.identifier.parent;
14655
+ if (!parent || !isNodeOfType(parent, "MemberExpression")) return false;
14656
+ if (!isNodeOfType(parent.property, "Identifier")) return false;
14657
+ return parent.property.name === "current";
14658
+ };
14659
+ const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
14660
+ const isPropCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isPropAlias(analysis, innerRef));
14661
+ const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
14662
+ const getUseStateDecl = (analysis, ref) => {
14663
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
14664
+ while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
14665
+ return node ?? null;
14666
+ };
14667
+ const isCleanupReturnArgument = (analysis, node) => {
14668
+ if (isFunctionLike$2(node)) return true;
14669
+ if (isNodeOfType(node, "MemberExpression")) return true;
14670
+ if (isNodeOfType(node, "Identifier")) {
14671
+ const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
14672
+ if (definitionNode && isFunctionLike$2(definitionNode)) return true;
14673
+ if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
14674
+ const initializer = definitionNode.init;
14675
+ return isFunctionLike$2(initializer);
14676
+ }
14677
+ }
14678
+ if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
14679
+ return false;
14680
+ };
14681
+ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet()) => {
14682
+ if (visited.has(node)) return false;
14683
+ visited.add(node);
14684
+ if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
14685
+ if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
14686
+ const record = node;
14687
+ for (const [key, value] of Object.entries(record)) {
14688
+ if (key === "parent") continue;
14689
+ if (Array.isArray(value)) {
14690
+ if (value.some((item) => isAstNode(item) && hasCleanupReturn(analysis, item, visited))) return true;
14691
+ } else if (isAstNode(value) && hasCleanupReturn(analysis, value, visited)) return true;
14692
+ }
14693
+ return false;
14694
+ };
14695
+ const hasCleanup = (analysis, node) => {
14696
+ const fn = getEffectFn(analysis, node);
14697
+ if (!isFunctionLike$2(fn)) return false;
14698
+ if (!isNodeOfType(fn.body, "BlockStatement")) return false;
14699
+ return hasCleanupReturn(analysis, fn.body);
14700
+ };
14701
+ const findContainingNode = (analysis, node) => {
14702
+ if (!node) return null;
14703
+ if (isReactFunctionalComponent(node) || isReactFunctionalHOC(analysis, node) || isCustomHook(node)) return node;
14704
+ const parent = node.parent;
14705
+ return findContainingNode(analysis, parent);
14706
+ };
14707
+ //#endregion
14708
+ //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
14709
+ const noAdjustStateOnPropChange = defineRule({
14710
+ id: "no-adjust-state-on-prop-change",
14711
+ title: "State synced to a prop inside an effect",
14712
+ severity: "error",
14713
+ tags: ["test-noise"],
14714
+ recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
14715
+ create: (context) => ({ CallExpression(node) {
14716
+ if (!isUseEffect(node)) return;
14717
+ const analysis = getProgramAnalysis(node);
14718
+ if (!analysis) return;
14719
+ const effectFnRefs = getEffectFnRefs(analysis, node);
14720
+ const depsRefs = getEffectDepsRefs(analysis, node);
14721
+ if (!effectFnRefs || !depsRefs) return;
14722
+ const effectFn = getEffectFn(analysis, node);
14723
+ if (!effectFn) return;
14724
+ if (!depsRefs.flatMap((ref) => getUpstreamRefs(analysis, ref)).some((ref) => isProp(analysis, ref))) return;
14725
+ for (const ref of effectFnRefs) {
14726
+ if (!isStateSetterCall(analysis, ref)) continue;
14727
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
14728
+ const callExpr = getCallExpr(ref);
14729
+ if (!callExpr) continue;
14730
+ if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
14538
14731
  context.report({
14539
- node: keyAttribute,
14540
- message: MESSAGE$29
14732
+ node: callExpr,
14733
+ message: "Your users briefly see the wrong value when the prop changes."
14541
14734
  });
14542
- },
14543
- CallExpression(node) {
14544
- if (!isReactCloneElement(node)) return;
14545
- if (node.arguments.length < 2 || node.arguments.length > 3) return;
14546
- const propsArgument = node.arguments[1];
14547
- if (!isNodeOfType(propsArgument, "ObjectExpression")) return;
14548
- const indexBinding = findIndexParameterBinding(node);
14549
- if (!indexBinding) return;
14550
- for (const property of propsArgument.properties) {
14551
- if (!isNodeOfType(property, "Property")) continue;
14552
- if (property.computed) continue;
14553
- const propKey = property.key;
14554
- let propName = null;
14555
- if (isNodeOfType(propKey, "Identifier")) propName = propKey.name;
14556
- else if (isNodeOfType(propKey, "Literal") && typeof propKey.value === "string") propName = propKey.value;
14557
- if (propName !== "key") continue;
14558
- if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
14559
- node: property,
14560
- message: MESSAGE$29
14561
- });
14735
+ }
14736
+ } })
14737
+ });
14738
+ //#endregion
14739
+ //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
14740
+ const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
14741
+ const noAriaHiddenOnFocusable = defineRule({
14742
+ id: "no-aria-hidden-on-focusable",
14743
+ title: "aria-hidden on focusable element",
14744
+ tags: ["react-jsx-only"],
14745
+ severity: "warn",
14746
+ recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable.",
14747
+ category: "Accessibility",
14748
+ create: (context) => ({ JSXOpeningElement(node) {
14749
+ const ariaHidden = hasJsxPropIgnoreCase(node.attributes, "aria-hidden");
14750
+ if (!ariaHidden) return;
14751
+ const value = ariaHidden.value;
14752
+ if (value) {
14753
+ if (isNodeOfType(value, "Literal") && value.value !== "true") return;
14754
+ if (isNodeOfType(value, "JSXExpressionContainer")) {
14755
+ const expression = value.expression;
14756
+ if (isNodeOfType(expression, "Literal") && !expression.value) return;
14757
+ }
14758
+ }
14759
+ const tag = getElementType(node, context.settings);
14760
+ const tabIndex = hasJsxPropIgnoreCase(node.attributes, "tabIndex");
14761
+ const tabIndexValue = tabIndex ? parseJsxValue(tabIndex.value ?? null) : null;
14762
+ if (tabIndexValue !== null && tabIndexValue < 0) return;
14763
+ const isExplicitlyFocusable = tabIndexValue !== null && tabIndexValue >= 0;
14764
+ const isImplicitlyFocusable = isInteractiveElement(tag, node);
14765
+ if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
14766
+ node: ariaHidden,
14767
+ message: MESSAGE$30
14768
+ });
14769
+ } })
14770
+ });
14771
+ //#endregion
14772
+ //#region src/plugin/utils/is-all-literal-array-expression.ts
14773
+ /**
14774
+ * True when the node is an `ArrayExpression` whose elements are all
14775
+ * primitive literals (string / number / boolean). Used by the two
14776
+ * "no array-index key" rule variants to detect a `keys={[1, 2, 3]}`
14777
+ * style stable-id array.
14778
+ */
14779
+ const isAllLiteralArrayExpression = (node) => {
14780
+ if (!isNodeOfType(node, "ArrayExpression")) return false;
14781
+ const elements = node.elements ?? [];
14782
+ if (elements.length < 1) return false;
14783
+ for (const element of elements) {
14784
+ if (!element) return false;
14785
+ if (!isNodeOfType(element, "Literal")) return false;
14786
+ const value = element.value;
14787
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
14788
+ }
14789
+ return true;
14790
+ };
14791
+ //#endregion
14792
+ //#region src/plugin/utils/jsx-stateless-leaf.ts
14793
+ const PURE_SVG_PRIMITIVE_TAGS = new Set([
14794
+ "circle",
14795
+ "ellipse",
14796
+ "g",
14797
+ "line",
14798
+ "path",
14799
+ "polygon",
14800
+ "polyline",
14801
+ "rect",
14802
+ "stop",
14803
+ "text",
14804
+ "tspan",
14805
+ "defs",
14806
+ "use",
14807
+ "mask",
14808
+ "marker",
14809
+ "linearGradient",
14810
+ "radialGradient",
14811
+ "clipPath",
14812
+ "filter",
14813
+ "feGaussianBlur",
14814
+ "feOffset",
14815
+ "feMerge",
14816
+ "feMergeNode",
14817
+ "feColorMatrix",
14818
+ "feFlood",
14819
+ "feComposite",
14820
+ "title",
14821
+ "desc"
14822
+ ]);
14823
+ const STATELESS_HTML_LEAF_TAGS = new Set([
14824
+ "div",
14825
+ "span",
14826
+ "p",
14827
+ "h1",
14828
+ "h2",
14829
+ "h3",
14830
+ "h4",
14831
+ "h5",
14832
+ "h6",
14833
+ "header",
14834
+ "footer",
14835
+ "section",
14836
+ "article",
14837
+ "aside",
14838
+ "main",
14839
+ "nav",
14840
+ "li",
14841
+ "ul",
14842
+ "ol",
14843
+ "dl",
14844
+ "dt",
14845
+ "dd",
14846
+ "tr",
14847
+ "td",
14848
+ "th",
14849
+ "tbody",
14850
+ "thead",
14851
+ "tfoot",
14852
+ "table",
14853
+ "caption",
14854
+ "colgroup",
14855
+ "col",
14856
+ "strong",
14857
+ "em",
14858
+ "small",
14859
+ "b",
14860
+ "i",
14861
+ "u",
14862
+ "s",
14863
+ "mark",
14864
+ "del",
14865
+ "ins",
14866
+ "sub",
14867
+ "sup",
14868
+ "abbr",
14869
+ "cite",
14870
+ "code",
14871
+ "kbd",
14872
+ "samp",
14873
+ "pre",
14874
+ "blockquote",
14875
+ "q",
14876
+ "br",
14877
+ "hr",
14878
+ "wbr",
14879
+ "img",
14880
+ "picture",
14881
+ "figure",
14882
+ "figcaption",
14883
+ "label",
14884
+ "legend",
14885
+ "fieldset",
14886
+ "address",
14887
+ "time",
14888
+ "data",
14889
+ "var",
14890
+ "ruby",
14891
+ "rt",
14892
+ "rp",
14893
+ "bdi",
14894
+ "bdo"
14895
+ ]);
14896
+ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
14897
+ "input",
14898
+ "textarea",
14899
+ "select",
14900
+ "option",
14901
+ "optgroup",
14902
+ "button",
14903
+ "form",
14904
+ "output",
14905
+ "progress",
14906
+ "meter",
14907
+ "video",
14908
+ "audio",
14909
+ "source",
14910
+ "track",
14911
+ "iframe",
14912
+ "embed",
14913
+ "object",
14914
+ "a",
14915
+ "details",
14916
+ "summary",
14917
+ "dialog",
14918
+ "canvas"
14919
+ ]);
14920
+ const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
14921
+ const containsStatefulDescendant = (jsxElement) => {
14922
+ let budget = STATEFUL_DESCENDANT_SCAN_BUDGET;
14923
+ const stack = [jsxElement];
14924
+ while (stack.length > 0) {
14925
+ if (budget <= 0) return true;
14926
+ budget -= 1;
14927
+ const node = stack.pop();
14928
+ if (isNodeOfType(node, "JSXElement")) {
14929
+ const name = node.openingElement.name;
14930
+ if (name && isNodeOfType(name, "JSXIdentifier")) {
14931
+ const tagName = name.name;
14932
+ const firstChar = tagName.charCodeAt(0);
14933
+ if (firstChar >= 65 && firstChar <= 90) return true;
14934
+ if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
14562
14935
  }
14936
+ if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
14937
+ const children = node.children ?? [];
14938
+ for (const child of children) stack.push(child);
14939
+ continue;
14563
14940
  }
14564
- })
14565
- });
14566
- //#endregion
14567
- //#region src/plugin/rules/a11y/no-autofocus.ts
14568
- const MESSAGE$28 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
14569
- const resolveSettings$21 = (settings) => {
14570
- const reactDoctor = settings?.["react-doctor"];
14571
- return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
14572
- };
14573
- const innerExpression = (expression) => {
14574
- if (expression.type === "ParenthesizedExpression" && "expression" in expression) return innerExpression(expression.expression);
14575
- return expression;
14576
- };
14577
- const isSameNameIdentifierForward = (attributeName, value) => {
14578
- if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
14579
- const expression = innerExpression(value.expression);
14580
- if (isNodeOfType(expression, "Identifier") && expression.name === attributeName) return true;
14581
- if (isNodeOfType(expression, "MemberExpression") && !expression.computed && isNodeOfType(expression.property, "Identifier") && expression.property.name === attributeName) return true;
14582
- return false;
14583
- };
14584
- const isFalseAttributeValue = (value) => {
14585
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value === "false" : value.value === false;
14586
- if (isNodeOfType(value, "JSXExpressionContainer")) {
14587
- const expression = innerExpression(value.expression);
14588
- if (isNodeOfType(expression, "Literal")) {
14589
- if (typeof expression.value === "boolean") return !expression.value;
14590
- if (typeof expression.value === "string") return expression.value === "false";
14591
- return false;
14941
+ if (isNodeOfType(node, "JSXFragment")) {
14942
+ const children = node.children ?? [];
14943
+ for (const child of children) stack.push(child);
14944
+ continue;
14945
+ }
14946
+ if (isNodeOfType(node, "JSXExpressionContainer")) {
14947
+ const expression = node.expression;
14948
+ if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "Identifier") || isNodeOfType(expression, "MemberExpression")) return true;
14949
+ stack.push(expression);
14950
+ continue;
14951
+ }
14952
+ if (isNodeOfType(node, "ConditionalExpression")) {
14953
+ stack.push(node.consequent, node.alternate);
14954
+ continue;
14955
+ }
14956
+ if (isNodeOfType(node, "LogicalExpression")) {
14957
+ stack.push(node.left, node.right);
14958
+ continue;
14592
14959
  }
14593
- if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression) === "false";
14594
14960
  }
14595
14961
  return false;
14596
14962
  };
14597
- const noAutofocus = defineRule({
14598
- id: "no-autofocus",
14599
- title: "Autofocus on an element",
14600
- tags: ["react-jsx-only"],
14601
- severity: "warn",
14602
- recommendation: "Do not use `autoFocus`. It disorients users on load.",
14603
- category: "Accessibility",
14604
- create: (context) => {
14605
- const settings = resolveSettings$21(context.settings);
14606
- const isTestlikeFile = isTestlikeFilename(context.filename);
14607
- return { JSXOpeningElement(node) {
14608
- if (isTestlikeFile) return;
14609
- const autoFocusAttribute = node.attributes.find((attribute) => {
14610
- if (!isNodeOfType(attribute, "JSXAttribute")) return false;
14611
- const attributeName = attribute.name;
14612
- return isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "autoFocus";
14613
- });
14614
- if (!autoFocusAttribute) return;
14615
- const attributeValue = autoFocusAttribute.value;
14616
- if (attributeValue && isFalseAttributeValue(attributeValue)) return;
14617
- if (isSameNameIdentifierForward("autoFocus", attributeValue)) return;
14618
- if (settings.ignoreNonDOM) {
14619
- const tag = getElementType(node, context.settings);
14620
- if (!HTML_TAGS.has(tag)) return;
14621
- }
14622
- context.report({
14623
- node: autoFocusAttribute,
14624
- message: MESSAGE$28
14625
- });
14626
- } };
14627
- }
14628
- });
14629
14963
  //#endregion
14630
- //#region src/plugin/utils/create-relative-import-source.ts
14631
- const createRelativeImportSource = (filename, targetFilePath) => {
14632
- const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
14633
- const targetModulePath = path.basename(targetPathWithoutExtension) === "index" ? path.dirname(targetPathWithoutExtension) : targetPathWithoutExtension;
14634
- const relativePath = path.relative(path.dirname(filename), targetModulePath).split(path.sep).join("/");
14635
- return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
14964
+ //#region src/plugin/rules/correctness/no-array-index-as-key.ts
14965
+ const STRING_COERCION_FUNCTIONS = new Set(["String", "Number"]);
14966
+ const extractIndexName = (node) => {
14967
+ if (isNodeOfType(node, "Identifier") && INDEX_PARAMETER_NAMES.has(node.name)) return node.name;
14968
+ if (isNodeOfType(node, "TemplateLiteral")) {
14969
+ for (const expression of node.expressions ?? []) if (isNodeOfType(expression, "Identifier") && INDEX_PARAMETER_NAMES.has(expression.name)) return expression.name;
14970
+ }
14971
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && INDEX_PARAMETER_NAMES.has(node.callee.object.name) && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "toString") return node.callee.object.name;
14972
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && STRING_COERCION_FUNCTIONS.has(node.callee.name) && isNodeOfType(node.arguments?.[0], "Identifier") && INDEX_PARAMETER_NAMES.has(node.arguments[0].name)) return node.arguments[0].name;
14973
+ if (isNodeOfType(node, "BinaryExpression") && node.operator === "+" && (isNodeOfType(node.left, "Identifier") && INDEX_PARAMETER_NAMES.has(node.left.name) && isNodeOfType(node.right, "Literal") && node.right.value === "" || isNodeOfType(node.right, "Identifier") && INDEX_PARAMETER_NAMES.has(node.right.name) && isNodeOfType(node.left, "Literal") && node.left.value === "")) {
14974
+ if (isNodeOfType(node.left, "Identifier")) return node.left.name;
14975
+ if (isNodeOfType(node.right, "Identifier")) return node.right.name;
14976
+ return null;
14977
+ }
14978
+ return null;
14636
14979
  };
14637
- //#endregion
14638
- //#region src/plugin/utils/parse-export-specifiers.ts
14639
- const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
14640
- const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
14641
- const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
14642
- const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
14643
- const localName = getSpecifierName(rawLocalName ?? "");
14644
- return {
14645
- localName,
14646
- exportedName: getSpecifierName(rawExportedName ?? localName),
14647
- isTypeOnly
14648
- };
14649
- });
14650
- //#endregion
14651
- //#region src/plugin/utils/strip-js-comments.ts
14652
- const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
14653
- const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
14654
- const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
14655
- //#endregion
14656
- //#region src/plugin/utils/is-barrel-index-module.ts
14657
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
14658
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14659
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14660
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14661
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
14662
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
14663
- const createNonBarrelInfo = () => ({
14664
- isBarrel: false,
14665
- exportsByName: /* @__PURE__ */ new Map(),
14666
- starExportSources: []
14667
- });
14668
- const addImportedBinding = (importedBindings, binding) => {
14669
- importedBindings.set(binding.localName, {
14670
- ...binding,
14671
- didExport: false
14672
- });
14980
+ const isNumericLiteralOrUndefined = (node) => {
14981
+ if (!node) return false;
14982
+ if (isNodeOfType(node, "Literal") && typeof node.value === "number") return true;
14983
+ if (isNodeOfType(node, "Identifier") && node.name === "undefined") return true;
14984
+ return false;
14673
14985
  };
14674
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
14675
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
14676
- localName: specifier.exportedName,
14677
- importedName: specifier.localName,
14678
- source,
14679
- isTypeOnly: specifier.isTypeOnly
14680
- });
14986
+ const isArrayConstructorCallWithNumericLength = (node) => {
14987
+ if (!node) return false;
14988
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Array" && isNumericLiteralOrUndefined(node.arguments?.[0])) return true;
14989
+ if (isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Array" && isNumericLiteralOrUndefined(node.arguments?.[0])) return true;
14990
+ return false;
14681
14991
  };
14682
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
14683
- const trimmedImportClause = importClause.trim();
14684
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
14685
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
14686
- localName: namespaceMatch[1],
14687
- importedName: "*",
14688
- source,
14689
- isTypeOnly: declarationIsTypeOnly
14690
- });
14691
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
14692
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
14693
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
14694
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
14695
- localName: defaultImportName,
14696
- importedName: "default",
14697
- source,
14698
- isTypeOnly: declarationIsTypeOnly
14699
- });
14992
+ const isArrayFromCall = (node) => {
14993
+ if (!node) return false;
14994
+ if (!isNodeOfType(node, "CallExpression")) return false;
14995
+ const callee = node.callee;
14996
+ return Boolean(isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from");
14700
14997
  };
14701
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
14702
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
14703
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
14704
- return "";
14705
- });
14706
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
14707
- const isTypeOnly = Boolean(typeKeyword);
14708
- if (namespaceExportName) {
14709
- exportsByName.set(namespaceExportName, {
14710
- exportedName: namespaceExportName,
14711
- importedName: "*",
14712
- source,
14713
- isTypeOnly
14714
- });
14715
- return "";
14716
- }
14717
- if (specifiersText) {
14718
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
14719
- exportedName: specifier.exportedName,
14720
- importedName: specifier.localName,
14721
- source,
14722
- isTypeOnly: specifier.isTypeOnly
14723
- });
14724
- return "";
14725
- }
14726
- starExportSources.push(source);
14727
- return "";
14728
- });
14729
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1, (_match, typeKeyword, specifiersText) => {
14730
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
14731
- const importedBinding = importedBindings.get(specifier.localName);
14732
- if (!importedBinding) return _match;
14733
- importedBinding.didExport = true;
14734
- exportsByName.set(specifier.exportedName, {
14735
- exportedName: specifier.exportedName,
14736
- importedName: importedBinding.importedName,
14737
- source: importedBinding.source,
14738
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
14739
- });
14998
+ /**
14999
+ * True if every element of an ArrayExpression is a primitive constant
15000
+ * (number/string/boolean literal) — `[1, 2, 3]`, `['a', 'b']`. Such arrays
15001
+ * have a fixed order at every render, so an index key is stable.
15002
+ */
15003
+ /**
15004
+ * True if the call expression looks like a placeholder constructor whose
15005
+ * elements have no identity beyond their position — i.e. `Array.from(...)`,
15006
+ * `Array(N)`, `new Array(N)`, `Array(N).fill(...)`, `[...Array(N)]`, or
15007
+ * a literal-only array `[1, 2, 3]` / `['a', 'b']`.
15008
+ *
15009
+ * Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
15010
+ */
15011
+ const isStaticPlaceholderReceiver = (receiver) => {
15012
+ if (isArrayFromCall(receiver)) return true;
15013
+ if (isArrayConstructorCallWithNumericLength(receiver)) return true;
15014
+ if (isAllLiteralArrayExpression(receiver)) return true;
15015
+ if (isNodeOfType(receiver, "CallExpression")) {
15016
+ const callee = receiver.callee;
15017
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
15018
+ }
15019
+ if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
15020
+ const only = receiver.elements[0];
15021
+ if (only && isNodeOfType(only, "SpreadElement")) {
15022
+ const arg = only.argument;
15023
+ if (isArrayConstructorCallWithNumericLength(arg)) return true;
15024
+ if (isArrayFromCall(arg)) return true;
14740
15025
  }
14741
- return "";
14742
- });
14743
- return withoutKnownDeclarations;
15026
+ }
15027
+ return false;
14744
15028
  };
14745
- const hasUnexportedRuntimeImport = (importedBindings) => {
14746
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
15029
+ const isArrayFromLengthObjectCall = (node) => {
15030
+ if (!isArrayFromCall(node)) return false;
15031
+ if (!isNodeOfType(node, "CallExpression")) return false;
15032
+ const first = node.arguments?.[0];
15033
+ if (!first || !isNodeOfType(first, "ObjectExpression")) return false;
15034
+ for (const prop of first.properties ?? []) {
15035
+ if (!isNodeOfType(prop, "Property")) continue;
15036
+ const key = prop.key;
15037
+ if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
15038
+ if (isNumericLiteralOrUndefined(prop.value)) return true;
15039
+ if (isNodeOfType(prop.value, "Identifier")) return true;
15040
+ }
14747
15041
  return false;
14748
15042
  };
14749
- const classifyBarrelModule = (sourceText) => {
14750
- const strippedSource = stripJsComments(sourceText).trim();
14751
- if (!strippedSource) return createNonBarrelInfo();
14752
- const importedBindings = /* @__PURE__ */ new Map();
14753
- const exportsByName = /* @__PURE__ */ new Map();
14754
- const starExportSources = [];
14755
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
14756
- return {
14757
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
14758
- exportsByName,
14759
- starExportSources
14760
- };
15043
+ const isInsideStaticPlaceholderMap = (node) => {
15044
+ let current = node;
15045
+ while (current.parent) {
15046
+ const parent = current.parent;
15047
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
15048
+ const callee = parent.callee;
15049
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
15050
+ if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
15051
+ }
15052
+ current = parent;
15053
+ }
15054
+ return false;
14761
15055
  };
14762
- const getBarrelIndexModuleInfo = (filePath) => {
14763
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
14764
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
14765
- if (cachedResult !== void 0) return cachedResult;
14766
- let moduleInfo = createNonBarrelInfo();
14767
- try {
14768
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
14769
- } catch {
14770
- moduleInfo = createNonBarrelInfo();
15056
+ /**
15057
+ * Walk up from a JSXAttribute node looking for the enclosing iterator
15058
+ * callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
15059
+ * and return the first parameter's name. The first param is the per-item
15060
+ * value, e.g. `item` in `arr.map((item, index) => …)`.
15061
+ */
15062
+ const findIteratorItemName$1 = (node) => {
15063
+ let current = node;
15064
+ while (current.parent) {
15065
+ const parent = current.parent;
15066
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
15067
+ const callee = parent.callee;
15068
+ const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
15069
+ const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
15070
+ if (isIteratorMethodCall || isArrayFromCallback) {
15071
+ const first = (current.params ?? [])[0];
15072
+ if (first && isNodeOfType(first, "Identifier")) return first.name;
15073
+ return null;
15074
+ }
15075
+ }
15076
+ current = parent;
14771
15077
  }
14772
- barrelIndexModuleInfoCache.set(filePath, moduleInfo);
14773
- return moduleInfo;
15078
+ return null;
14774
15079
  };
14775
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
15080
+ const templateLiteralHasIteratorIdentity = (template, itemName) => {
15081
+ for (const expression of template.expressions ?? []) {
15082
+ if (isNodeOfType(expression, "Identifier") && expression.name === itemName) return true;
15083
+ if (isNodeOfType(expression, "MemberExpression") && isNodeOfType(expression.object, "Identifier") && expression.object.name === itemName) return true;
15084
+ }
15085
+ return false;
15086
+ };
15087
+ /**
15088
+ * True when the JSX key value is a template literal mixing an index with at
15089
+ * least one stable per-item identifier (e.g. `${item.id}-${index}`). Common
15090
+ * defensive pattern in user code — the index is just a uniqueness fallback,
15091
+ * the real identity is `item.id`.
15092
+ */
15093
+ const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
15094
+ if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
15095
+ if ((keyExpression.expressions ?? []).length < 2) return false;
15096
+ const itemName = findIteratorItemName$1(attributeNode);
15097
+ if (!itemName) return false;
15098
+ return templateLiteralHasIteratorIdentity(keyExpression, itemName);
15099
+ };
15100
+ const noArrayIndexAsKey = defineRule({
15101
+ id: "no-array-index-as-key",
15102
+ title: "Array index used as a key",
15103
+ severity: "warn",
15104
+ recommendation: "Use a stable id from the item, like `key={item.id}` or `key={item.slug}`. Index keys break when the list reorders or filters.",
15105
+ create: (context) => ({ JSXAttribute(node) {
15106
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "key") return;
15107
+ if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
15108
+ const indexName = extractIndexName(node.value.expression);
15109
+ if (!indexName) return;
15110
+ if (isInsideStaticPlaceholderMap(node)) return;
15111
+ if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
15112
+ const openingElement = node.parent;
15113
+ if (openingElement && isNodeOfType(openingElement, "JSXOpeningElement")) {
15114
+ const elementName = openingElement.name;
15115
+ if (isNodeOfType(elementName, "JSXIdentifier")) {
15116
+ if (elementName.name === "Fragment") return;
15117
+ if (PURE_SVG_PRIMITIVE_TAGS.has(elementName.name)) return;
15118
+ if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
15119
+ const jsxElement = openingElement.parent;
15120
+ if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
15121
+ if (!containsStatefulDescendant(jsxElement)) return;
15122
+ }
15123
+ }
15124
+ }
15125
+ if (isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && isNodeOfType(elementName.property, "JSXIdentifier") && elementName.object.name === "React" && elementName.property.name === "Fragment") return;
15126
+ }
15127
+ context.report({
15128
+ node,
15129
+ message: `Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like \`key={item.id}\`, not the array index "${indexName}".`
15130
+ });
15131
+ } })
15132
+ });
14776
15133
  //#endregion
14777
- //#region src/plugin/utils/does-module-export-name.ts
14778
- const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
14779
- const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
14780
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14781
- const doesSourceTextExportName = (sourceText, exportedName) => {
14782
- const strippedSource = stripJsComments(sourceText);
14783
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
14784
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
14785
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
15134
+ //#region src/plugin/rules/react-builtins/no-array-index-key.ts
15135
+ const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
15136
+ const SECOND_INDEX_METHODS = new Set([
15137
+ "every",
15138
+ "filter",
15139
+ "find",
15140
+ "findIndex",
15141
+ "flatMap",
15142
+ "forEach",
15143
+ "map",
15144
+ "some"
15145
+ ]);
15146
+ const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
15147
+ const isPositionallyStableIterationReceiver = (receiver) => {
15148
+ if (isAllLiteralArrayExpression(receiver)) return true;
15149
+ if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
15150
+ const only = receiver.elements[0];
15151
+ if (only && isNodeOfType(only, "SpreadElement")) {
15152
+ const arg = only.argument;
15153
+ if (arg && isPositionallyStableIterationReceiver(arg)) return true;
15154
+ }
15155
+ }
15156
+ if (!isNodeOfType(receiver, "CallExpression")) return false;
15157
+ const callee = receiver.callee;
15158
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
15159
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
15160
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
15161
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
14786
15162
  return false;
14787
15163
  };
14788
- const doesModuleExportName = (filePath, exportedName) => {
14789
- try {
14790
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
14791
- } catch {
15164
+ const templateHasIteratorMember = (templateLiteral, iteratorName) => {
15165
+ for (const expression of templateLiteral.expressions ?? []) {
15166
+ if (isNodeOfType(expression, "Identifier") && expression.name === iteratorName) return true;
15167
+ if (isNodeOfType(expression, "MemberExpression") && isNodeOfType(expression.object, "Identifier") && expression.object.name === iteratorName) return true;
15168
+ }
15169
+ return false;
15170
+ };
15171
+ const isArrayFromMapperCallback = (parentCall, callback) => {
15172
+ if (parentCall.arguments[1] !== callback) return false;
15173
+ const callee = parentCall.callee;
15174
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
15175
+ };
15176
+ const isArrayFromSourcePositionallyStable = (source) => {
15177
+ if (isNodeOfType(source, "ObjectExpression")) {
15178
+ for (const property of source.properties ?? []) {
15179
+ if (!isNodeOfType(property, "Property")) continue;
15180
+ const key = property.key;
15181
+ if (isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length") return true;
15182
+ }
14792
15183
  return false;
14793
15184
  }
15185
+ return isPositionallyStableIterationReceiver(source);
14794
15186
  };
14795
- //#endregion
14796
- //#region src/plugin/utils/resolve-relative-import-path.ts
14797
- const MODULE_FILE_EXTENSIONS = [
14798
- ".ts",
14799
- ".tsx",
14800
- ".js",
14801
- ".jsx",
14802
- ".mjs",
14803
- ".cjs",
14804
- ".mts",
14805
- ".cts"
14806
- ];
14807
- const PACKAGE_EXPORT_CONDITIONS = [
14808
- "import",
14809
- "default",
14810
- "module",
14811
- "browser",
14812
- "require"
14813
- ];
14814
- const PACKAGE_ENTRY_FIELDS = [
14815
- "module",
14816
- "main",
14817
- "browser"
14818
- ];
14819
- const getExistingFilePath = (filePath) => {
14820
- try {
14821
- return fs.statSync(filePath).isFile() ? filePath : null;
14822
- } catch {
14823
- return null;
15187
+ const findIteratorItemName = (node) => {
15188
+ let current = node;
15189
+ while (current) {
15190
+ if (isNodeOfType(current, "ArrowFunctionExpression") || isNodeOfType(current, "FunctionExpression")) {
15191
+ const parent = current.parent;
15192
+ const callbackItemName = readIteratorItemFromCallback(current, parent);
15193
+ if (callbackItemName !== void 0) return callbackItemName;
15194
+ if (current.params.length > 0) return null;
15195
+ }
15196
+ current = current.parent ?? null;
15197
+ }
15198
+ return null;
15199
+ };
15200
+ const readIteratorItemFromCallback = (callback, parent) => {
15201
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return void 0;
15202
+ const callee = parent.callee;
15203
+ if (parent.arguments[0] === callback && isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
15204
+ const methodName = callee.property.name;
15205
+ if (SECOND_INDEX_METHODS.has(methodName)) {
15206
+ const item = callback.params[0];
15207
+ return item && isNodeOfType(item, "Identifier") ? item.name : null;
15208
+ }
15209
+ if (THIRD_INDEX_METHODS.has(methodName)) {
15210
+ const item = callback.params[1];
15211
+ return item && isNodeOfType(item, "Identifier") ? item.name : null;
15212
+ }
15213
+ }
15214
+ if (isArrayFromMapperCallback(parent, callback)) {
15215
+ const item = callback.params[0];
15216
+ return item && isNodeOfType(item, "Identifier") ? item.name : null;
14824
15217
  }
14825
15218
  };
14826
- const getExistingDirectoryPath = (directoryPath) => {
14827
- try {
14828
- return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
14829
- } catch {
14830
- return null;
15219
+ const findIndexParameterBinding = (node) => {
15220
+ let current = node.parent;
15221
+ while (current) {
15222
+ if (isNodeOfType(current, "ArrowFunctionExpression") || isNodeOfType(current, "FunctionExpression")) {
15223
+ const indexParam = readIteratorIndexFromCallback(current, current.parent);
15224
+ if (indexParam !== void 0) return indexParam;
15225
+ if (current.params.length > 0) return null;
15226
+ }
15227
+ current = current.parent ?? null;
14831
15228
  }
15229
+ return null;
14832
15230
  };
14833
- const getModuleFilePathCandidates = (modulePath) => {
14834
- const extension = path.extname(modulePath);
14835
- if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
14836
- const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
14837
- if (extension === ".js") return [
14838
- modulePath,
14839
- `${modulePathWithoutExtension}.ts`,
14840
- `${modulePathWithoutExtension}.tsx`,
14841
- `${modulePathWithoutExtension}.jsx`
14842
- ];
14843
- if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
14844
- if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
14845
- if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
14846
- return [modulePath];
14847
- };
14848
- const isObjectRecord = (value) => typeof value === "object" && value !== null;
14849
- const getConditionalExportEntry = (exportEntry) => {
14850
- if (typeof exportEntry === "string") return exportEntry;
14851
- if (Array.isArray(exportEntry)) {
14852
- for (const fallbackEntry of exportEntry) {
14853
- const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
14854
- if (resolvedFallbackEntry) return resolvedFallbackEntry;
15231
+ const readIteratorIndexFromCallback = (callback, parent) => {
15232
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return void 0;
15233
+ const callee = parent.callee;
15234
+ if (parent.arguments[0] === callback && isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
15235
+ const methodName = callee.property.name;
15236
+ let indexParamPosition = null;
15237
+ if (SECOND_INDEX_METHODS.has(methodName)) indexParamPosition = 1;
15238
+ else if (THIRD_INDEX_METHODS.has(methodName)) indexParamPosition = 2;
15239
+ if (indexParamPosition !== null) {
15240
+ const receiver = callee.object;
15241
+ if (isPositionallyStableIterationReceiver(receiver)) return null;
15242
+ const indexParam = callback.params[indexParamPosition];
15243
+ return indexParam && isNodeOfType(indexParam, "Identifier") ? indexParam : null;
14855
15244
  }
14856
- return null;
14857
15245
  }
14858
- if (!isObjectRecord(exportEntry)) return null;
14859
- for (const condition of PACKAGE_EXPORT_CONDITIONS) {
14860
- const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
14861
- if (nestedEntry) return nestedEntry;
15246
+ if (isArrayFromMapperCallback(parent, callback)) {
15247
+ const source = parent.arguments[0];
15248
+ if (source && isArrayFromSourcePositionallyStable(source)) return null;
15249
+ const indexParam = callback.params[1];
15250
+ return indexParam && isNodeOfType(indexParam, "Identifier") ? indexParam : null;
14862
15251
  }
14863
- return null;
14864
- };
14865
- const getPackageExportEntry = (packageJson) => {
14866
- const exportsField = packageJson.exports;
14867
- if (!exportsField) return null;
14868
- const directExportEntry = getConditionalExportEntry(exportsField);
14869
- if (directExportEntry) return directExportEntry;
14870
- if (!isObjectRecord(exportsField)) return null;
14871
- return getConditionalExportEntry(exportsField["."]);
14872
- };
14873
- const resolveModulePathWithIndexFallback = (modulePath) => {
14874
- const filePath = resolveModuleFilePath(modulePath);
14875
- if (filePath) return filePath;
14876
- return resolveModuleFilePath(path.join(modulePath, "index"));
14877
15252
  };
14878
- const resolvePackageDirectoryEntry = (directoryPath) => {
14879
- const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
14880
- if (!existingDirectoryPath) return null;
14881
- const packageJsonPath = path.join(existingDirectoryPath, "package.json");
14882
- try {
14883
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
14884
- const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
14885
- if (!packageEntry) return null;
14886
- return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
14887
- } catch {
14888
- return null;
15253
+ const isIndexReference = (expression, paramName) => isNodeOfType(expression, "Identifier") && expression.name === paramName;
15254
+ const expressionUsesIndex = (expression, paramName) => {
15255
+ if (isIndexReference(expression, paramName)) return true;
15256
+ if (isNodeOfType(expression, "TemplateLiteral")) return expression.expressions.some((innerExpression) => isIndexReference(innerExpression, paramName));
15257
+ if (isNodeOfType(expression, "BinaryExpression")) {
15258
+ const usesInLeft = isIndexReference(expression.left, paramName);
15259
+ const usesInRight = isIndexReference(expression.right, paramName);
15260
+ if (usesInLeft || usesInRight) return true;
15261
+ if (isNodeOfType(expression.left, "BinaryExpression") && expressionUsesIndex(expression.left, paramName)) return true;
15262
+ if (isNodeOfType(expression.right, "BinaryExpression") && expressionUsesIndex(expression.right, paramName)) return true;
15263
+ return false;
14889
15264
  }
14890
- };
14891
- const resolveModuleFilePath = (modulePath) => {
14892
- const exactFilePath = getExistingFilePath(modulePath);
14893
- if (exactFilePath) return exactFilePath;
14894
- for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
14895
- const filePath = getExistingFilePath(candidateFilePath);
14896
- if (filePath) return filePath;
15265
+ if (isNodeOfType(expression, "CallExpression")) {
15266
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
15267
+ if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
14897
15268
  }
14898
- return null;
15269
+ return false;
14899
15270
  };
14900
- const resolveRelativeImportPath = (filename, source) => {
14901
- const importPath = path.resolve(path.dirname(filename), source);
14902
- const directFilePath = resolveModuleFilePath(importPath);
14903
- if (directFilePath) return directFilePath;
14904
- const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
14905
- if (packageEntryFilePath) return packageEntryFilePath;
14906
- return resolveModuleFilePath(path.join(importPath, "index"));
15271
+ const isReactCloneElement = (callExpression) => {
15272
+ const callee = callExpression.callee;
15273
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
15274
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
15275
+ if (callee.property.name !== "cloneElement") return false;
15276
+ return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
14907
15277
  };
15278
+ const isPureSvgPrimitiveJsxName = (jsxOpeningName) => isNodeOfType(jsxOpeningName, "JSXIdentifier") && PURE_SVG_PRIMITIVE_TAGS.has(jsxOpeningName.name);
15279
+ const isStatelessLeafJsxName = (jsxOpeningName) => isNodeOfType(jsxOpeningName, "JSXIdentifier") && STATELESS_HTML_LEAF_TAGS.has(jsxOpeningName.name);
15280
+ const isFragmentJsxName = (jsxOpeningName) => {
15281
+ if (isNodeOfType(jsxOpeningName, "JSXIdentifier")) return jsxOpeningName.name === "Fragment";
15282
+ if (isNodeOfType(jsxOpeningName, "JSXMemberExpression") && isNodeOfType(jsxOpeningName.object, "JSXIdentifier") && isNodeOfType(jsxOpeningName.property, "JSXIdentifier") && jsxOpeningName.object.name === "React" && jsxOpeningName.property.name === "Fragment") return true;
15283
+ return false;
15284
+ };
15285
+ const noArrayIndexKey = defineRule({
15286
+ id: "no-array-index-key",
15287
+ title: "Array index used as a key",
15288
+ severity: "warn",
15289
+ defaultEnabled: false,
15290
+ recommendation: "Use a stable `key` from your data instead of the array index.",
15291
+ category: "Performance",
15292
+ create: (context) => ({
15293
+ JSXOpeningElement(node) {
15294
+ const keyAttribute = hasJsxPropIgnoreCase(node.attributes, "key");
15295
+ if (!keyAttribute) return;
15296
+ if (!keyAttribute.value || !isNodeOfType(keyAttribute.value, "JSXExpressionContainer")) return;
15297
+ const expression = keyAttribute.value.expression;
15298
+ if (expression.type === "JSXEmptyExpression") return;
15299
+ if (isFragmentJsxName(node.name)) return;
15300
+ if (isPureSvgPrimitiveJsxName(node.name)) return;
15301
+ const indexBinding = findIndexParameterBinding(node);
15302
+ if (!indexBinding) return;
15303
+ if (!expressionUsesIndex(expression, indexBinding.name)) return;
15304
+ if (isNodeOfType(expression, "TemplateLiteral")) {
15305
+ const itemName = findIteratorItemName(node);
15306
+ if (itemName && templateHasIteratorMember(expression, itemName)) return;
15307
+ const interpolations = expression.expressions ?? [];
15308
+ let interpolationsBeyondIndex = 0;
15309
+ for (const interpolation of interpolations) {
15310
+ if (isIndexReference(interpolation, indexBinding.name)) continue;
15311
+ interpolationsBeyondIndex += 1;
15312
+ if (interpolationsBeyondIndex >= 1) break;
15313
+ }
15314
+ if (interpolationsBeyondIndex >= 1) return;
15315
+ }
15316
+ if (isNodeOfType(expression, "BinaryExpression") && expression.operator === "+") {
15317
+ let interpolationsBeyondIndex = 0;
15318
+ const walkOperand = (operand) => {
15319
+ if (isNodeOfType(operand, "BinaryExpression") && operand.operator === "+") {
15320
+ walkOperand(operand.left);
15321
+ walkOperand(operand.right);
15322
+ return;
15323
+ }
15324
+ if (isIndexReference(operand, indexBinding.name)) return;
15325
+ if (isNodeOfType(operand, "Literal") && typeof operand.value === "string") return;
15326
+ interpolationsBeyondIndex += 1;
15327
+ };
15328
+ walkOperand(expression);
15329
+ if (interpolationsBeyondIndex >= 1) return;
15330
+ }
15331
+ if (isStatelessLeafJsxName(node.name)) {
15332
+ const jsxElement = node.parent;
15333
+ if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
15334
+ if (!containsStatefulDescendant(jsxElement)) return;
15335
+ }
15336
+ }
15337
+ context.report({
15338
+ node: keyAttribute,
15339
+ message: MESSAGE$29
15340
+ });
15341
+ },
15342
+ CallExpression(node) {
15343
+ if (!isReactCloneElement(node)) return;
15344
+ if (node.arguments.length < 2 || node.arguments.length > 3) return;
15345
+ const propsArgument = node.arguments[1];
15346
+ if (!isNodeOfType(propsArgument, "ObjectExpression")) return;
15347
+ const indexBinding = findIndexParameterBinding(node);
15348
+ if (!indexBinding) return;
15349
+ for (const property of propsArgument.properties) {
15350
+ if (!isNodeOfType(property, "Property")) continue;
15351
+ if (property.computed) continue;
15352
+ const propKey = property.key;
15353
+ let propName = null;
15354
+ if (isNodeOfType(propKey, "Identifier")) propName = propKey.name;
15355
+ else if (isNodeOfType(propKey, "Literal") && typeof propKey.value === "string") propName = propKey.value;
15356
+ if (propName !== "key") continue;
15357
+ if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
15358
+ node: property,
15359
+ message: MESSAGE$29
15360
+ });
15361
+ }
15362
+ }
15363
+ })
15364
+ });
14908
15365
  //#endregion
14909
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
14910
- const getUniqueFilePath = (filePaths) => {
14911
- const uniqueFilePaths = new Set(filePaths);
14912
- if (uniqueFilePaths.size !== 1) return null;
14913
- const [filePath] = uniqueFilePaths;
14914
- return filePath ?? null;
15366
+ //#region src/plugin/rules/a11y/no-autofocus.ts
15367
+ const MESSAGE$28 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
15368
+ const resolveSettings$21 = (settings) => {
15369
+ const reactDoctor = settings?.["react-doctor"];
15370
+ return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
14915
15371
  };
14916
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
14917
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
14918
- if (!resolvedTargetPath) return null;
14919
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
14920
- if (nestedTargetPath) return nestedTargetPath;
14921
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
15372
+ const innerExpression = (expression) => {
15373
+ if (expression.type === "ParenthesizedExpression" && "expression" in expression) return innerExpression(expression.expression);
15374
+ return expression;
14922
15375
  };
14923
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
14924
- if (visitedFilePaths.has(barrelFilePath)) return null;
14925
- visitedFilePaths.add(barrelFilePath);
14926
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
14927
- if (!moduleInfo.isBarrel) return null;
14928
- const target = moduleInfo.exportsByName.get(exportedName);
14929
- if (target) {
14930
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
14931
- if (!resolvedTargetPath) return null;
14932
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
15376
+ const isSameNameIdentifierForward = (attributeName, value) => {
15377
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
15378
+ const expression = innerExpression(value.expression);
15379
+ if (isNodeOfType(expression, "Identifier") && expression.name === attributeName) return true;
15380
+ if (isNodeOfType(expression, "MemberExpression") && !expression.computed && isNodeOfType(expression.property, "Identifier") && expression.property.name === attributeName) return true;
15381
+ return false;
15382
+ };
15383
+ const isFalseAttributeValue = (value) => {
15384
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value === "false" : value.value === false;
15385
+ if (isNodeOfType(value, "JSXExpressionContainer")) {
15386
+ const expression = innerExpression(value.expression);
15387
+ if (isNodeOfType(expression, "Literal")) {
15388
+ if (typeof expression.value === "boolean") return !expression.value;
15389
+ if (typeof expression.value === "string") return expression.value === "false";
15390
+ return false;
15391
+ }
15392
+ if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression) === "false";
15393
+ }
15394
+ return false;
15395
+ };
15396
+ const noAutofocus = defineRule({
15397
+ id: "no-autofocus",
15398
+ title: "Autofocus on an element",
15399
+ tags: ["react-jsx-only"],
15400
+ severity: "warn",
15401
+ recommendation: "Do not use `autoFocus`. It disorients users on load.",
15402
+ category: "Accessibility",
15403
+ create: (context) => {
15404
+ const settings = resolveSettings$21(context.settings);
15405
+ const isTestlikeFile = isTestlikeFilename(context.filename);
15406
+ return { JSXOpeningElement(node) {
15407
+ if (isTestlikeFile) return;
15408
+ const autoFocusAttribute = node.attributes.find((attribute) => {
15409
+ if (!isNodeOfType(attribute, "JSXAttribute")) return false;
15410
+ const attributeName = attribute.name;
15411
+ return isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "autoFocus";
15412
+ });
15413
+ if (!autoFocusAttribute) return;
15414
+ const attributeValue = autoFocusAttribute.value;
15415
+ if (attributeValue && isFalseAttributeValue(attributeValue)) return;
15416
+ if (isSameNameIdentifierForward("autoFocus", attributeValue)) return;
15417
+ if (settings.ignoreNonDOM) {
15418
+ const tag = getElementType(node, context.settings);
15419
+ if (!HTML_TAGS.has(tag)) return;
15420
+ }
15421
+ context.report({
15422
+ node: autoFocusAttribute,
15423
+ message: MESSAGE$28
15424
+ });
15425
+ } };
14933
15426
  }
14934
- if (exportedName === "default") return null;
14935
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
15427
+ });
15428
+ //#endregion
15429
+ //#region src/plugin/utils/create-relative-import-source.ts
15430
+ const createRelativeImportSource = (filename, targetFilePath) => {
15431
+ const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
15432
+ const targetModulePath = path.basename(targetPathWithoutExtension) === "index" ? path.dirname(targetPathWithoutExtension) : targetPathWithoutExtension;
15433
+ const relativePath = path.relative(path.dirname(filename), targetModulePath).split(path.sep).join("/");
15434
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
14936
15435
  };
14937
15436
  //#endregion
14938
15437
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
@@ -19279,224 +19778,7 @@ const isLodashMutatorCall = (callExpression) => {
19279
19778
  return false;
19280
19779
  };
19281
19780
  //#endregion
19282
- //#region src/plugin/utils/find-exported-function-body.ts
19283
- const isFunctionLike = (node) => {
19284
- if (!node) return false;
19285
- return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
19286
- };
19287
- const findExportedFunctionBody = (programRoot, exportedName) => {
19288
- if (!isNodeOfType(programRoot, "Program")) return null;
19289
- const localBindings = /* @__PURE__ */ new Map();
19290
- const namedExports = /* @__PURE__ */ new Map();
19291
- let defaultExport = null;
19292
- let defaultExportIdentifierName = null;
19293
- const recordVariableDeclaration = (declaration) => {
19294
- for (const declarator of declaration.declarations ?? []) {
19295
- if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
19296
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
19297
- const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
19298
- if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
19299
- }
19300
- };
19301
- for (const statement of programRoot.body ?? []) {
19302
- if (isNodeOfType(statement, "VariableDeclaration")) {
19303
- recordVariableDeclaration(statement);
19304
- continue;
19305
- }
19306
- if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
19307
- localBindings.set(statement.id.name, statement);
19308
- continue;
19309
- }
19310
- if (isNodeOfType(statement, "ExportNamedDeclaration")) {
19311
- const declaration = statement.declaration;
19312
- if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
19313
- recordVariableDeclaration(declaration);
19314
- for (const declarator of declaration.declarations ?? []) {
19315
- if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
19316
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
19317
- namedExports.set(declarator.id.name, declarator.id.name);
19318
- }
19319
- } else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
19320
- localBindings.set(declaration.id.name, declaration);
19321
- namedExports.set(declaration.id.name, declaration.id.name);
19322
- }
19323
- for (const specifier of statement.specifiers ?? []) {
19324
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
19325
- const local = specifier.local;
19326
- const exported = specifier.exported;
19327
- if (!isNodeOfType(local, "Identifier")) continue;
19328
- const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
19329
- if (!exportedNameSpec) continue;
19330
- namedExports.set(exportedNameSpec, local.name);
19331
- }
19332
- continue;
19333
- }
19334
- if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
19335
- const declaration = statement.declaration;
19336
- if (!declaration) continue;
19337
- if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
19338
- localBindings.set(declaration.id.name, declaration);
19339
- defaultExport = declaration;
19340
- continue;
19341
- }
19342
- if (isFunctionLike(declaration)) {
19343
- defaultExport = declaration;
19344
- continue;
19345
- }
19346
- if (isNodeOfType(declaration, "Identifier")) {
19347
- defaultExportIdentifierName = declaration.name;
19348
- continue;
19349
- }
19350
- }
19351
- }
19352
- if (exportedName === "default") {
19353
- if (defaultExport) return defaultExport;
19354
- if (defaultExportIdentifierName) {
19355
- const binding = localBindings.get(defaultExportIdentifierName);
19356
- if (binding) return binding;
19357
- }
19358
- }
19359
- const localName = namedExports.get(exportedName);
19360
- if (!localName) return null;
19361
- return localBindings.get(localName) ?? null;
19362
- };
19363
- const resolveImportedExportName = (importSpecifier) => {
19364
- if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
19365
- const imported = importSpecifier.imported;
19366
- if (isNodeOfType(imported, "Identifier")) return imported.name;
19367
- if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
19368
- return null;
19369
- }
19370
- if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
19371
- return null;
19372
- };
19373
- const findReExportSourcesForName = (programRoot, exportedName) => {
19374
- if (!isNodeOfType(programRoot, "Program")) return [];
19375
- const exportAllSources = [];
19376
- for (const statement of programRoot.body ?? []) {
19377
- if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
19378
- const sourceValue = statement.source.value;
19379
- if (typeof sourceValue !== "string") continue;
19380
- for (const specifier of statement.specifiers ?? []) {
19381
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
19382
- const exported = specifier.exported;
19383
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
19384
- }
19385
- }
19386
- if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
19387
- const sourceValue = statement.source.value;
19388
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
19389
- }
19390
- }
19391
- return exportAllSources;
19392
- };
19393
- //#endregion
19394
- //#region src/plugin/utils/attach-parent-references.ts
19395
- const attachParentReferences = (root) => {
19396
- const visit = (node, parent) => {
19397
- const writableNode = node;
19398
- writableNode.parent = parent;
19399
- const nodeRecord = node;
19400
- for (const key of Object.keys(nodeRecord)) {
19401
- if (key === "parent") continue;
19402
- const child = nodeRecord[key];
19403
- if (Array.isArray(child)) {
19404
- for (const item of child) if (isAstNode(item)) visit(item, node);
19405
- } else if (isAstNode(child)) visit(child, node);
19406
- }
19407
- };
19408
- visit(root, null);
19409
- };
19410
- //#endregion
19411
- //#region src/plugin/utils/parse-source-file.ts
19412
- const FILENAME_TO_LANG = {
19413
- ".ts": "ts",
19414
- ".tsx": "tsx",
19415
- ".js": "js",
19416
- ".jsx": "jsx",
19417
- ".mjs": "js",
19418
- ".cjs": "js",
19419
- ".mts": "ts",
19420
- ".cts": "ts"
19421
- };
19422
- const resolveLang = (filename) => {
19423
- return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
19424
- };
19425
- const parseCache = /* @__PURE__ */ new Map();
19426
- const parseSourceFile = (absoluteFilePath) => {
19427
- let fileStat;
19428
- try {
19429
- fileStat = fs.statSync(absoluteFilePath);
19430
- } catch {
19431
- return null;
19432
- }
19433
- if (!fileStat.isFile()) return null;
19434
- if (fileStat.size > 2e6) return null;
19435
- const cached = parseCache.get(absoluteFilePath);
19436
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
19437
- if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
19438
- parseCache.set(absoluteFilePath, {
19439
- mtimeMs: fileStat.mtimeMs,
19440
- size: fileStat.size,
19441
- program: null
19442
- });
19443
- return null;
19444
- }
19445
- let sourceText;
19446
- try {
19447
- sourceText = fs.readFileSync(absoluteFilePath, "utf8");
19448
- } catch {
19449
- parseCache.set(absoluteFilePath, {
19450
- mtimeMs: fileStat.mtimeMs,
19451
- size: fileStat.size,
19452
- program: null
19453
- });
19454
- return null;
19455
- }
19456
- let parsedProgram = null;
19457
- try {
19458
- const result = parseSync(absoluteFilePath, sourceText, {
19459
- astType: "ts",
19460
- lang: resolveLang(absoluteFilePath)
19461
- });
19462
- if (!result.errors.some((parseError) => parseError.severity === "Error")) {
19463
- parsedProgram = result.program;
19464
- attachParentReferences(parsedProgram);
19465
- }
19466
- } catch {
19467
- parsedProgram = null;
19468
- }
19469
- parseCache.set(absoluteFilePath, {
19470
- mtimeMs: fileStat.mtimeMs,
19471
- size: fileStat.size,
19472
- program: parsedProgram
19473
- });
19474
- return parsedProgram;
19475
- };
19476
- //#endregion
19477
19781
  //#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
19478
- const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
19479
- if (visitedFilePaths.size >= 4) return null;
19480
- if (visitedFilePaths.has(filePath)) return null;
19481
- visitedFilePaths.add(filePath);
19482
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
19483
- const programRoot = parseSourceFile(actualFilePath);
19484
- if (!programRoot) return null;
19485
- const exported = findExportedFunctionBody(programRoot, exportedName);
19486
- if (exported) return exported;
19487
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
19488
- const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
19489
- if (!nextFilePath) continue;
19490
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
19491
- if (resolved) return resolved;
19492
- }
19493
- return null;
19494
- };
19495
- const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
19496
- const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
19497
- if (!resolvedFilePath) return null;
19498
- return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
19499
- };
19500
19782
  const resolveReducerFunction = (node, currentFilename) => {
19501
19783
  if (!node) return null;
19502
19784
  const unwrappedNode = stripParenExpression(node);
@@ -19518,7 +19800,6 @@ const resolveReducerFunction = (node, currentFilename) => {
19518
19800
  if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
19519
19801
  const sourceValue = importDeclaration.source?.value;
19520
19802
  if (typeof sourceValue !== "string") return null;
19521
- if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
19522
19803
  const exportedName = resolveImportedExportName(initializer);
19523
19804
  if (!exportedName) return null;
19524
19805
  const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);