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