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.
- package/dist/index.js +1928 -1645
- 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/
|
|
13370
|
-
const
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
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
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
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
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
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
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
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
|
-
|
|
13463
|
-
|
|
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
|
|
13472
|
-
|
|
13473
|
-
|
|
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
|
-
|
|
13476
|
-
|
|
13477
|
-
const
|
|
13478
|
-
const
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
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
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13507
|
-
|
|
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
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
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
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
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
|
-
|
|
13524
|
-
restoreParents(restorations);
|
|
13531
|
+
return null;
|
|
13525
13532
|
}
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
}
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
13535
|
-
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
13539
|
-
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
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
|
-
|
|
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/
|
|
13551
|
-
const
|
|
13552
|
-
const
|
|
13553
|
-
|
|
13554
|
-
const
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
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
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
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
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
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
|
|
13589
|
-
const
|
|
13590
|
-
|
|
13591
|
-
|
|
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
|
|
13649
|
-
const
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
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
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
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
|
-
|
|
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
|
|
13675
|
-
const
|
|
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
|
|
13689
|
-
|
|
13690
|
-
if (!
|
|
13691
|
-
const
|
|
13692
|
-
|
|
13693
|
-
const
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
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
|
|
13723
|
-
if (!
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
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
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
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
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
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
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
if (
|
|
13766
|
-
|
|
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
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
if (
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
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
|
|
13784
|
-
const
|
|
13785
|
-
if (!
|
|
13786
|
-
|
|
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
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
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
|
|
13795
|
-
const
|
|
13796
|
-
if (!
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
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
|
|
13841
|
-
const
|
|
13842
|
-
if (
|
|
13843
|
-
const
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
}
|
|
13847
|
-
|
|
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
|
|
13860
|
-
const
|
|
13861
|
-
|
|
13862
|
-
const
|
|
13863
|
-
|
|
13864
|
-
|
|
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
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
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
|
-
|
|
13879
|
-
return false;
|
|
13915
|
+
return output.replace(/,(\s*[}\]])/g, "$1");
|
|
13880
13916
|
};
|
|
13881
|
-
const
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
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
|
|
13896
|
-
const
|
|
13897
|
-
if (
|
|
13898
|
-
|
|
13899
|
-
|
|
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
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
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/
|
|
13909
|
-
const
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
const
|
|
13923
|
-
if (!
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
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/
|
|
13940
|
-
const
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
if (
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
|
|
13958
|
-
|
|
13959
|
-
|
|
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
|
|
14106
|
+
return false;
|
|
13990
14107
|
};
|
|
13991
14108
|
//#endregion
|
|
13992
|
-
//#region src/plugin/
|
|
13993
|
-
const
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
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
|
-
|
|
14164
|
-
|
|
14165
|
-
const
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
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
|
|
14140
|
+
return names;
|
|
14179
14141
|
};
|
|
14180
|
-
const
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
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
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
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
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
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
|
|
14305
|
-
create: (context) => ({
|
|
14306
|
-
if (
|
|
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:
|
|
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/
|
|
14335
|
-
const MESSAGE$
|
|
14336
|
-
const
|
|
14337
|
-
|
|
14338
|
-
"
|
|
14339
|
-
"
|
|
14340
|
-
"
|
|
14341
|
-
"
|
|
14342
|
-
"
|
|
14343
|
-
"
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
const
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
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
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
}
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
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
|
|
14372
|
-
|
|
14373
|
-
|
|
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
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
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
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14326
|
+
const analysis = {
|
|
14327
|
+
programNode,
|
|
14328
|
+
scopeManager
|
|
14329
|
+
};
|
|
14330
|
+
programToAnalysis.set(programNode, analysis);
|
|
14331
|
+
return analysis;
|
|
14399
14332
|
};
|
|
14400
|
-
const
|
|
14401
|
-
if (!
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
if (
|
|
14410
|
-
|
|
14411
|
-
|
|
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
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
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
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
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
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
if (
|
|
14435
|
-
|
|
14436
|
-
|
|
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
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
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
|
|
14454
|
-
|
|
14455
|
-
if (
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
if (
|
|
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(
|
|
14466
|
-
|
|
14467
|
-
|
|
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
|
|
14439
|
+
return result;
|
|
14470
14440
|
};
|
|
14471
|
-
const
|
|
14472
|
-
|
|
14473
|
-
if (
|
|
14474
|
-
if (
|
|
14475
|
-
if (
|
|
14476
|
-
return
|
|
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
|
|
14479
|
-
const
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
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
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
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:
|
|
14539
|
-
message:
|
|
14732
|
+
node: callExpr,
|
|
14733
|
+
message: "Your users briefly see the wrong value when the prop changes."
|
|
14540
14734
|
});
|
|
14541
|
-
}
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
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
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
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/
|
|
14630
|
-
const
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
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
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
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
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
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
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
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
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
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
|
-
|
|
14741
|
-
|
|
14742
|
-
return withoutKnownDeclarations;
|
|
15026
|
+
}
|
|
15027
|
+
return false;
|
|
14743
15028
|
};
|
|
14744
|
-
const
|
|
14745
|
-
|
|
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
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
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
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
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
|
-
|
|
14772
|
-
return moduleInfo;
|
|
15078
|
+
return null;
|
|
14773
15079
|
};
|
|
14774
|
-
const
|
|
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/
|
|
14777
|
-
const
|
|
14778
|
-
const
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
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
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
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
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
"
|
|
14809
|
-
|
|
14810
|
-
"
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
const
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
];
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
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
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
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
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
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 (
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
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
|
|
14878
|
-
|
|
14879
|
-
if (
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
const
|
|
14883
|
-
const
|
|
14884
|
-
if (
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
return
|
|
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
|
-
|
|
14891
|
-
|
|
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
|
|
15269
|
+
return false;
|
|
14898
15270
|
};
|
|
14899
|
-
const
|
|
14900
|
-
const
|
|
14901
|
-
|
|
14902
|
-
if (
|
|
14903
|
-
|
|
14904
|
-
|
|
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/
|
|
14909
|
-
const
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
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
|
|
14916
|
-
|
|
14917
|
-
|
|
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
|
|
14923
|
-
if (
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
if (!
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
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
|
-
|
|
14934
|
-
|
|
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) {
|