oxlint-plugin-react-doctor 0.6.3-dev.8325343 → 0.6.3-dev.bdf8074

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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { StaticImport } from "oxc-parser";
1
2
  import { TSESTree } from "@typescript-eslint/types";
2
3
 
3
4
  //#region src/plugin/utils/es-tree-node.d.ts
@@ -16644,6 +16645,85 @@ declare const MOTION_LIBRARY_PACKAGES: Set<string>;
16644
16645
  //#region src/plugin/constants/cross-file-rule-ids.d.ts
16645
16646
  declare const CROSS_FILE_RULE_IDS: ReadonlySet<string>;
16646
16647
  //#endregion
16648
+ //#region src/plugin/utils/cross-file-probe-recorder.d.ts
16649
+ interface CrossFileProbeTrace {
16650
+ /** Paths whose existence classification (file / dir / none) was consulted. */
16651
+ readonly existencePaths: Set<string>;
16652
+ /** Paths whose content (bytes, exports, parse) was consulted. */
16653
+ readonly contentPaths: Set<string>;
16654
+ }
16655
+ //#endregion
16656
+ //#region src/plugin/cross-file-dependencies.d.ts
16657
+ /**
16658
+ * Per-rule cross-file dependency collectors — the foundation of the sidecar
16659
+ * lint cache's dependency fingerprints (`@react-doctor/core`,
16660
+ * `runners/oxlint/sidecar-lint-cache.ts`).
16661
+ *
16662
+ * A collector re-runs the SAME resolver helpers its rule calls at lint time
16663
+ * (import resolution, barrel following, ancestor-layout walks, package.json
16664
+ * classification) under the probe recorder
16665
+ * (`utils/cross-file-probe-recorder.ts`), so the recorded probe set is the
16666
+ * exact set of filesystem facts the rule's execution consulted — including
16667
+ * the negative probes (extension candidates that did NOT exist) that make
16668
+ * resolution shadowing detectable when a new file appears.
16669
+ *
16670
+ * SOUNDNESS INVARIANT — each collector's probe set must be a SUPERSET of
16671
+ * every filesystem read its rule can make while linting the file, for any
16672
+ * file content and any filesystem state:
16673
+ *
16674
+ * - Over-approximation is always safe (extra probes only cause a spurious
16675
+ * re-lint); under-approximation is forbidden (a missed probe could
16676
+ * replay a stale verdict).
16677
+ * - Where a rule gates its cross-file reads on in-file conditions, a
16678
+ * collector may skip the gate (probe more) or must mirror it EXACTLY
16679
+ * (probe the same). Each collector documents which it does per gate.
16680
+ * - The replay argument: the stored probe set is the complete read set of
16681
+ * the collector's execution at store time. If every stored probe has
16682
+ * the same answer on a later scan, re-executing the collector — and
16683
+ * therefore the rule, whose reads are a subset — reproduces the same
16684
+ * execution step by step (the file's own content is pinned separately
16685
+ * by the cache key's content hash), so the stored diagnostics are
16686
+ * exactly what a fresh sidecar lint would produce.
16687
+ *
16688
+ * A cross-file rule WITHOUT a collector here must be listed in
16689
+ * `UNBOUNDED_CROSS_FILE_RULE_IDS` — it then re-lints every file on every
16690
+ * scan (the sound fallback). `cross-file-rule-ids.test.ts` in
16691
+ * `@react-doctor/core` forces every `CROSS_FILE_RULE_IDS` entry into
16692
+ * exactly one of the two classifications.
16693
+ */
16694
+ interface CrossFileDependencyCollectorInput {
16695
+ /** Absolute, forward-slash-normalized path of the file being fingerprinted. */
16696
+ readonly absoluteFilePath: string;
16697
+ readonly sourceText: string;
16698
+ /** oxc module record — the file's static import declarations. */
16699
+ readonly staticImports: ReadonlyArray<StaticImport>;
16700
+ /** Parsed program (no parent references attached). */
16701
+ readonly program: EsTreeNode;
16702
+ }
16703
+ type CrossFileDependencyCollector = (input: CrossFileDependencyCollectorInput) => void;
16704
+ declare const CROSS_FILE_DEPENDENCY_COLLECTORS: ReadonlyMap<string, CrossFileDependencyCollector>;
16705
+ /**
16706
+ * Cross-file rules whose dependency set CANNOT be soundly bounded — they are
16707
+ * excluded from fingerprinting and re-lint every file on every scan. Empty
16708
+ * today; a new cross-file rule must be added either here or to
16709
+ * `CROSS_FILE_DEPENDENCY_COLLECTORS` (the core guard test enforces the
16710
+ * partition), forcing a conscious classification.
16711
+ */
16712
+ declare const UNBOUNDED_CROSS_FILE_RULE_IDS: ReadonlySet<string>;
16713
+ /**
16714
+ * Runs the collectors for `ruleIds` over one file and returns every
16715
+ * filesystem probe they made — the file's cross-file dependency set.
16716
+ *
16717
+ * Returns `null` (caller must treat the file as unfingerprintable and always
16718
+ * re-lint it) when the file has a fatal parse error — an execution the
16719
+ * collectors cannot mirror — or when a requested rule has no collector.
16720
+ */
16721
+ declare const collectCrossFileDependencyProbes: (input: {
16722
+ absoluteFilePath: string;
16723
+ sourceText: string;
16724
+ ruleIds: ReadonlyArray<string>;
16725
+ }) => CrossFileProbeTrace | null;
16726
+ //#endregion
16647
16727
  //#region src/plugin/rules/security-scan/utils/classify-security-scan-file.d.ts
