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.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/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
13371
- const fileMentionsSuspense = (programNode) => {
13372
- let didSee = false;
13370
+ //#region src/plugin/utils/find-exported-function-body.ts
13371
+ const isFunctionLike = (node) => {
13372
+ if (!node) return false;
13373
+ return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
13374
+ };
13375
+ const findExportedFunctionBody = (programRoot, exportedName) => {
13376
+ if (!isNodeOfType(programRoot, "Program")) return null;
13377
+ const localBindings = /* @__PURE__ */ new Map();
13378
+ const namedExports = /* @__PURE__ */ new Map();
13379
+ let defaultExport = null;
13380
+ let defaultExportIdentifierName = null;
13381
+ const recordVariableDeclaration = (declaration) => {
13382
+ for (const declarator of declaration.declarations ?? []) {
13383
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
13384
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
13385
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
13386
+ if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
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 (didSee) return false;
13375
- if (isNodeOfType(child, "JSXOpeningElement") && isNodeOfType(child.name, "JSXIdentifier") && child.name.name === "Suspense") {
13376
- didSee = true;
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
- didSee = true;
14076
+ didDetect = true;
13382
14077
  return false;
13383
14078
  }
13384
14079
  }
13385
14080
  });
13386
- return didSee;
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
- hasSuspenseInFile = fileMentionsSuspense(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);
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 (!BOOLEAN_PROP_PREFIX_PATTERN.test(child.property.name)) return;
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 (BOOLEAN_PROP_PREFIX_PATTERN.test(keyName)) booleanLikePropNames.push(keyName);
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",