oxlint-plugin-react-doctor 0.4.0-dev.ee980d9 → 0.4.0-dev.f61adc4
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.d.ts +38 -0
- package/dist/index.js +951 -533
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { analyze } from "eslint-scope";
|
|
3
|
-
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
4
2
|
import * as fs from "node:fs";
|
|
5
3
|
import { parseSync } from "oxc-parser";
|
|
4
|
+
import { analyze } from "eslint-scope";
|
|
5
|
+
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
6
6
|
//#region src/plugin/utils/is-testlike-filename.ts
|
|
7
7
|
const NON_PRODUCTION_PATH_SEGMENTS = [
|
|
8
8
|
"/test/",
|
|
@@ -13367,23 +13367,796 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
13367
13367
|
}
|
|
13368
13368
|
});
|
|
13369
13369
|
//#endregion
|
|
13370
|
-
//#region src/plugin/
|
|
13371
|
-
const
|
|
13372
|
-
|
|
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);
|
|
13387
|
+
}
|
|
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);
|
|
13419
|
+
}
|
|
13420
|
+
continue;
|
|
13421
|
+
}
|
|
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;
|
|
13437
|
+
}
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
if (exportedName === "default") {
|
|
13441
|
+
if (defaultExport) return defaultExport;
|
|
13442
|
+
if (defaultExportIdentifierName) {
|
|
13443
|
+
const binding = localBindings.get(defaultExportIdentifierName);
|
|
13444
|
+
if (binding) return binding;
|
|
13445
|
+
}
|
|
13446
|
+
}
|
|
13447
|
+
const localName = namedExports.get(exportedName);
|
|
13448
|
+
if (!localName) return null;
|
|
13449
|
+
return localBindings.get(localName) ?? null;
|
|
13450
|
+
};
|
|
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;
|
|
13460
|
+
};
|
|
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
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
13475
|
+
const sourceValue = statement.source.value;
|
|
13476
|
+
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
13477
|
+
}
|
|
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)) {
|
|
13489
|
+
if (key === "parent") continue;
|
|
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);
|
|
13494
|
+
}
|
|
13495
|
+
};
|
|
13496
|
+
visit(root, null);
|
|
13497
|
+
};
|
|
13498
|
+
//#endregion
|
|
13499
|
+
//#region src/plugin/utils/parse-source-file.ts
|
|
13500
|
+
const FILENAME_TO_LANG = {
|
|
13501
|
+
".ts": "ts",
|
|
13502
|
+
".tsx": "tsx",
|
|
13503
|
+
".js": "js",
|
|
13504
|
+
".jsx": "jsx",
|
|
13505
|
+
".mjs": "js",
|
|
13506
|
+
".cjs": "js",
|
|
13507
|
+
".mts": "ts",
|
|
13508
|
+
".cts": "ts"
|
|
13509
|
+
};
|
|
13510
|
+
const resolveLang = (filename) => {
|
|
13511
|
+
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
13512
|
+
};
|
|
13513
|
+
const parseCache = /* @__PURE__ */ new Map();
|
|
13514
|
+
const parseSourceFile = (absoluteFilePath) => {
|
|
13515
|
+
let fileStat;
|
|
13516
|
+
try {
|
|
13517
|
+
fileStat = fs.statSync(absoluteFilePath);
|
|
13518
|
+
} catch {
|
|
13519
|
+
return null;
|
|
13520
|
+
}
|
|
13521
|
+
if (!fileStat.isFile()) return null;
|
|
13522
|
+
if (fileStat.size > 2e6) return null;
|
|
13523
|
+
const cached = parseCache.get(absoluteFilePath);
|
|
13524
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
13525
|
+
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
13526
|
+
parseCache.set(absoluteFilePath, {
|
|
13527
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13528
|
+
size: fileStat.size,
|
|
13529
|
+
program: null
|
|
13530
|
+
});
|
|
13531
|
+
return null;
|
|
13532
|
+
}
|
|
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);
|
|
13553
|
+
}
|
|
13554
|
+
} catch {
|
|
13555
|
+
parsedProgram = null;
|
|
13556
|
+
}
|
|
13557
|
+
parseCache.set(absoluteFilePath, {
|
|
13558
|
+
mtimeMs: fileStat.mtimeMs,
|
|
13559
|
+
size: fileStat.size,
|
|
13560
|
+
program: parsedProgram
|
|
13561
|
+
});
|
|
13562
|
+
return parsedProgram;
|
|
13563
|
+
};
|
|
13564
|
+
//#endregion
|
|
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;
|
|
13593
|
+
};
|
|
13594
|
+
const doesModuleExportName = (filePath, exportedName) => {
|
|
13595
|
+
try {
|
|
13596
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
13597
|
+
} catch {
|
|
13598
|
+
return false;
|
|
13599
|
+
}
|
|
13600
|
+
};
|
|
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
|
|
13618
|
+
});
|
|
13619
|
+
};
|
|
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
|
|
13626
|
+
});
|
|
13627
|
+
};
|
|
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
|
|
13645
|
+
});
|
|
13646
|
+
};
|
|
13647
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
13648
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
13649
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
13650
|
+
return "";
|
|
13651
|
+
});
|
|
13652
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
13653
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
13654
|
+
if (namespaceExportName) {
|
|
13655
|
+
exportsByName.set(namespaceExportName, {
|
|
13656
|
+
exportedName: namespaceExportName,
|
|
13657
|
+
importedName: "*",
|
|
13658
|
+
source,
|
|
13659
|
+
isTypeOnly
|
|
13660
|
+
});
|
|
13661
|
+
return "";
|
|
13662
|
+
}
|
|
13663
|
+
if (specifiersText) {
|
|
13664
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
13665
|
+
exportedName: specifier.exportedName,
|
|
13666
|
+
importedName: specifier.localName,
|
|
13667
|
+
source,
|
|
13668
|
+
isTypeOnly: specifier.isTypeOnly
|
|
13669
|
+
});
|
|
13670
|
+
return "";
|
|
13671
|
+
}
|
|
13672
|
+
starExportSources.push(source);
|
|
13673
|
+
return "";
|
|
13674
|
+
});
|
|
13675
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
13676
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
13677
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
13678
|
+
if (!importedBinding) return _match;
|
|
13679
|
+
importedBinding.didExport = true;
|
|
13680
|
+
exportsByName.set(specifier.exportedName, {
|
|
13681
|
+
exportedName: specifier.exportedName,
|
|
13682
|
+
importedName: importedBinding.importedName,
|
|
13683
|
+
source: importedBinding.source,
|
|
13684
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
13685
|
+
});
|
|
13686
|
+
}
|
|
13687
|
+
return "";
|
|
13688
|
+
});
|
|
13689
|
+
return withoutKnownDeclarations;
|
|
13690
|
+
};
|
|
13691
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
13692
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
13693
|
+
return false;
|
|
13694
|
+
};
|
|
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
|
|
13706
|
+
};
|
|
13707
|
+
};
|
|
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();
|
|
13717
|
+
}
|
|
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;
|
|
13751
|
+
}
|
|
13752
|
+
};
|
|
13753
|
+
const getExistingDirectoryPath = (directoryPath) => {
|
|
13754
|
+
try {
|
|
13755
|
+
return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
|
|
13756
|
+
} catch {
|
|
13757
|
+
return null;
|
|
13758
|
+
}
|
|
13759
|
+
};
|
|
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];
|
|
13774
|
+
};
|
|
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;
|
|
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;
|
|
13789
|
+
}
|
|
13790
|
+
return null;
|
|
13791
|
+
};
|
|
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["."]);
|
|
13799
|
+
};
|
|
13800
|
+
const resolveModulePathWithIndexFallback = (modulePath) => {
|
|
13801
|
+
const filePath = resolveModuleFilePath(modulePath);
|
|
13802
|
+
if (filePath) return filePath;
|
|
13803
|
+
return resolveModuleFilePath(path.join(modulePath, "index"));
|
|
13804
|
+
};
|
|
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;
|
|
13816
|
+
}
|
|
13817
|
+
};
|
|
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;
|
|
13826
|
+
};
|
|
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"));
|
|
13833
|
+
};
|
|
13834
|
+
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
13835
|
+
//#endregion
|
|
13836
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
13837
|
+
const getUniqueFilePath = (filePaths) => {
|
|
13838
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
13839
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
13840
|
+
const [filePath] = uniqueFilePaths;
|
|
13841
|
+
return filePath ?? null;
|
|
13842
|
+
};
|
|
13843
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
13844
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
13845
|
+
if (!resolvedTargetPath) return null;
|
|
13846
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
13847
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
13848
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
13849
|
+
};
|
|
13850
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
13851
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
13852
|
+
visitedFilePaths.add(barrelFilePath);
|
|
13853
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
13854
|
+
if (!moduleInfo.isBarrel) return null;
|
|
13855
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
13856
|
+
if (target) {
|
|
13857
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
13858
|
+
if (!resolvedTargetPath) return null;
|
|
13859
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
13860
|
+
}
|
|
13861
|
+
if (exportedName === "default") return null;
|
|
13862
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
13863
|
+
};
|
|
13864
|
+
//#endregion
|
|
13865
|
+
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
13866
|
+
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
13867
|
+
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
13868
|
+
const stripJsonComments = (text) => {
|
|
13869
|
+
let output = "";
|
|
13870
|
+
let inString = false;
|
|
13871
|
+
let inLineComment = false;
|
|
13872
|
+
let inBlockComment = false;
|
|
13873
|
+
for (let index = 0; index < text.length; index++) {
|
|
13874
|
+
const character = text[index];
|
|
13875
|
+
const nextCharacter = text[index + 1];
|
|
13876
|
+
if (inLineComment) {
|
|
13877
|
+
if (character === "\n") {
|
|
13878
|
+
inLineComment = false;
|
|
13879
|
+
output += character;
|
|
13880
|
+
}
|
|
13881
|
+
continue;
|
|
13882
|
+
}
|
|
13883
|
+
if (inBlockComment) {
|
|
13884
|
+
if (character === "*" && nextCharacter === "/") {
|
|
13885
|
+
inBlockComment = false;
|
|
13886
|
+
index++;
|
|
13887
|
+
}
|
|
13888
|
+
continue;
|
|
13889
|
+
}
|
|
13890
|
+
if (inString) {
|
|
13891
|
+
output += character;
|
|
13892
|
+
if (character === "\\") {
|
|
13893
|
+
output += nextCharacter ?? "";
|
|
13894
|
+
index++;
|
|
13895
|
+
} else if (character === "\"") inString = false;
|
|
13896
|
+
continue;
|
|
13897
|
+
}
|
|
13898
|
+
if (character === "\"") {
|
|
13899
|
+
inString = true;
|
|
13900
|
+
output += character;
|
|
13901
|
+
continue;
|
|
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;
|
|
13914
|
+
}
|
|
13915
|
+
return output.replace(/,(\s*[}\]])/g, "$1");
|
|
13916
|
+
};
|
|
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;
|
|
13929
|
+
}
|
|
13930
|
+
};
|
|
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;
|
|
13986
|
+
};
|
|
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;
|
|
14032
|
+
};
|
|
14033
|
+
//#endregion
|
|
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;
|
|
13373
14068
|
walkAst(programNode, (child) => {
|
|
13374
|
-
if (
|
|
13375
|
-
if (isNodeOfType(child, "JSXOpeningElement") &&
|
|
13376
|
-
|
|
14069
|
+
if (didDetect) return false;
|
|
14070
|
+
if (isNodeOfType(child, "JSXOpeningElement") && isSuspenseJsxOpeningName(child.name)) {
|
|
14071
|
+
didDetect = true;
|
|
13377
14072
|
return false;
|
|
13378
14073
|
}
|
|
13379
14074
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
13380
14075
|
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
|
|
13381
|
-
|
|
14076
|
+
didDetect = true;
|
|
13382
14077
|
return false;
|
|
13383
14078
|
}
|
|
13384
14079
|
}
|
|
13385
14080
|
});
|
|
13386
|
-
return
|
|
14081
|
+
return didDetect;
|
|
14082
|
+
};
|
|
14083
|
+
//#endregion
|
|
14084
|
+
//#region src/plugin/utils/find-ancestor-suspense-layout.ts
|
|
14085
|
+
const LAYOUT_FILE_NAMES = [
|
|
14086
|
+
"layout.tsx",
|
|
14087
|
+
"layout.jsx",
|
|
14088
|
+
"layout.ts",
|
|
14089
|
+
"layout.js"
|
|
14090
|
+
];
|
|
14091
|
+
const hasAncestorSuspenseLayout = (pageFilename) => {
|
|
14092
|
+
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
14093
|
+
let currentDirectory = path.dirname(normalizedPage);
|
|
14094
|
+
for (let level = 0; level < 30; level++) {
|
|
14095
|
+
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
14096
|
+
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
14097
|
+
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
14098
|
+
const programRoot = parseSourceFile(layoutPath);
|
|
14099
|
+
if (programRoot && astMentionsSuspense(programRoot)) return true;
|
|
14100
|
+
}
|
|
14101
|
+
if (path.basename(currentDirectory) === "app") break;
|
|
14102
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
14103
|
+
if (parentDirectory === currentDirectory) break;
|
|
14104
|
+
currentDirectory = parentDirectory;
|
|
14105
|
+
}
|
|
14106
|
+
return false;
|
|
14107
|
+
};
|
|
14108
|
+
//#endregion
|
|
14109
|
+
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
14110
|
+
const astContainsUseSearchParams = (root) => {
|
|
14111
|
+
let didFind = false;
|
|
14112
|
+
walkAst(root, (child) => {
|
|
14113
|
+
if (didFind) return false;
|
|
14114
|
+
if (isHookCall$1(child, "useSearchParams")) {
|
|
14115
|
+
didFind = true;
|
|
14116
|
+
return false;
|
|
14117
|
+
}
|
|
14118
|
+
});
|
|
14119
|
+
return didFind;
|
|
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;
|
|
14130
|
+
}
|
|
14131
|
+
return false;
|
|
14132
|
+
};
|
|
14133
|
+
const collectSuspenseLocalNames = (programNode) => {
|
|
14134
|
+
const names = /* @__PURE__ */ new Set();
|
|
14135
|
+
for (const statement of programNode.body ?? []) {
|
|
14136
|
+
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
14137
|
+
if (statement.source?.value !== "react") continue;
|
|
14138
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
|
|
14139
|
+
}
|
|
14140
|
+
return names;
|
|
14141
|
+
};
|
|
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;
|
|
13387
14160
|
};
|
|
13388
14161
|
const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
13389
14162
|
id: "nextjs-no-use-search-params-without-suspense",
|
|
@@ -13393,18 +14166,44 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
13393
14166
|
severity: "warn",
|
|
13394
14167
|
recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
13395
14168
|
create: (context) => {
|
|
14169
|
+
let isPageOrLayoutFile = false;
|
|
14170
|
+
let hasAncestorLayoutSuspense = false;
|
|
13396
14171
|
let hasSuspenseInFile = false;
|
|
14172
|
+
let importedComponents = /* @__PURE__ */ new Map();
|
|
14173
|
+
let suspenseLocalNames = /* @__PURE__ */ new Set();
|
|
13397
14174
|
return {
|
|
13398
14175
|
Program(programNode) {
|
|
13399
|
-
|
|
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);
|
|
13400
14184
|
},
|
|
13401
14185
|
CallExpression(node) {
|
|
13402
|
-
if (hasSuspenseInFile) return;
|
|
14186
|
+
if (!isPageOrLayoutFile || hasAncestorLayoutSuspense || hasSuspenseInFile) return;
|
|
13403
14187
|
if (!isHookCall$1(node, "useSearchParams")) return;
|
|
13404
14188
|
context.report({
|
|
13405
14189
|
node,
|
|
13406
14190
|
message: "useSearchParams() without a <Suspense> boundary forces the whole page into client-side rendering."
|
|
13407
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
|
+
});
|
|
13408
14207
|
}
|
|
13409
14208
|
};
|
|
13410
14209
|
}
|
|
@@ -14635,306 +15434,6 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
14635
15434
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
14636
15435
|
};
|
|
14637
15436
|
//#endregion
|
|
14638
|
-
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
14639
|
-
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
14640
|
-
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
14641
|
-
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
14642
|
-
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
14643
|
-
const localName = getSpecifierName(rawLocalName ?? "");
|
|
14644
|
-
return {
|
|
14645
|
-
localName,
|
|
14646
|
-
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
14647
|
-
isTypeOnly
|
|
14648
|
-
};
|
|
14649
|
-
});
|
|
14650
|
-
//#endregion
|
|
14651
|
-
//#region src/plugin/utils/strip-js-comments.ts
|
|
14652
|
-
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
14653
|
-
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
14654
|
-
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
14655
|
-
//#endregion
|
|
14656
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
14657
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
14658
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14659
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14660
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14661
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
14662
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
14663
|
-
const createNonBarrelInfo = () => ({
|
|
14664
|
-
isBarrel: false,
|
|
14665
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
14666
|
-
starExportSources: []
|
|
14667
|
-
});
|
|
14668
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
14669
|
-
importedBindings.set(binding.localName, {
|
|
14670
|
-
...binding,
|
|
14671
|
-
didExport: false
|
|
14672
|
-
});
|
|
14673
|
-
};
|
|
14674
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
14675
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
14676
|
-
localName: specifier.exportedName,
|
|
14677
|
-
importedName: specifier.localName,
|
|
14678
|
-
source,
|
|
14679
|
-
isTypeOnly: specifier.isTypeOnly
|
|
14680
|
-
});
|
|
14681
|
-
};
|
|
14682
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
14683
|
-
const trimmedImportClause = importClause.trim();
|
|
14684
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
14685
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
14686
|
-
localName: namespaceMatch[1],
|
|
14687
|
-
importedName: "*",
|
|
14688
|
-
source,
|
|
14689
|
-
isTypeOnly: declarationIsTypeOnly
|
|
14690
|
-
});
|
|
14691
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
14692
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
14693
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
14694
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
14695
|
-
localName: defaultImportName,
|
|
14696
|
-
importedName: "default",
|
|
14697
|
-
source,
|
|
14698
|
-
isTypeOnly: declarationIsTypeOnly
|
|
14699
|
-
});
|
|
14700
|
-
};
|
|
14701
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
14702
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
14703
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
14704
|
-
return "";
|
|
14705
|
-
});
|
|
14706
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
14707
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
14708
|
-
if (namespaceExportName) {
|
|
14709
|
-
exportsByName.set(namespaceExportName, {
|
|
14710
|
-
exportedName: namespaceExportName,
|
|
14711
|
-
importedName: "*",
|
|
14712
|
-
source,
|
|
14713
|
-
isTypeOnly
|
|
14714
|
-
});
|
|
14715
|
-
return "";
|
|
14716
|
-
}
|
|
14717
|
-
if (specifiersText) {
|
|
14718
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
14719
|
-
exportedName: specifier.exportedName,
|
|
14720
|
-
importedName: specifier.localName,
|
|
14721
|
-
source,
|
|
14722
|
-
isTypeOnly: specifier.isTypeOnly
|
|
14723
|
-
});
|
|
14724
|
-
return "";
|
|
14725
|
-
}
|
|
14726
|
-
starExportSources.push(source);
|
|
14727
|
-
return "";
|
|
14728
|
-
});
|
|
14729
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1, (_match, typeKeyword, specifiersText) => {
|
|
14730
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
14731
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
14732
|
-
if (!importedBinding) return _match;
|
|
14733
|
-
importedBinding.didExport = true;
|
|
14734
|
-
exportsByName.set(specifier.exportedName, {
|
|
14735
|
-
exportedName: specifier.exportedName,
|
|
14736
|
-
importedName: importedBinding.importedName,
|
|
14737
|
-
source: importedBinding.source,
|
|
14738
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
14739
|
-
});
|
|
14740
|
-
}
|
|
14741
|
-
return "";
|
|
14742
|
-
});
|
|
14743
|
-
return withoutKnownDeclarations;
|
|
14744
|
-
};
|
|
14745
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
14746
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
14747
|
-
return false;
|
|
14748
|
-
};
|
|
14749
|
-
const classifyBarrelModule = (sourceText) => {
|
|
14750
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
14751
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
14752
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
14753
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
14754
|
-
const starExportSources = [];
|
|
14755
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
14756
|
-
return {
|
|
14757
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
14758
|
-
exportsByName,
|
|
14759
|
-
starExportSources
|
|
14760
|
-
};
|
|
14761
|
-
};
|
|
14762
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
14763
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
14764
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
14765
|
-
if (cachedResult !== void 0) return cachedResult;
|
|
14766
|
-
let moduleInfo = createNonBarrelInfo();
|
|
14767
|
-
try {
|
|
14768
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
14769
|
-
} catch {
|
|
14770
|
-
moduleInfo = createNonBarrelInfo();
|
|
14771
|
-
}
|
|
14772
|
-
barrelIndexModuleInfoCache.set(filePath, moduleInfo);
|
|
14773
|
-
return moduleInfo;
|
|
14774
|
-
};
|
|
14775
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
14776
|
-
//#endregion
|
|
14777
|
-
//#region src/plugin/utils/does-module-export-name.ts
|
|
14778
|
-
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
14779
|
-
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
14780
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
14781
|
-
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
14782
|
-
const strippedSource = stripJsComments(sourceText);
|
|
14783
|
-
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
14784
|
-
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
14785
|
-
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
14786
|
-
return false;
|
|
14787
|
-
};
|
|
14788
|
-
const doesModuleExportName = (filePath, exportedName) => {
|
|
14789
|
-
try {
|
|
14790
|
-
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
14791
|
-
} catch {
|
|
14792
|
-
return false;
|
|
14793
|
-
}
|
|
14794
|
-
};
|
|
14795
|
-
//#endregion
|
|
14796
|
-
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
14797
|
-
const MODULE_FILE_EXTENSIONS = [
|
|
14798
|
-
".ts",
|
|
14799
|
-
".tsx",
|
|
14800
|
-
".js",
|
|
14801
|
-
".jsx",
|
|
14802
|
-
".mjs",
|
|
14803
|
-
".cjs",
|
|
14804
|
-
".mts",
|
|
14805
|
-
".cts"
|
|
14806
|
-
];
|
|
14807
|
-
const PACKAGE_EXPORT_CONDITIONS = [
|
|
14808
|
-
"import",
|
|
14809
|
-
"default",
|
|
14810
|
-
"module",
|
|
14811
|
-
"browser",
|
|
14812
|
-
"require"
|
|
14813
|
-
];
|
|
14814
|
-
const PACKAGE_ENTRY_FIELDS = [
|
|
14815
|
-
"module",
|
|
14816
|
-
"main",
|
|
14817
|
-
"browser"
|
|
14818
|
-
];
|
|
14819
|
-
const getExistingFilePath = (filePath) => {
|
|
14820
|
-
try {
|
|
14821
|
-
return fs.statSync(filePath).isFile() ? filePath : null;
|
|
14822
|
-
} catch {
|
|
14823
|
-
return null;
|
|
14824
|
-
}
|
|
14825
|
-
};
|
|
14826
|
-
const getExistingDirectoryPath = (directoryPath) => {
|
|
14827
|
-
try {
|
|
14828
|
-
return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
|
|
14829
|
-
} catch {
|
|
14830
|
-
return null;
|
|
14831
|
-
}
|
|
14832
|
-
};
|
|
14833
|
-
const getModuleFilePathCandidates = (modulePath) => {
|
|
14834
|
-
const extension = path.extname(modulePath);
|
|
14835
|
-
if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
|
|
14836
|
-
const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
|
|
14837
|
-
if (extension === ".js") return [
|
|
14838
|
-
modulePath,
|
|
14839
|
-
`${modulePathWithoutExtension}.ts`,
|
|
14840
|
-
`${modulePathWithoutExtension}.tsx`,
|
|
14841
|
-
`${modulePathWithoutExtension}.jsx`
|
|
14842
|
-
];
|
|
14843
|
-
if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
|
|
14844
|
-
if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
|
|
14845
|
-
if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
|
|
14846
|
-
return [modulePath];
|
|
14847
|
-
};
|
|
14848
|
-
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
14849
|
-
const getConditionalExportEntry = (exportEntry) => {
|
|
14850
|
-
if (typeof exportEntry === "string") return exportEntry;
|
|
14851
|
-
if (Array.isArray(exportEntry)) {
|
|
14852
|
-
for (const fallbackEntry of exportEntry) {
|
|
14853
|
-
const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
|
|
14854
|
-
if (resolvedFallbackEntry) return resolvedFallbackEntry;
|
|
14855
|
-
}
|
|
14856
|
-
return null;
|
|
14857
|
-
}
|
|
14858
|
-
if (!isObjectRecord(exportEntry)) return null;
|
|
14859
|
-
for (const condition of PACKAGE_EXPORT_CONDITIONS) {
|
|
14860
|
-
const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
|
|
14861
|
-
if (nestedEntry) return nestedEntry;
|
|
14862
|
-
}
|
|
14863
|
-
return null;
|
|
14864
|
-
};
|
|
14865
|
-
const getPackageExportEntry = (packageJson) => {
|
|
14866
|
-
const exportsField = packageJson.exports;
|
|
14867
|
-
if (!exportsField) return null;
|
|
14868
|
-
const directExportEntry = getConditionalExportEntry(exportsField);
|
|
14869
|
-
if (directExportEntry) return directExportEntry;
|
|
14870
|
-
if (!isObjectRecord(exportsField)) return null;
|
|
14871
|
-
return getConditionalExportEntry(exportsField["."]);
|
|
14872
|
-
};
|
|
14873
|
-
const resolveModulePathWithIndexFallback = (modulePath) => {
|
|
14874
|
-
const filePath = resolveModuleFilePath(modulePath);
|
|
14875
|
-
if (filePath) return filePath;
|
|
14876
|
-
return resolveModuleFilePath(path.join(modulePath, "index"));
|
|
14877
|
-
};
|
|
14878
|
-
const resolvePackageDirectoryEntry = (directoryPath) => {
|
|
14879
|
-
const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
|
|
14880
|
-
if (!existingDirectoryPath) return null;
|
|
14881
|
-
const packageJsonPath = path.join(existingDirectoryPath, "package.json");
|
|
14882
|
-
try {
|
|
14883
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
14884
|
-
const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
|
|
14885
|
-
if (!packageEntry) return null;
|
|
14886
|
-
return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
|
|
14887
|
-
} catch {
|
|
14888
|
-
return null;
|
|
14889
|
-
}
|
|
14890
|
-
};
|
|
14891
|
-
const resolveModuleFilePath = (modulePath) => {
|
|
14892
|
-
const exactFilePath = getExistingFilePath(modulePath);
|
|
14893
|
-
if (exactFilePath) return exactFilePath;
|
|
14894
|
-
for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
|
|
14895
|
-
const filePath = getExistingFilePath(candidateFilePath);
|
|
14896
|
-
if (filePath) return filePath;
|
|
14897
|
-
}
|
|
14898
|
-
return null;
|
|
14899
|
-
};
|
|
14900
|
-
const resolveRelativeImportPath = (filename, source) => {
|
|
14901
|
-
const importPath = path.resolve(path.dirname(filename), source);
|
|
14902
|
-
const directFilePath = resolveModuleFilePath(importPath);
|
|
14903
|
-
if (directFilePath) return directFilePath;
|
|
14904
|
-
const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
|
|
14905
|
-
if (packageEntryFilePath) return packageEntryFilePath;
|
|
14906
|
-
return resolveModuleFilePath(path.join(importPath, "index"));
|
|
14907
|
-
};
|
|
14908
|
-
//#endregion
|
|
14909
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
14910
|
-
const getUniqueFilePath = (filePaths) => {
|
|
14911
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
14912
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
14913
|
-
const [filePath] = uniqueFilePaths;
|
|
14914
|
-
return filePath ?? null;
|
|
14915
|
-
};
|
|
14916
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
14917
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
14918
|
-
if (!resolvedTargetPath) return null;
|
|
14919
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
14920
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
14921
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
14922
|
-
};
|
|
14923
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
14924
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
14925
|
-
visitedFilePaths.add(barrelFilePath);
|
|
14926
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
14927
|
-
if (!moduleInfo.isBarrel) return null;
|
|
14928
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
14929
|
-
if (target) {
|
|
14930
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
14931
|
-
if (!resolvedTargetPath) return null;
|
|
14932
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
14933
|
-
}
|
|
14934
|
-
if (exportedName === "default") return null;
|
|
14935
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
14936
|
-
};
|
|
14937
|
-
//#endregion
|
|
14938
15437
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
14939
15438
|
const getLiteralName = (node) => {
|
|
14940
15439
|
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
@@ -18669,11 +19168,14 @@ const noLongTransitionDuration = defineRule({
|
|
|
18669
19168
|
} })
|
|
18670
19169
|
});
|
|
18671
19170
|
//#endregion
|
|
19171
|
+
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
19172
|
+
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
19173
|
+
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
19174
|
+
//#endregion
|
|
18672
19175
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18673
19176
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
18674
19177
|
//#endregion
|
|
18675
19178
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
18676
|
-
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
18677
19179
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
18678
19180
|
const found = /* @__PURE__ */ new Set();
|
|
18679
19181
|
if (!componentBody) return found;
|
|
@@ -18683,7 +19185,7 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
|
18683
19185
|
if (!isNodeOfType(child.object, "Identifier")) return;
|
|
18684
19186
|
if (child.object.name !== propsParamName) return;
|
|
18685
19187
|
if (!isNodeOfType(child.property, "Identifier")) return;
|
|
18686
|
-
if (!
|
|
19188
|
+
if (!isBooleanPrefixedPropName(child.property.name)) return;
|
|
18687
19189
|
found.add(child.property.name);
|
|
18688
19190
|
});
|
|
18689
19191
|
return found;
|
|
@@ -18709,7 +19211,7 @@ const noManyBooleanProps = defineRule({
|
|
|
18709
19211
|
if (!isNodeOfType(property, "Property")) continue;
|
|
18710
19212
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
18711
19213
|
if (!keyName) continue;
|
|
18712
|
-
if (
|
|
19214
|
+
if (isBooleanPrefixedPropName(keyName)) booleanLikePropNames.push(keyName);
|
|
18713
19215
|
}
|
|
18714
19216
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
18715
19217
|
return;
|
|
@@ -19279,224 +19781,7 @@ const isLodashMutatorCall = (callExpression) => {
|
|
|
19279
19781
|
return false;
|
|
19280
19782
|
};
|
|
19281
19783
|
//#endregion
|
|
19282
|
-
//#region src/plugin/utils/find-exported-function-body.ts
|
|
19283
|
-
const isFunctionLike = (node) => {
|
|
19284
|
-
if (!node) return false;
|
|
19285
|
-
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
19286
|
-
};
|
|
19287
|
-
const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
19288
|
-
if (!isNodeOfType(programRoot, "Program")) return null;
|
|
19289
|
-
const localBindings = /* @__PURE__ */ new Map();
|
|
19290
|
-
const namedExports = /* @__PURE__ */ new Map();
|
|
19291
|
-
let defaultExport = null;
|
|
19292
|
-
let defaultExportIdentifierName = null;
|
|
19293
|
-
const recordVariableDeclaration = (declaration) => {
|
|
19294
|
-
for (const declarator of declaration.declarations ?? []) {
|
|
19295
|
-
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
19296
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
19297
|
-
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
19298
|
-
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
19299
|
-
}
|
|
19300
|
-
};
|
|
19301
|
-
for (const statement of programRoot.body ?? []) {
|
|
19302
|
-
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
19303
|
-
recordVariableDeclaration(statement);
|
|
19304
|
-
continue;
|
|
19305
|
-
}
|
|
19306
|
-
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
|
|
19307
|
-
localBindings.set(statement.id.name, statement);
|
|
19308
|
-
continue;
|
|
19309
|
-
}
|
|
19310
|
-
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
19311
|
-
const declaration = statement.declaration;
|
|
19312
|
-
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
19313
|
-
recordVariableDeclaration(declaration);
|
|
19314
|
-
for (const declarator of declaration.declarations ?? []) {
|
|
19315
|
-
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
19316
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
19317
|
-
namedExports.set(declarator.id.name, declarator.id.name);
|
|
19318
|
-
}
|
|
19319
|
-
} else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
19320
|
-
localBindings.set(declaration.id.name, declaration);
|
|
19321
|
-
namedExports.set(declaration.id.name, declaration.id.name);
|
|
19322
|
-
}
|
|
19323
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
19324
|
-
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
19325
|
-
const local = specifier.local;
|
|
19326
|
-
const exported = specifier.exported;
|
|
19327
|
-
if (!isNodeOfType(local, "Identifier")) continue;
|
|
19328
|
-
const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
|
|
19329
|
-
if (!exportedNameSpec) continue;
|
|
19330
|
-
namedExports.set(exportedNameSpec, local.name);
|
|
19331
|
-
}
|
|
19332
|
-
continue;
|
|
19333
|
-
}
|
|
19334
|
-
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
19335
|
-
const declaration = statement.declaration;
|
|
19336
|
-
if (!declaration) continue;
|
|
19337
|
-
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
19338
|
-
localBindings.set(declaration.id.name, declaration);
|
|
19339
|
-
defaultExport = declaration;
|
|
19340
|
-
continue;
|
|
19341
|
-
}
|
|
19342
|
-
if (isFunctionLike(declaration)) {
|
|
19343
|
-
defaultExport = declaration;
|
|
19344
|
-
continue;
|
|
19345
|
-
}
|
|
19346
|
-
if (isNodeOfType(declaration, "Identifier")) {
|
|
19347
|
-
defaultExportIdentifierName = declaration.name;
|
|
19348
|
-
continue;
|
|
19349
|
-
}
|
|
19350
|
-
}
|
|
19351
|
-
}
|
|
19352
|
-
if (exportedName === "default") {
|
|
19353
|
-
if (defaultExport) return defaultExport;
|
|
19354
|
-
if (defaultExportIdentifierName) {
|
|
19355
|
-
const binding = localBindings.get(defaultExportIdentifierName);
|
|
19356
|
-
if (binding) return binding;
|
|
19357
|
-
}
|
|
19358
|
-
}
|
|
19359
|
-
const localName = namedExports.get(exportedName);
|
|
19360
|
-
if (!localName) return null;
|
|
19361
|
-
return localBindings.get(localName) ?? null;
|
|
19362
|
-
};
|
|
19363
|
-
const resolveImportedExportName = (importSpecifier) => {
|
|
19364
|
-
if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
|
|
19365
|
-
const imported = importSpecifier.imported;
|
|
19366
|
-
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
19367
|
-
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
19368
|
-
return null;
|
|
19369
|
-
}
|
|
19370
|
-
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
19371
|
-
return null;
|
|
19372
|
-
};
|
|
19373
|
-
const findReExportSourcesForName = (programRoot, exportedName) => {
|
|
19374
|
-
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
19375
|
-
const exportAllSources = [];
|
|
19376
|
-
for (const statement of programRoot.body ?? []) {
|
|
19377
|
-
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
19378
|
-
const sourceValue = statement.source.value;
|
|
19379
|
-
if (typeof sourceValue !== "string") continue;
|
|
19380
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
19381
|
-
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
19382
|
-
const exported = specifier.exported;
|
|
19383
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
|
|
19384
|
-
}
|
|
19385
|
-
}
|
|
19386
|
-
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
19387
|
-
const sourceValue = statement.source.value;
|
|
19388
|
-
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
19389
|
-
}
|
|
19390
|
-
}
|
|
19391
|
-
return exportAllSources;
|
|
19392
|
-
};
|
|
19393
|
-
//#endregion
|
|
19394
|
-
//#region src/plugin/utils/attach-parent-references.ts
|
|
19395
|
-
const attachParentReferences = (root) => {
|
|
19396
|
-
const visit = (node, parent) => {
|
|
19397
|
-
const writableNode = node;
|
|
19398
|
-
writableNode.parent = parent;
|
|
19399
|
-
const nodeRecord = node;
|
|
19400
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19401
|
-
if (key === "parent") continue;
|
|
19402
|
-
const child = nodeRecord[key];
|
|
19403
|
-
if (Array.isArray(child)) {
|
|
19404
|
-
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
19405
|
-
} else if (isAstNode(child)) visit(child, node);
|
|
19406
|
-
}
|
|
19407
|
-
};
|
|
19408
|
-
visit(root, null);
|
|
19409
|
-
};
|
|
19410
|
-
//#endregion
|
|
19411
|
-
//#region src/plugin/utils/parse-source-file.ts
|
|
19412
|
-
const FILENAME_TO_LANG = {
|
|
19413
|
-
".ts": "ts",
|
|
19414
|
-
".tsx": "tsx",
|
|
19415
|
-
".js": "js",
|
|
19416
|
-
".jsx": "jsx",
|
|
19417
|
-
".mjs": "js",
|
|
19418
|
-
".cjs": "js",
|
|
19419
|
-
".mts": "ts",
|
|
19420
|
-
".cts": "ts"
|
|
19421
|
-
};
|
|
19422
|
-
const resolveLang = (filename) => {
|
|
19423
|
-
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
19424
|
-
};
|
|
19425
|
-
const parseCache = /* @__PURE__ */ new Map();
|
|
19426
|
-
const parseSourceFile = (absoluteFilePath) => {
|
|
19427
|
-
let fileStat;
|
|
19428
|
-
try {
|
|
19429
|
-
fileStat = fs.statSync(absoluteFilePath);
|
|
19430
|
-
} catch {
|
|
19431
|
-
return null;
|
|
19432
|
-
}
|
|
19433
|
-
if (!fileStat.isFile()) return null;
|
|
19434
|
-
if (fileStat.size > 2e6) return null;
|
|
19435
|
-
const cached = parseCache.get(absoluteFilePath);
|
|
19436
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
19437
|
-
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
19438
|
-
parseCache.set(absoluteFilePath, {
|
|
19439
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19440
|
-
size: fileStat.size,
|
|
19441
|
-
program: null
|
|
19442
|
-
});
|
|
19443
|
-
return null;
|
|
19444
|
-
}
|
|
19445
|
-
let sourceText;
|
|
19446
|
-
try {
|
|
19447
|
-
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
19448
|
-
} catch {
|
|
19449
|
-
parseCache.set(absoluteFilePath, {
|
|
19450
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19451
|
-
size: fileStat.size,
|
|
19452
|
-
program: null
|
|
19453
|
-
});
|
|
19454
|
-
return null;
|
|
19455
|
-
}
|
|
19456
|
-
let parsedProgram = null;
|
|
19457
|
-
try {
|
|
19458
|
-
const result = parseSync(absoluteFilePath, sourceText, {
|
|
19459
|
-
astType: "ts",
|
|
19460
|
-
lang: resolveLang(absoluteFilePath)
|
|
19461
|
-
});
|
|
19462
|
-
if (!result.errors.some((parseError) => parseError.severity === "Error")) {
|
|
19463
|
-
parsedProgram = result.program;
|
|
19464
|
-
attachParentReferences(parsedProgram);
|
|
19465
|
-
}
|
|
19466
|
-
} catch {
|
|
19467
|
-
parsedProgram = null;
|
|
19468
|
-
}
|
|
19469
|
-
parseCache.set(absoluteFilePath, {
|
|
19470
|
-
mtimeMs: fileStat.mtimeMs,
|
|
19471
|
-
size: fileStat.size,
|
|
19472
|
-
program: parsedProgram
|
|
19473
|
-
});
|
|
19474
|
-
return parsedProgram;
|
|
19475
|
-
};
|
|
19476
|
-
//#endregion
|
|
19477
19784
|
//#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
|
|
19478
|
-
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
19479
|
-
if (visitedFilePaths.size >= 4) return null;
|
|
19480
|
-
if (visitedFilePaths.has(filePath)) return null;
|
|
19481
|
-
visitedFilePaths.add(filePath);
|
|
19482
|
-
const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
|
|
19483
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
19484
|
-
if (!programRoot) return null;
|
|
19485
|
-
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
19486
|
-
if (exported) return exported;
|
|
19487
|
-
for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
|
|
19488
|
-
const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
|
|
19489
|
-
if (!nextFilePath) continue;
|
|
19490
|
-
const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
|
|
19491
|
-
if (resolved) return resolved;
|
|
19492
|
-
}
|
|
19493
|
-
return null;
|
|
19494
|
-
};
|
|
19495
|
-
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
19496
|
-
const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
|
|
19497
|
-
if (!resolvedFilePath) return null;
|
|
19498
|
-
return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
|
|
19499
|
-
};
|
|
19500
19785
|
const resolveReducerFunction = (node, currentFilename) => {
|
|
19501
19786
|
if (!node) return null;
|
|
19502
19787
|
const unwrappedNode = stripParenExpression(node);
|
|
@@ -19518,7 +19803,6 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
19518
19803
|
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
19519
19804
|
const sourceValue = importDeclaration.source?.value;
|
|
19520
19805
|
if (typeof sourceValue !== "string") return null;
|
|
19521
|
-
if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
|
|
19522
19806
|
const exportedName = resolveImportedExportName(initializer);
|
|
19523
19807
|
if (!exportedName) return null;
|
|
19524
19808
|
const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);
|
|
@@ -24923,6 +25207,128 @@ const preferEs6Class = defineRule({
|
|
|
24923
25207
|
}
|
|
24924
25208
|
});
|
|
24925
25209
|
//#endregion
|
|
25210
|
+
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
25211
|
+
/**
|
|
25212
|
+
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
25213
|
+
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
25214
|
+
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
25215
|
+
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
25216
|
+
* callers that need the semantic expression should `stripParenExpression`
|
|
25217
|
+
* first.
|
|
25218
|
+
*/
|
|
25219
|
+
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
25220
|
+
//#endregion
|
|
25221
|
+
//#region src/plugin/rules/architecture/prefer-explicit-variants.ts
|
|
25222
|
+
const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
|
|
25223
|
+
let identifierNode = stripParenExpression(testNode);
|
|
25224
|
+
if (isNodeOfType(identifierNode, "UnaryExpression") && identifierNode.operator === "!") identifierNode = stripParenExpression(identifierNode.argument);
|
|
25225
|
+
if (!isNodeOfType(identifierNode, "Identifier")) return null;
|
|
25226
|
+
return booleanPropBindings.has(identifierNode.name) ? identifierNode.name : null;
|
|
25227
|
+
};
|
|
25228
|
+
const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
25229
|
+
"isLoading",
|
|
25230
|
+
"isPending",
|
|
25231
|
+
"isFetching",
|
|
25232
|
+
"isRefetching",
|
|
25233
|
+
"isSubmitting",
|
|
25234
|
+
"isError",
|
|
25235
|
+
"isSuccess",
|
|
25236
|
+
"isEmpty",
|
|
25237
|
+
"isReady",
|
|
25238
|
+
"isDirty",
|
|
25239
|
+
"isValid",
|
|
25240
|
+
"isInvalid",
|
|
25241
|
+
"isOpen",
|
|
25242
|
+
"isClosed",
|
|
25243
|
+
"isVisible",
|
|
25244
|
+
"isHidden",
|
|
25245
|
+
"isActive",
|
|
25246
|
+
"isInactive",
|
|
25247
|
+
"isExpanded",
|
|
25248
|
+
"isCollapsed",
|
|
25249
|
+
"isSelected",
|
|
25250
|
+
"isChecked",
|
|
25251
|
+
"isDisabled",
|
|
25252
|
+
"isEnabled",
|
|
25253
|
+
"isFocused",
|
|
25254
|
+
"isHovered",
|
|
25255
|
+
"isDragging",
|
|
25256
|
+
"isFullscreen",
|
|
25257
|
+
"isMobile",
|
|
25258
|
+
"isDesktop",
|
|
25259
|
+
"isTablet",
|
|
25260
|
+
"isOnline",
|
|
25261
|
+
"isOffline",
|
|
25262
|
+
"isLoggedIn",
|
|
25263
|
+
"isAuthenticated",
|
|
25264
|
+
"isAuthorized",
|
|
25265
|
+
"isDark",
|
|
25266
|
+
"isLight"
|
|
25267
|
+
]);
|
|
25268
|
+
const collectBooleanPropBindings = (param) => {
|
|
25269
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
25270
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
25271
|
+
for (const property of param.properties ?? []) {
|
|
25272
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
25273
|
+
if (property.computed) continue;
|
|
25274
|
+
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
25275
|
+
if (!isBooleanPrefixedPropName(property.key.name)) continue;
|
|
25276
|
+
if (CROSS_CUTTING_STATE_BOOLEAN_NAMES.has(property.key.name)) continue;
|
|
25277
|
+
const propertyValue = property.value;
|
|
25278
|
+
if (isNodeOfType(propertyValue, "Identifier")) bindings.add(propertyValue.name);
|
|
25279
|
+
else if (isNodeOfType(propertyValue, "AssignmentPattern") && isNodeOfType(propertyValue.left, "Identifier")) bindings.add(propertyValue.left.name);
|
|
25280
|
+
}
|
|
25281
|
+
return bindings;
|
|
25282
|
+
};
|
|
25283
|
+
const collectVariantBranchProps = (body, booleanPropBindings) => {
|
|
25284
|
+
const variantBranchProps = /* @__PURE__ */ new Set();
|
|
25285
|
+
if (!body) return variantBranchProps;
|
|
25286
|
+
walkAst(body, (current) => {
|
|
25287
|
+
if (isNodeOfType(current, "FunctionDeclaration") || isInlineFunctionExpression(current)) return false;
|
|
25288
|
+
if (!isNodeOfType(current, "ConditionalExpression")) return;
|
|
25289
|
+
const propName = resolveBooleanPropTestName(current.test, booleanPropBindings);
|
|
25290
|
+
if (!propName) return;
|
|
25291
|
+
const consequent = stripParenExpression(current.consequent);
|
|
25292
|
+
const alternate = stripParenExpression(current.alternate);
|
|
25293
|
+
if (!isJsxElementOrFragment(consequent) || !isJsxElementOrFragment(alternate)) return;
|
|
25294
|
+
variantBranchProps.add(propName);
|
|
25295
|
+
});
|
|
25296
|
+
return variantBranchProps;
|
|
25297
|
+
};
|
|
25298
|
+
const preferExplicitVariants = defineRule({
|
|
25299
|
+
id: "prefer-explicit-variants",
|
|
25300
|
+
title: "Prefer explicit variant components",
|
|
25301
|
+
severity: "warn",
|
|
25302
|
+
tags: ["test-noise", "react-jsx-only"],
|
|
25303
|
+
recommendation: "Replace boolean props that switch whole subtrees with explicit variant components, like `<ThreadComposer />` and `<EditMessageComposer />`, so each variant renders one clear path.",
|
|
25304
|
+
create: (context) => {
|
|
25305
|
+
const checkComponent = (param, body, componentName, reportNode) => {
|
|
25306
|
+
const booleanPropBindings = collectBooleanPropBindings(param);
|
|
25307
|
+
if (booleanPropBindings.size < 2) return;
|
|
25308
|
+
const variantBranchProps = collectVariantBranchProps(body, booleanPropBindings);
|
|
25309
|
+
if (variantBranchProps.size < 2) return;
|
|
25310
|
+
const propList = [...variantBranchProps].slice(0, 3).join(", ");
|
|
25311
|
+
const overflow = variantBranchProps.size > 3 ? "…" : "";
|
|
25312
|
+
context.report({
|
|
25313
|
+
node: reportNode,
|
|
25314
|
+
message: `Component "${componentName}" picks which component to render from ${variantBranchProps.size} boolean props (${propList}${overflow}), which multiplies untestable variants. Split it into explicit variant components so each renders one clear path.`
|
|
25315
|
+
});
|
|
25316
|
+
};
|
|
25317
|
+
return {
|
|
25318
|
+
FunctionDeclaration(node) {
|
|
25319
|
+
if (!isComponentDeclaration(node) || !node.id) return;
|
|
25320
|
+
checkComponent(node.params?.[0], node.body, node.id.name, node.id);
|
|
25321
|
+
},
|
|
25322
|
+
VariableDeclarator(node) {
|
|
25323
|
+
if (!isComponentAssignment(node)) return;
|
|
25324
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
25325
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
25326
|
+
checkComponent(node.init.params?.[0], node.init.body, node.id.name, node.id);
|
|
25327
|
+
}
|
|
25328
|
+
};
|
|
25329
|
+
}
|
|
25330
|
+
});
|
|
25331
|
+
//#endregion
|
|
24926
25332
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
24927
25333
|
const MESSAGE$7 = "This class component is harder to maintain than a function component.";
|
|
24928
25334
|
const resolveSettings$4 = (settings) => {
|
|
@@ -33166,6 +33572,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
33166
33572
|
id: "server-fetch-without-revalidate",
|
|
33167
33573
|
title: "Fetch without revalidate",
|
|
33168
33574
|
severity: "warn",
|
|
33575
|
+
disabledBy: ["nextjs:15"],
|
|
33169
33576
|
recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
|
|
33170
33577
|
create: (context) => {
|
|
33171
33578
|
let isServerSideFile = false;
|
|
@@ -37242,6 +37649,17 @@ const reactDoctorRules = [
|
|
|
37242
37649
|
category: "Maintainability"
|
|
37243
37650
|
}
|
|
37244
37651
|
},
|
|
37652
|
+
{
|
|
37653
|
+
key: "react-doctor/prefer-explicit-variants",
|
|
37654
|
+
id: "prefer-explicit-variants",
|
|
37655
|
+
source: "react-doctor",
|
|
37656
|
+
originallyExternal: false,
|
|
37657
|
+
rule: {
|
|
37658
|
+
...preferExplicitVariants,
|
|
37659
|
+
framework: "global",
|
|
37660
|
+
category: "Maintainability"
|
|
37661
|
+
}
|
|
37662
|
+
},
|
|
37245
37663
|
{
|
|
37246
37664
|
key: "react-doctor/prefer-function-component",
|
|
37247
37665
|
id: "prefer-function-component",
|