16648
16728
  interface SecurityScanFileClassification {
16649
16729
  readonly bucket: "priority" | "artifact" | "other";
@@ -16657,5 +16737,5 @@ declare const REACT_NATIVE_DEPENDENCY_NAMES: ReadonlySet<string>;
16657
16737
  declare const REACT_NATIVE_DEPENDENCY_PREFIXES: ReadonlyArray<string>;
16658
16738
  declare const isReactNativeDependencyName: (dependencyName: string) => boolean;
16659
16739
  //#endregion
16660
- export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, type EsTreeNode, FRAMEWORK_SPECIFIC_RULE_KEYS, type FileScan, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, type OxlintRuleSeverity, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, type Rule, type RuleFramework, type RulePlugin, type RuleSeverity, type RuleVisitors, type ScanFinding, type ScannedFile, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, classifySecurityScanFile, plugin as default, isReactNativeDependencyName, shouldReadSecurityScanContent };
16740
+ export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, type CrossFileProbeTrace, EXTERNAL_RULES, type EsTreeNode, FRAMEWORK_SPECIFIC_RULE_KEYS, type FileScan, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, type OxlintRuleSeverity, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, type Rule, type RuleFramework, type RulePlugin, type RuleSeverity, type RuleVisitors, type ScanFinding, type ScannedFile, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, plugin as default, isReactNativeDependencyName, shouldReadSecurityScanContent };
16661
16741
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -15564,6 +15564,30 @@ const nextjsInlineScriptMissingId = defineRule({
15564
15564
  } })
15565
15565
  });
15566
15566
  //#endregion
15567
+ //#region src/plugin/utils/cross-file-probe-recorder.ts
15568
+ let activeProbeTrace = null;
15569
+ const recordExistenceProbe = (absolutePath) => {
15570
+ activeProbeTrace?.existencePaths.add(absolutePath);
15571
+ };
15572
+ const recordContentProbe = (absolutePath) => {
15573
+ activeProbeTrace?.contentPaths.add(absolutePath);
15574
+ };
15575
+ const isProbeRecorderActive = () => activeProbeTrace !== null;
15576
+ const collectCrossFileProbes = (collect) => {
15577
+ const previousTrace = activeProbeTrace;
15578
+ const trace = {
15579
+ existencePaths: /* @__PURE__ */ new Set(),
15580
+ contentPaths: /* @__PURE__ */ new Set()
15581
+ };
15582
+ activeProbeTrace = trace;
15583
+ try {
15584
+ collect();
15585
+ } finally {
15586
+ activeProbeTrace = previousTrace;
15587
+ }
15588
+ return trace;
15589
+ };
15590
+ //#endregion
15567
15591
  //#region src/plugin/utils/parse-export-specifiers.ts
15568
15592
  const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
15569
15593
  const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
@@ -15599,6 +15623,7 @@ const collectSourceTextExportNames = (sourceText) => {
15599
15623
  };
15600
15624
  const exportNamesCache = /* @__PURE__ */ new Map();
15601
15625
  const doesModuleExportName = (filePath, exportedName) => {
15626
+ recordContentProbe(filePath);
15602
15627
  try {
15603
15628
  const fileStat = fs.statSync(filePath);
15604
15629
  const cached = exportNamesCache.get(filePath);
@@ -16643,6 +16668,7 @@ const parseSourceText = (filename, sourceText) => {
16643
16668
  };
16644
16669
  const parseCache = /* @__PURE__ */ new Map();
16645
16670
  const parseSourceFile = (absoluteFilePath) => {
16671
+ if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
16646
16672
  let fileStat;
16647
16673
  try {
16648
16674
  fileStat = fs.statSync(absoluteFilePath);
@@ -16789,15 +16815,27 @@ const classifyBarrelModule = (sourceText) => {
16789
16815
  };
16790
16816
  const getBarrelIndexModuleInfo = (filePath) => {
16791
16817
  if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
16818
+ recordContentProbe(filePath);
16819
+ let fileStat;
16820
+ try {
16821
+ fileStat = fs.statSync(filePath);
16822
+ } catch {
16823
+ fileStat = null;
16824
+ }
16792
16825
  const cachedResult = barrelIndexModuleInfoCache.get(filePath);
16793
- if (cachedResult !== void 0) return cachedResult;
16826
+ if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
16827
+ if (fileStat === null) return createNonBarrelInfo();
16794
16828
  let moduleInfo = createNonBarrelInfo();
16795
16829
  try {
16796
16830
  moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
16797
16831
  } catch {
16798
16832
  moduleInfo = createNonBarrelInfo();
16799
16833
  }
16800
- barrelIndexModuleInfoCache.set(filePath, moduleInfo);
16834
+ barrelIndexModuleInfoCache.set(filePath, {
16835
+ mtimeMs: fileStat.mtimeMs,
16836
+ size: fileStat.size,
16837
+ moduleInfo
16838
+ });
16801
16839
  return moduleInfo;
16802
16840
  };
16803
16841
  const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
@@ -16826,6 +16864,7 @@ const PACKAGE_ENTRY_FIELDS = [
16826
16864
  "browser"
16827
16865
  ];
16828
16866
  const getExistingFilePath = (filePath) => {
16867
+ recordExistenceProbe(filePath);
16829
16868
  try {
16830
16869
  return fs.statSync(filePath).isFile() ? filePath : null;
16831
16870
  } catch {
@@ -16833,6 +16872,7 @@ const getExistingFilePath = (filePath) => {
16833
16872
  }
16834
16873
  };
16835
16874
  const getExistingDirectoryPath = (directoryPath) => {
16875
+ recordExistenceProbe(directoryPath);
16836
16876
  try {
16837
16877
  return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
16838
16878
  } catch {
@@ -16888,6 +16928,7 @@ const resolvePackageDirectoryEntry = (directoryPath) => {
16888
16928
  const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
16889
16929
  if (!existingDirectoryPath) return null;
16890
16930
  const packageJsonPath = path.join(existingDirectoryPath, "package.json");
16931
+ recordContentProbe(packageJsonPath);
16891
16932
  try {
16892
16933
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
16893
16934
  const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
@@ -16996,7 +17037,9 @@ const stripJsonComments = (text) => {
16996
17037
  }
16997
17038
  return output.replace(/,(\s*[}\]])/g, "$1");
16998
17039
  };
16999
- const parseTsconfigFile = (configFilePath) => {
17040
+ const parseTsconfigFile = (configFilePath, probedPaths) => {
17041
+ recordContentProbe(configFilePath);
17042
+ probedPaths.push(configFilePath);
17000
17043
  let sourceText;
17001
17044
  try {
17002
17045
  sourceText = fs.readFileSync(configFilePath, "utf8");
@@ -17025,8 +17068,8 @@ const parsePathsField = (pathsField) => {
17025
17068
  }
17026
17069
  return paths;
17027
17070
  };
17028
- const readResolvedTsconfig = (configFilePath, extendsDepth) => {
17029
- const parsed = parseTsconfigFile(configFilePath);
17071
+ const readResolvedTsconfig = (configFilePath, extendsDepth, probedPaths) => {
17072
+ const parsed = parseTsconfigFile(configFilePath, probedPaths);
17030
17073
  if (!parsed) return null;
17031
17074
  const configDirectory = path.dirname(configFilePath);
17032
17075
  const compilerOptions = isObjectRecord(parsed.compilerOptions) ? parsed.compilerOptions : {};
@@ -17040,7 +17083,7 @@ const readResolvedTsconfig = (configFilePath, extendsDepth) => {
17040
17083
  };
17041
17084
  if (typeof parsed.extends === "string" && extendsDepth < 8) {
17042
17085
  const parentPath = resolveExtendsPath(parsed.extends, configDirectory);
17043
- const inherited = parentPath ? readResolvedTsconfig(parentPath, extendsDepth + 1) : null;
17086
+ const inherited = parentPath ? readResolvedTsconfig(parentPath, extendsDepth + 1, probedPaths) : null;
17044
17087
  if (inherited) return inherited;
17045
17088
  }
17046
17089
  return hasExplicitBaseUrl ? {
@@ -17051,6 +17094,7 @@ const readResolvedTsconfig = (configFilePath, extendsDepth) => {
17051
17094
  };
17052
17095
  const configByFilePath = /* @__PURE__ */ new Map();
17053
17096
  const loadTsconfigCached = (configFilePath) => {
17097
+ recordContentProbe(configFilePath);
17054
17098
  let fileStat;
17055
17099
  try {
17056
17100
  fileStat = fs.statSync(configFilePath);
@@ -17058,11 +17102,16 @@ const loadTsconfigCached = (configFilePath) => {
17058
17102
  return null;
17059
17103
  }
17060
17104
  const cached = configByFilePath.get(configFilePath);
17061
- if (cached && cached.mtimeMs === fileStat.mtimeMs) return cached.config;
17062
- const config = readResolvedTsconfig(configFilePath, 0);
17105
+ if (cached && cached.mtimeMs === fileStat.mtimeMs) {
17106
+ for (const probedPath of cached.probedChainPaths) recordContentProbe(probedPath);
17107
+ return cached.config;
17108
+ }
17109
+ const probedChainPaths = [];
17110
+ const config = readResolvedTsconfig(configFilePath, 0, probedChainPaths);
17063
17111
  configByFilePath.set(configFilePath, {
17064
17112
  mtimeMs: fileStat.mtimeMs,
17065
- config
17113
+ config,
17114
+ probedChainPaths
17066
17115
  });
17067
17116
  return config;
17068
17117
  };
@@ -18909,10 +18958,23 @@ const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
18909
18958
  const findNearestPackageDirectory = (filename) => {
18910
18959
  if (!filename) return null;
18911
18960
  const fromCache = cachedPackageDirectoryByFilename.get(filename);
18912
- if (fromCache !== void 0) return fromCache;
18961
+ if (fromCache !== void 0) {
18962
+ if (isProbeRecorderActive()) {
18963
+ let probedDirectory = path.dirname(filename);
18964
+ while (true) {
18965
+ recordExistenceProbe(path.join(probedDirectory, "package.json"));
18966
+ if (probedDirectory === fromCache) break;
18967
+ const parentDirectory = path.dirname(probedDirectory);
18968
+ if (parentDirectory === probedDirectory) break;
18969
+ probedDirectory = parentDirectory;
18970
+ }
18971
+ }
18972
+ return fromCache;
18973
+ }
18913
18974
  let currentDirectory = path.dirname(filename);
18914
18975
  while (true) {
18915
18976
  const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
18977
+ recordExistenceProbe(candidatePackageJsonPath);
18916
18978
  let hasPackageJson = false;
18917
18979
  try {
18918
18980
  hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
@@ -18975,6 +19037,7 @@ const isWebFrameworkOnly = (packageJson) => {
18975
19037
  const classifyPackagePlatform = (filename) => {
18976
19038
  const packageDirectory = findNearestPackageDirectory(filename);
18977
19039
  if (!packageDirectory) return "unknown";
19040
+ recordContentProbe(path.join(packageDirectory, "package.json"));
18978
19041
  const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
18979
19042
  if (cached !== void 0) return cached;
18980
19043
  const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
@@ -47486,6 +47549,178 @@ const CROSS_FILE_RULE_IDS = new Set([
47486
47549
  "rn-prefer-expo-image"
47487
47550
  ]);
47488
47551
  //#endregion
47552
+ //#region src/plugin/cross-file-dependencies.ts
47553
+ const flattenImportEntries = (staticImports) => {
47554
+ const flattened = [];
47555
+ for (const staticImport of staticImports) for (const entry of staticImport.entries) {
47556
+ if (entry.importName.kind === "NamespaceObject") continue;
47557
+ const exportedName = entry.importName.kind === "Default" ? "default" : entry.importName.name ?? null;
47558
+ if (exportedName === null) continue;
47559
+ flattened.push({
47560
+ source: staticImport.moduleRequest.value,
47561
+ exportedName
47562
+ });
47563
+ }
47564
+ return flattened;
47565
+ };
47566
+ const collectNoBarrelImportDependencies = ({ absoluteFilePath, staticImports }) => {
47567
+ let didProbePackagePlatform = false;
47568
+ for (const staticImport of staticImports) {
47569
+ const source = staticImport.moduleRequest.value;
47570
+ if (!source.startsWith(".")) continue;
47571
+ const runtimeEntries = staticImport.entries.filter((entry) => !entry.isType);
47572
+ if (runtimeEntries.length === 0) continue;
47573
+ const resolvedImportPath = resolveRelativeImportPath(absoluteFilePath, source);
47574
+ if (!resolvedImportPath || !isBarrelIndexModule(resolvedImportPath)) continue;
47575
+ if (!didProbePackagePlatform) {
47576
+ didProbePackagePlatform = true;
47577
+ classifyPackagePlatform(absoluteFilePath);
47578
+ }
47579
+ for (const entry of runtimeEntries) {
47580
+ const importedName = entry.importName.kind === "Default" ? "default" : entry.importName.name;
47581
+ if (importedName) resolveBarrelExportFilePath(resolvedImportPath, importedName);
47582
+ }
47583
+ }
47584
+ };
47585
+ const collectNextjsMissingMetadataDependencies = ({ absoluteFilePath }) => {
47586
+ if (!PAGE_FILE_PATTERN.test(absoluteFilePath)) return;
47587
+ if (INTERNAL_PAGE_PATH_PATTERN.test(absoluteFilePath)) return;
47588
+ hasAncestorMetadataLayout(absoluteFilePath);
47589
+ };
47590
+ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports }) => {
47591
+ if (!PAGE_OR_LAYOUT_FILE_PATTERN.test(absoluteFilePath)) return;
47592
+ if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
47593
+ for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
47594
+ };
47595
+ const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
47596
+ const namedUseReducerLocals = /* @__PURE__ */ new Set();
47597
+ const reactObjectLocals = /* @__PURE__ */ new Set();
47598
+ for (const staticImport of staticImports) {
47599
+ if (staticImport.moduleRequest.value !== "react") continue;
47600
+ for (const entry of staticImport.entries) if (entry.importName.kind === "Name" && entry.importName.name === "useReducer") namedUseReducerLocals.add(entry.localName.value);
47601
+ else if (entry.importName.kind === "Default" || entry.importName.kind === "NamespaceObject") reactObjectLocals.add(entry.localName.value);
47602
+ }
47603
+ if (!(namedUseReducerLocals.size > 0 || reactObjectLocals.size > 0 && (sourceText.includes("useReducer") || sourceText.includes("\\u")))) return;
47604
+ const identifierName = (node) => {
47605
+ if (node?.type !== "Identifier") return null;
47606
+ const { name } = node;
47607
+ return typeof name === "string" ? name : null;
47608
+ };
47609
+ const isUseReducerCallee = (callee) => {
47610
+ const calleeName = identifierName(callee);
47611
+ if (calleeName !== null) return namedUseReducerLocals.has(calleeName);
47612
+ if (callee.type !== "MemberExpression") return false;
47613
+ const member = callee;
47614
+ const objectName = identifierName(member.object);
47615
+ return objectName !== null && reactObjectLocals.has(objectName) && identifierName(member.property) === "useReducer";
47616
+ };
47617
+ const reducerArgumentNames = /* @__PURE__ */ new Set();
47618
+ walkAst(program, (node) => {
47619
+ if (node.type !== "CallExpression") return;
47620
+ const call = node;
47621
+ if (!call.callee || !isUseReducerCallee(call.callee)) return;
47622
+ const reducerArgument = call.arguments?.[0];
47623
+ if (!reducerArgument) return;
47624
+ const argumentName = identifierName(stripParenExpression(reducerArgument));
47625
+ if (argumentName !== null) reducerArgumentNames.add(argumentName);
47626
+ });
47627
+ if (reducerArgumentNames.size === 0) return;
47628
+ for (const staticImport of staticImports) for (const entry of staticImport.entries) {
47629
+ if (entry.importName.kind === "NamespaceObject") continue;
47630
+ if (!reducerArgumentNames.has(entry.localName.value)) continue;
47631
+ const exportedName = entry.importName.kind === "Default" ? "default" : entry.importName.name;
47632
+ if (exportedName) resolveCrossFileFunctionExport(absoluteFilePath, staticImport.moduleRequest.value, exportedName);
47633
+ }
47634
+ };
47635
+ const collectJsxBoundaryNames = (program) => {
47636
+ const jsxNames = /* @__PURE__ */ new Set();
47637
+ walkAst(program, (node) => {
47638
+ if (node.type !== "JSXOpeningElement") return;
47639
+ const nameNode = node.name;
47640
+ if (!nameNode) return;
47641
+ if (nameNode.type === "JSXIdentifier") {
47642
+ const name = nameNode.name;
47643
+ if (typeof name === "string") jsxNames.add(name);
47644
+ } else if (nameNode.type === "JSXMemberExpression") {
47645
+ const property = nameNode.property;
47646
+ if (property?.type === "JSXIdentifier" && typeof property.name === "string") jsxNames.add(property.name);
47647
+ } else if (nameNode.type === "JSXNamespacedName") {
47648
+ const namespace = nameNode.namespace;
47649
+ if (typeof namespace?.name === "string") jsxNames.add(namespace.name);
47650
+ }
47651
+ });
47652
+ return jsxNames;
47653
+ };
47654
+ const collectRnNoRawTextDependencies = ({ absoluteFilePath, staticImports, program }) => {
47655
+ classifyPackagePlatform(absoluteFilePath);
47656
+ const jsxNames = collectJsxBoundaryNames(program);
47657
+ if (jsxNames.size === 0) return;
47658
+ for (const staticImport of staticImports) for (const entry of staticImport.entries) {
47659
+ if (entry.importName.kind === "NamespaceObject") continue;
47660
+ if (!jsxNames.has(entry.localName.value)) continue;
47661
+ const exportedName = entry.importName.kind === "Default" ? "default" : entry.importName.name;
47662
+ if (exportedName) resolveCrossFileFunctionExport(absoluteFilePath, staticImport.moduleRequest.value, exportedName);
47663
+ }
47664
+ };
47665
+ const collectRnPreferExpoImageDependencies = ({ absoluteFilePath }) => {
47666
+ classifyPackagePlatform(absoluteFilePath);
47667
+ };
47668
+ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
47669
+ ["no-barrel-import", collectNoBarrelImportDependencies],
47670
+ ["nextjs-missing-metadata", collectNextjsMissingMetadataDependencies],
47671
+ ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
47672
+ ["no-mutating-reducer-state", collectMutatingReducerDependencies],
47673
+ ["rn-no-raw-text", collectRnNoRawTextDependencies],
47674
+ ["rn-prefer-expo-image", collectRnPreferExpoImageDependencies]
47675
+ ]);
47676
+ /**
47677
+ * Cross-file rules whose dependency set CANNOT be soundly bounded — they are
47678
+ * excluded from fingerprinting and re-lint every file on every scan. Empty
47679
+ * today; a new cross-file rule must be added either here or to
47680
+ * `CROSS_FILE_DEPENDENCY_COLLECTORS` (the core guard test enforces the
47681
+ * partition), forcing a conscious classification.
47682
+ */
47683
+ const UNBOUNDED_CROSS_FILE_RULE_IDS = /* @__PURE__ */ new Set();
47684
+ /**
47685
+ * Runs the collectors for `ruleIds` over one file and returns every
47686
+ * filesystem probe they made — the file's cross-file dependency set.
47687
+ *
47688
+ * Returns `null` (caller must treat the file as unfingerprintable and always
47689
+ * re-lint it) when the file has a fatal parse error — an execution the
47690
+ * collectors cannot mirror — or when a requested rule has no collector.
47691
+ */
47692
+ const collectCrossFileDependencyProbes = (input) => {
47693
+ const collectors = [];
47694
+ for (const ruleId of input.ruleIds) {
47695
+ const collector = CROSS_FILE_DEPENDENCY_COLLECTORS.get(ruleId);
47696
+ if (!collector) return null;
47697
+ collectors.push(collector);
47698
+ }
47699
+ const absoluteFilePath = normalizeFilename(input.absoluteFilePath);
47700
+ let staticImports;
47701
+ let program;
47702
+ try {
47703
+ const parseResult = parseSync(absoluteFilePath, input.sourceText, {
47704
+ astType: "ts",
47705
+ lang: resolveLang(absoluteFilePath)
47706
+ });
47707
+ if (parseResult.errors.some((parseError) => parseError.severity === "Error")) return null;
47708
+ staticImports = parseResult.module.staticImports;
47709
+ program = parseResult.program;
47710
+ } catch {
47711
+ return null;
47712
+ }
47713
+ const collectorInput = {
47714
+ absoluteFilePath,
47715
+ sourceText: input.sourceText,
47716
+ staticImports,
47717
+ program
47718
+ };
47719
+ return collectCrossFileProbes(() => {
47720
+ for (const collector of collectors) collector(collectorInput);
47721
+ });
47722
+ };
47723
+ //#endregion
47489
47724
  //#region src/plugin/rules/security-scan/utils/is-probably-text-file.ts
47490
47725
  const isProbablyTextFile = (relativePath) => TEXT_FILE_PATTERN.test(relativePath) || DOTENV_FILE_PATTERN.test(relativePath);
47491
47726
  //#endregion
@@ -47511,6 +47746,6 @@ const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGen
47511
47746
  //#region src/index.ts
47512
47747
  var src_default = plugin;
47513
47748
  //#endregion
47514
- export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, classifySecurityScanFile, src_default as default, isReactNativeDependencyName, shouldReadSecurityScanContent };
47749
+ export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, shouldReadSecurityScanContent };
47515
47750
 
47516
47751
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.3-dev.8325343",
3
+ "version": "0.6.3-dev.bdf8074",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",