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/lsp.js CHANGED
@@ -8,13 +8,14 @@ import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
9
  import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
11
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { createJiti } from "jiti";
16
16
  import * as Crypto from "node:crypto";
17
17
  import crypto, { createHash } from "node:crypto";
18
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
18
19
  import { gzipSync } from "node:zlib";
19
20
  import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
20
21
  import { TextDocument } from "vscode-languageserver-textdocument";
@@ -32986,6 +32987,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32986
32987
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32987
32988
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32988
32989
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32990
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
32991
+ const CORE_PACKAGE_VERSION = "0.6.3";
32992
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32993
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32989
32994
  const SUPPLY_CHAIN_PLUGIN = "socket";
32990
32995
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32991
32996
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -35646,6 +35651,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35646
35651
  if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35647
35652
  return true;
35648
35653
  } }) {};
35654
+ /**
35655
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
35656
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
35657
+ * diagnostics for every file whose dependency fingerprint (the probe set the
35658
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
35659
+ * rest. Only reachable when the per-file lint cache itself is on (the
35660
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
35661
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
35662
+ *
35663
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35664
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
35665
+ * per-file cache but run the always-fresh sidecar over every hit
35666
+ * (the pre-cache behavior).
35667
+ *
35668
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
35669
+ */
35670
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
35671
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35672
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
35673
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35674
+ if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
35675
+ return true;
35676
+ } }) {};
35677
+ /**
35678
+ * Whether the whole-project dead-code result cache
35679
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35680
+ * whose inputs (source tree, manifests, configs, analyzer version) are
35681
+ * unchanged replays the stored diagnostics instead of re-running the
35682
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
35683
+ *
35684
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
35685
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
35686
+ *
35687
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
35688
+ */
35689
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
35690
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35691
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
35692
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35693
+ if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
35694
+ return true;
35695
+ } }) {};
35649
35696
  const DIAGNOSTIC_SURFACES = [
35650
35697
  "cli",
35651
35698
  "prComment",
@@ -37286,6 +37333,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37286
37333
  return [...seen].filter((pattern) => pattern.length > 0);
37287
37334
  };
37288
37335
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37336
+ const atomicWriteJson = (filePath, value) => {
37337
+ try {
37338
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
37339
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
37340
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
37341
+ NFS.renameSync(temporaryPath, filePath);
37342
+ } catch {
37343
+ return;
37344
+ }
37345
+ };
37346
+ const failOpenReadJson = (filePath, fallback) => {
37347
+ try {
37348
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
37349
+ } catch {
37350
+ return fallback;
37351
+ }
37352
+ };
37353
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
37354
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
37355
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
37356
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37357
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37358
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
37359
+ const fingerprints = [];
37360
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37361
+ if (!isFingerprintedFile(name)) continue;
37362
+ try {
37363
+ const fileStat = NFS.statSync(absolutePath);
37364
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
37365
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
37366
+ } catch {}
37367
+ }
37368
+ return fingerprints.sort();
37369
+ };
37370
+ const bundledRequire$2 = createRequire(import.meta.url);
37371
+ const resolveDeslopVersion = () => {
37372
+ try {
37373
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
37374
+ return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
37375
+ } catch {
37376
+ return "unknown";
37377
+ }
37378
+ };
37379
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37380
+ schemaVersion: 1,
37381
+ coreVersion: input.coreVersion,
37382
+ deslopVersion: resolveDeslopVersion(),
37383
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37384
+ entryPatterns: input.entryPatterns,
37385
+ ignorePatterns: input.ignorePatterns,
37386
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37387
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
37388
+ })).digest("hex");
37389
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37390
+ const decodeCachedDiagnostics = (raw) => {
37391
+ try {
37392
+ for (const entry of raw) validateDiagnostic$2(entry);
37393
+ return raw;
37394
+ } catch {
37395
+ return null;
37396
+ }
37397
+ };
37398
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
37399
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
37400
+ if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
37401
+ return decodeCachedDiagnostics(persisted.diagnostics);
37402
+ };
37403
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
37404
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
37405
+ version: 1,
37406
+ key: cacheKey,
37407
+ diagnostics
37408
+ });
37409
+ };
37289
37410
  const readSystemFacts = () => ({
37290
37411
  availableCores: os.availableParallelism(),
37291
37412
  totalMemoryBytes: os.totalmem(),
@@ -37354,6 +37475,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37354
37475
  releaseSlot();
37355
37476
  }
37356
37477
  };
37478
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
37479
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
37480
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
37481
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
37482
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
37483
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
37484
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
37485
+ };
37486
+ const resolveUserCacheScope = () => {
37487
+ try {
37488
+ return String(process.getuid?.() ?? os.userInfo().username);
37489
+ } catch {
37490
+ return "shared";
37491
+ }
37492
+ };
37357
37493
  /**
37358
37494
  * Resolves a path to its canonical, symlink-free form, falling back to
37359
37495
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37407,6 +37543,14 @@ process.stdin.on("end", () => {
37407
37543
  circularDependencies: result.circularDependencies.map((cycle) => ({
37408
37544
  files: cycle.files,
37409
37545
  })),
37546
+ ...(result.incrementalCacheStats
37547
+ ? {
37548
+ summaryCacheStats: {
37549
+ hits: result.incrementalCacheStats.summaryHits,
37550
+ misses: result.incrementalCacheStats.summaryMisses,
37551
+ },
37552
+ }
37553
+ : {}),
37410
37554
  });
37411
37555
 
37412
37556
  const serializeError = (error) =>
@@ -37430,6 +37574,9 @@ process.stdin.on("end", () => {
37430
37574
  ...(workerInput.ignorePatterns.length > 0
37431
37575
  ? { ignorePatterns: workerInput.ignorePatterns }
37432
37576
  : {}),
37577
+ ...(workerInput.incrementalCachePath
37578
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
37579
+ : {}),
37433
37580
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37434
37581
  // unusedDependencies, circularDependencies). Everything else deslop can compute
37435
37582
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -37440,12 +37587,18 @@ process.stdin.on("end", () => {
37440
37587
  // are the single most expensive pass — duplicate-block detection alone was
37441
37588
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37442
37589
  // speedup on a large repo.
37443
- // Both are provably safe: the consumed graph findings are computed by their own
37590
+ // - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
37591
+ // simplifiable functions, identity wrappers, …) — ~120 ms of discarded
37592
+ // output per scan, and skipping them lets the incremental cache drop the
37593
+ // DRY-pattern summary fields (the largest slice of the cache file).
37594
+ // All are provably safe: the consumed graph findings are computed by their own
37444
37595
  // detectors, independent of these passes (confirmed byte-identical on
37445
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37446
- // needs it for path-alias resolution in the import graph.
37596
+ // excalidraw + mui-material + sentry; re-verified on sentry after the
37597
+ // reportRedundancy flip). tsConfigPath stays — the module resolver needs it
37598
+ // for path-alias resolution in the import graph.
37447
37599
  semantic: { enabled: false },
37448
37600
  reportCodeQuality: false,
37601
+ reportRedundancy: false,
37449
37602
  };
37450
37603
  const result = await analyze(defineConfig(config));
37451
37604
  emit({ ok: true, result: normalizeResult(result) });
@@ -37529,13 +37682,23 @@ const parseCircularDependencies = (value) => {
37529
37682
  }
37530
37683
  return circularDependencies;
37531
37684
  };
37685
+ const parseSummaryCacheStats = (value) => {
37686
+ if (!isRecord(value)) return void 0;
37687
+ if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
37688
+ return {
37689
+ hits: value.hits,
37690
+ misses: value.misses
37691
+ };
37692
+ };
37532
37693
  const parseDeadCodeWorkerResult = (value) => {
37533
37694
  if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
37695
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
37534
37696
  return {
37535
37697
  unusedFiles: parseUnusedFiles(value.unusedFiles),
37536
37698
  unusedExports: parseUnusedExports(value.unusedExports),
37537
37699
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
37538
- circularDependencies: parseCircularDependencies(value.circularDependencies)
37700
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
37701
+ ...summaryCacheStats ? { summaryCacheStats } : {}
37539
37702
  };
37540
37703
  };
37541
37704
  const parseDeadCodeWorkerError = (value) => {
@@ -37649,17 +37812,35 @@ const checkDeadCode = async (options) => {
37649
37812
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37650
37813
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37651
37814
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37815
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
37816
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
37817
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
37818
+ rootDirectory,
37819
+ entryPatterns,
37820
+ ignorePatterns,
37821
+ tsConfigPath,
37822
+ deslopJsModuleSpecifier,
37823
+ coreVersion: CORE_PACKAGE_VERSION
37824
+ }) : null;
37825
+ if (cacheKey !== null) {
37826
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37827
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
37828
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37829
+ }
37830
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
37652
37831
  const spawnAndRun = () => {
37653
37832
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37654
37833
  rootDirectory,
37655
37834
  entryPatterns,
37656
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37835
+ tsConfigPath,
37657
37836
  ignorePatterns,
37658
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37659
- parseConcurrency: options.parseConcurrency
37837
+ deslopJsModuleSpecifier,
37838
+ parseConcurrency: options.parseConcurrency,
37839
+ incrementalCachePath
37660
37840
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37661
37841
  };
37662
37842
  const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37843
+ if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
37663
37844
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37664
37845
  const diagnostics = [];
37665
37846
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37716,6 +37897,7 @@ const checkDeadCode = async (options) => {
37716
37897
  category: DEAD_CODE_CATEGORY
37717
37898
  });
37718
37899
  }
37900
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
37719
37901
  return diagnostics;
37720
37902
  };
37721
37903
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -37818,12 +38000,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
37818
38000
  */
37819
38001
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37820
38002
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
38003
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
37821
38004
  return yield* tryPromise({
37822
38005
  try: (signal) => checkDeadCode({
37823
38006
  rootDirectory: input.rootDirectory,
37824
38007
  parseConcurrency: input.parseConcurrency,
37825
38008
  workerTimeoutMs: input.workerTimeoutMs,
37826
- abortSignal: signal
38009
+ abortSignal: signal,
38010
+ cacheEnabled,
38011
+ onCacheOutcome: input.onCacheOutcome,
38012
+ onSummaryCacheStats: input.onSummaryCacheStats
37827
38013
  }),
37828
38014
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37829
38015
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38693,7 +38879,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38693
38879
  }
38694
38880
  return enabled;
38695
38881
  };
38696
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38882
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
38697
38883
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38698
38884
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38699
38885
  const jsPlugins = [];
@@ -38705,6 +38891,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38705
38891
  if (!rule) continue;
38706
38892
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38707
38893
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38894
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
38708
38895
  if (rule.scan !== void 0) continue;
38709
38896
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38710
38897
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38749,33 +38936,152 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38749
38936
  }
38750
38937
  };
38751
38938
  };
38752
- const atomicWriteJson = (filePath, value) => {
38939
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
38940
+ const decodeFileDiagnostics = (raw) => {
38941
+ if (!Array.isArray(raw)) return null;
38753
38942
  try {
38754
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38755
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
38756
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38757
- NFS.renameSync(temporaryPath, filePath);
38943
+ for (const entry of raw) validateDiagnostic$1(entry);
38944
+ return raw;
38758
38945
  } catch {
38759
- return;
38946
+ return null;
38760
38947
  }
38761
38948
  };
38762
- const failOpenReadJson = (filePath, fallback) => {
38949
+ const emptyCache$1 = () => ({
38950
+ version: 1,
38951
+ rulesets: {}
38952
+ });
38953
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
38954
+ const entries = /* @__PURE__ */ new Map();
38955
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
38956
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38957
+ const bucket = persisted.rulesets[rulesetHash];
38958
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38959
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38960
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38961
+ if (decoded !== null) entries.set(fileKey, decoded);
38962
+ }
38963
+ return entries;
38964
+ };
38965
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38966
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38967
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
38968
+ return {
38969
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38970
+ store: (fileKey, diagnostics) => {
38971
+ entries.delete(fileKey);
38972
+ entries.set(fileKey, diagnostics);
38973
+ },
38974
+ persist: () => {
38975
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
38976
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38977
+ const existingBucket = rulesets[rulesetHash];
38978
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38979
+ const ourFiles = {};
38980
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38981
+ const cappedEntries = Object.entries({
38982
+ ...existingFiles,
38983
+ ...ourFiles
38984
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
38985
+ rulesets[rulesetHash] = {
38986
+ updatedAtMs: Date.now(),
38987
+ files: Object.fromEntries(cappedEntries)
38988
+ };
38989
+ atomicWriteJson(cacheFilePath, {
38990
+ version: 1,
38991
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38992
+ });
38993
+ }
38994
+ };
38995
+ };
38996
+ const hashFileContents = (filePath) => {
38763
38997
  try {
38764
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38998
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
38765
38999
  } catch {
38766
- return fallback;
39000
+ return null;
38767
39001
  }
38768
39002
  };
39003
+ const ABSENT_CONTENT_ANSWER = "absent";
39004
+ /**
39005
+ * Answers dependency probes for the sidecar lint cache — used both when
39006
+ * STORING an entry (turn the collector's probe paths into answers) and when
39007
+ * VERIFYING one (re-answer each stored probe against the current tree).
39008
+ * Store and verify must agree exactly, so both go through this one resolver.
39009
+ *
39010
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
39011
+ * (missing, a directory, permission failure). Source files reuse the
39012
+ * hashes the cache-partition loop already computed
39013
+ * (`contentHashByRelativePath`); everything else (package.json,
39014
+ * tsconfig, ignored files) is hashed on demand.
39015
+ * - `exists` — the existence class a module-resolution candidate probe
39016
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
39017
+ * `statSync` follows symlinks, matching the resolvers).
39018
+ *
39019
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
39020
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
39021
+ * tsconfig chain) repeat across many files' probe sets.
39022
+ */
39023
+ const createSidecarProbeAnswerResolver = (input) => {
39024
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
39025
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
39026
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
39027
+ const contentAnswer = (relativePath) => {
39028
+ const memoized = contentAnswerByPath.get(relativePath);
39029
+ if (memoized !== void 0) return memoized;
39030
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
39031
+ contentAnswerByPath.set(relativePath, answer);
39032
+ return answer;
39033
+ };
39034
+ const existsAnswer = (relativePath) => {
39035
+ const memoized = existsAnswerByPath.get(relativePath);
39036
+ if (memoized !== void 0) return memoized;
39037
+ let answer;
39038
+ try {
39039
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
39040
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
39041
+ } catch {
39042
+ answer = "none";
39043
+ }
39044
+ existsAnswerByPath.set(relativePath, answer);
39045
+ return answer;
39046
+ };
39047
+ return {
39048
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
39049
+ toRelativePath
39050
+ };
39051
+ };
38769
39052
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
38770
- const decodeFileDiagnostics = (raw) => {
38771
- if (!Array.isArray(raw)) return null;
39053
+ const isProbeKind = (value) => value === "content" || value === "exists";
39054
+ const decodeSidecarFileEntry = (raw) => {
39055
+ if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
39056
+ const probes = [];
39057
+ for (const rawProbe of raw.probes) {
39058
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
39059
+ const [kind, probePath, answer] = rawProbe;
39060
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
39061
+ probes.push({
39062
+ kind,
39063
+ path: probePath,
39064
+ answer
39065
+ });
39066
+ }
38772
39067
  try {
38773
- for (const entry of raw) validateDiagnostic(entry);
38774
- return raw;
39068
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
38775
39069
  } catch {
38776
39070
  return null;
38777
39071
  }
39072
+ return {
39073
+ probes,
39074
+ diagnostics: raw.diagnostics
39075
+ };
38778
39076
  };
39077
+ const encodeSidecarFileEntry = (entry) => ({
39078
+ probes: entry.probes.map((probe) => [
39079
+ probe.kind,
39080
+ probe.path,
39081
+ probe.answer
39082
+ ]),
39083
+ diagnostics: entry.diagnostics
39084
+ });
38779
39085
  const emptyCache = () => ({
38780
39086
  version: 1,
38781
39087
  rulesets: {}
@@ -38786,20 +39092,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38786
39092
  if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38787
39093
  const bucket = persisted.rulesets[rulesetHash];
38788
39094
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38789
- for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38790
- const decoded = decodeFileDiagnostics(rawDiagnostics);
39095
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
39096
+ const decoded = decodeSidecarFileEntry(rawEntry);
38791
39097
  if (decoded !== null) entries.set(fileKey, decoded);
38792
39098
  }
38793
39099
  return entries;
38794
39100
  };
38795
- const createFileLintCache = (cacheDirectory, rulesetHash) => {
38796
- const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
39101
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39102
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
38797
39103
  const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38798
39104
  return {
38799
39105
  lookup: (fileKey) => entries.get(fileKey) ?? null,
38800
- store: (fileKey, diagnostics) => {
39106
+ store: (fileKey, entry) => {
38801
39107
  entries.delete(fileKey);
38802
- entries.set(fileKey, diagnostics);
39108
+ entries.set(fileKey, entry);
38803
39109
  },
38804
39110
  persist: () => {
38805
39111
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
@@ -38807,7 +39113,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38807
39113
  const existingBucket = rulesets[rulesetHash];
38808
39114
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38809
39115
  const ourFiles = {};
38810
- for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
39116
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
38811
39117
  const cappedEntries = Object.entries({
38812
39118
  ...existingFiles,
38813
39119
  ...ourFiles
@@ -39822,13 +40128,6 @@ const validateRuleRegistration = () => {
39822
40128
  ].filter((entry) => entry !== null).join("; ");
39823
40129
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39824
40130
  };
39825
- const hashFileContents = (filePath) => {
39826
- try {
39827
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39828
- } catch {
39829
- return null;
39830
- }
39831
- };
39832
40131
  /**
39833
40132
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39834
40133
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -39893,21 +40192,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
39893
40192
  }
39894
40193
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
39895
40194
  };
39896
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39897
- const resolveReactDoctorCacheDir = (projectDirectory) => {
39898
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39899
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39900
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39901
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39902
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39903
- };
39904
- const resolveUserCacheScope = () => {
39905
- try {
39906
- return String(process.getuid?.() ?? os.userInfo().username);
39907
- } catch {
39908
- return "shared";
39909
- }
39910
- };
39911
40195
  /**
39912
40196
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39913
40197
  * the runner: once for the primary scan, once for the
@@ -39925,6 +40209,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39925
40209
  NFS.closeSync(fileHandle);
39926
40210
  }
39927
40211
  };
40212
+ /**
40213
+ * Attributes diagnostics back to the file that produced them by the
40214
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
40215
+ * attributed — the path forms don't align, so the caller must skip its cache
40216
+ * store rather than risk caching a wrong empty result for a file that
40217
+ * actually had diagnostics.
40218
+ */
40219
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
40220
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
40221
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
40222
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
40223
+ for (const diagnostic of diagnostics) {
40224
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
40225
+ if (file === void 0) return null;
40226
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
40227
+ fileDiagnostics.push(diagnostic);
40228
+ diagnosticsByFile.set(file, fileDiagnostics);
40229
+ }
40230
+ return diagnosticsByFile;
40231
+ };
40232
+ /**
40233
+ * Collects each file's cross-file dependency probes (via the plugin's
40234
+ * collectors) with their current answers. Fail-open per file: an unreadable /
40235
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
40236
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
40237
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
40238
+ * unawaited before a lint pass and awaits it at store time, hiding the
40239
+ * in-process parse + resolution cost under the subprocess wait. The loop
40240
+ * yields on the cooperative time budget so the child's stdout handling (and
40241
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
40242
+ * the module-level probe recorder never sees interleaved use. Never rejects.
40243
+ */
40244
+ const collectSidecarProbesForFiles = async (input) => {
40245
+ const buildProbes = (file) => {
40246
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
40247
+ let trace;
40248
+ try {
40249
+ trace = collectCrossFileDependencyProbes({
40250
+ absoluteFilePath,
40251
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
40252
+ ruleIds: input.boundedSidecarRuleIds
40253
+ });
40254
+ } catch {
40255
+ return null;
40256
+ }
40257
+ if (trace === null) return null;
40258
+ const probes = [];
40259
+ for (const contentPath of trace.contentPaths) {
40260
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
40261
+ probes.push({
40262
+ kind: "content",
40263
+ path: relativePath,
40264
+ answer: input.probeAnswers.answerFor("content", relativePath)
40265
+ });
40266
+ }
40267
+ for (const existencePath of trace.existencePaths) {
40268
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
40269
+ probes.push({
40270
+ kind: "exists",
40271
+ path: relativePath,
40272
+ answer: input.probeAnswers.answerFor("exists", relativePath)
40273
+ });
40274
+ }
40275
+ return probes;
40276
+ };
40277
+ const probesByFile = /* @__PURE__ */ new Map();
40278
+ let collectSliceStartedAt = performance.now();
40279
+ for (const file of input.files) {
40280
+ probesByFile.set(file, buildProbes(file));
40281
+ if (performance.now() - collectSliceStartedAt >= 12) {
40282
+ await yieldToEventLoop();
40283
+ collectSliceStartedAt = performance.now();
40284
+ }
40285
+ }
40286
+ return probesByFile;
40287
+ };
40288
+ /**
40289
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
40290
+ * the next scan can replay instead of re-linting.
40291
+ */
40292
+ const storeSidecarEntries = (input) => {
40293
+ let didStoreAnyEntry = false;
40294
+ for (const pass of input.storablePasses) {
40295
+ if (!pass.isTrusted) continue;
40296
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
40297
+ if (diagnosticsByFile === null) continue;
40298
+ for (const file of pass.files) {
40299
+ const cacheKey = input.cacheKeyByFile.get(file);
40300
+ if (cacheKey === void 0) continue;
40301
+ const probes = pass.probesByFile.get(file);
40302
+ if (probes === void 0 || probes === null) continue;
40303
+ input.sidecarCache.store(cacheKey, {
40304
+ probes,
40305
+ diagnostics: diagnosticsByFile.get(file) ?? []
40306
+ });
40307
+ didStoreAnyEntry = true;
40308
+ }
40309
+ }
40310
+ if (didStoreAnyEntry) input.sidecarCache.persist();
40311
+ };
39928
40312
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39929
40313
  /**
39930
40314
  * Detects an oxlint config-load crash caused by the optional
@@ -39966,7 +40350,7 @@ const reactHooksJsPluginDropNote = (error) => {
39966
40350
  * 6. always restore disable directives + clean up the temp dir
39967
40351
  */
39968
40352
  const runOxlint = async (options) => {
39969
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
40353
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
39970
40354
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39971
40355
  const severityControls = buildRuleSeverityControls(userConfig);
39972
40356
  validateRuleRegistration();
@@ -39984,7 +40368,8 @@ const runOxlint = async (options) => {
39984
40368
  severityControls,
39985
40369
  userPlugins,
39986
40370
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
39987
- ruleSelection: overrides.ruleSelection
40371
+ ruleSelection: overrides.ruleSelection,
40372
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
39988
40373
  });
39989
40374
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39990
40375
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -40087,15 +40472,18 @@ const runOxlint = async (options) => {
40087
40472
  });
40088
40473
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
40089
40474
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40475
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
40090
40476
  const missFiles = [];
40091
40477
  const replayedDiagnostics = [];
40092
40478
  let hashSliceStartedAt = performance.now();
40093
40479
  for (const candidateFile of candidateFiles) {
40094
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40480
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
40481
+ const contentHash = hashFileContents(absoluteCandidatePath);
40095
40482
  if (contentHash === null) {
40096
40483
  missFiles.push(candidateFile);
40097
40484
  continue;
40098
40485
  }
40486
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
40099
40487
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
40100
40488
  cacheKeyByFile.set(candidateFile, cacheKey);
40101
40489
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -40107,43 +40495,111 @@ const runOxlint = async (options) => {
40107
40495
  }
40108
40496
  }
40109
40497
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
40498
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
40499
+ extendsPaths: [],
40500
+ ruleSelection: "sidecar"
40501
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
40502
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40503
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
40504
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
40505
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
40506
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
40507
+ config: buildConfig({
40508
+ extendsPaths: [],
40509
+ ruleSelection: "sidecar",
40510
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
40511
+ }),
40512
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
40513
+ ignorePatterns: combinedPatterns,
40514
+ tsconfigContent
40515
+ })) : null;
40516
+ const probeAnswers = createSidecarProbeAnswerResolver({
40517
+ rootDirectory,
40518
+ contentHashByRelativePath
40519
+ });
40520
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40521
+ files: missFiles,
40522
+ rootDirectory,
40523
+ boundedSidecarRuleIds,
40524
+ probeAnswers
40525
+ });
40110
40526
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
40111
40527
  extendsPaths: [],
40112
40528
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
40113
40529
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
40114
40530
  const missFileSet = new Set(missFiles);
40115
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
40531
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
40532
+ const sidecarReplayedDiagnostics = [];
40533
+ const sidecarLintFiles = [];
40534
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
40535
+ else {
40536
+ let verifySliceStartedAt = performance.now();
40537
+ for (const hitFile of hitFiles) {
40538
+ const cacheKey = cacheKeyByFile.get(hitFile);
40539
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
40540
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
40541
+ else sidecarLintFiles.push(hitFile);
40542
+ if (performance.now() - verifySliceStartedAt >= 12) {
40543
+ await yieldToEventLoop();
40544
+ verifySliceStartedAt = performance.now();
40545
+ }
40546
+ }
40547
+ }
40548
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
40549
+ files: sidecarLintFiles,
40550
+ rootDirectory,
40551
+ boundedSidecarRuleIds,
40552
+ probeAnswers
40553
+ });
40554
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
40555
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
40556
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
40116
40557
  extendsPaths: [],
40117
- ruleSelection: "sidecar"
40118
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
40558
+ ruleSelection: "sidecar",
40559
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
40560
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
40561
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
40562
+ extendsPaths: [],
40563
+ ruleSelection: "sidecar",
40564
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
40565
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
40566
+ diagnostics: [],
40567
+ didDropReactHooksJsPlugin: false,
40568
+ hadPartialFailure: false
40569
+ };
40119
40570
  const freshCacheableDiagnostics = [];
40120
40571
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
40121
40572
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
40122
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
40123
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
40124
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
40125
- let isAttributionSound = true;
40126
- for (const diagnostic of freshCacheableDiagnostics) {
40127
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
40128
- if (missFile === void 0) {
40129
- isAttributionSound = false;
40130
- break;
40131
- }
40132
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
40133
- fileDiagnostics.push(diagnostic);
40134
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
40135
- }
40136
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
40573
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
40574
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
40575
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
40137
40576
  for (const missFile of missFiles) {
40138
40577
  const cacheKey = cacheKeyByFile.get(missFile);
40139
40578
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
40140
40579
  }
40141
40580
  cache.persist();
40142
40581
  }
40582
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
40583
+ sidecarCache,
40584
+ cacheKeyByFile,
40585
+ storablePasses: [{
40586
+ files: sidecarLintFiles,
40587
+ diagnostics: boundedSidecarResult.diagnostics,
40588
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
40589
+ probesByFile: await sidecarProbesTask
40590
+ }, {
40591
+ files: missFiles,
40592
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
40593
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
40594
+ probesByFile: await missProbesTask
40595
+ }]
40596
+ });
40143
40597
  return dedupeDiagnostics([
40144
40598
  ...replayedDiagnostics,
40145
40599
  ...fullResult.diagnostics,
40146
- ...sidecarResult.diagnostics
40600
+ ...sidecarReplayedDiagnostics,
40601
+ ...boundedSidecarResult.diagnostics,
40602
+ ...unboundedSidecarResult.diagnostics
40147
40603
  ]);
40148
40604
  }
40149
40605
  const baseArgs = makeBaseArgs(configPath);
@@ -40241,6 +40697,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40241
40697
  const concurrency = yield* OxlintConcurrency;
40242
40698
  const lintBatchOrdering = yield* LintBatchOrdering;
40243
40699
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
40700
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
40244
40701
  const collectedFailures = [];
40245
40702
  const diagnostics = yield* tryPromise({
40246
40703
  try: (signal) => runOxlint({
@@ -40259,7 +40716,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40259
40716
  },
40260
40717
  onFileProgress: input.onFileProgress,
40261
40718
  perFileLintCacheEnabled,
40719
+ sidecarLintCacheEnabled,
40262
40720
  onCacheStats: input.onCacheStats,
40721
+ onSidecarStats: input.onSidecarStats,
40263
40722
  spawnTimeoutMs,
40264
40723
  outputMaxBytes,
40265
40724
  concurrency,
@@ -41034,7 +41493,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41034
41493
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
41035
41494
  rootDirectory: scanDirectory,
41036
41495
  parseConcurrency: deadCodeParseConcurrency,
41037
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
41496
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41497
+ onCacheOutcome: (didHitCache) => {
41498
+ deadCodeCacheHit = didHitCache;
41499
+ },
41500
+ onSummaryCacheStats: (stats) => {
41501
+ deadCodeSummaryCacheHits = stats.hits;
41502
+ deadCodeSummaryCacheMisses = stats.misses;
41503
+ }
41038
41504
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41039
41505
  yield* set(deadCodeFailure, {
41040
41506
  didFail: true,
@@ -41062,6 +41528,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41062
41528
  let lastReportedTotalFileCount = 0;
41063
41529
  let lintCacheHitFileCount = null;
41064
41530
  let lintCacheTotalFileCount = null;
41531
+ let lintSidecarReplayedFileCount = null;
41532
+ let lintSidecarTotalFileCount = null;
41533
+ let deadCodeCacheHit = null;
41534
+ let deadCodeSummaryCacheHits = null;
41535
+ let deadCodeSummaryCacheMisses = null;
41065
41536
  const baseLintStream = linterService.run({
41066
41537
  rootDirectory: scanDirectory,
41067
41538
  project,
@@ -41081,6 +41552,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41081
41552
  lintCacheHitFileCount = cacheHitFileCount;
41082
41553
  lintCacheTotalFileCount = totalConsideredFileCount;
41083
41554
  },
41555
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
41556
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
41557
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
41558
+ },
41084
41559
  deadlineEpochMs: input.deadlineEpochMs
41085
41560
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41086
41561
  yield* set(lintFailure, {
@@ -41191,6 +41666,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41191
41666
  securityScanFailed,
41192
41667
  lintCacheHitFileCount,
41193
41668
  lintCacheTotalFileCount,
41669
+ lintSidecarReplayedFileCount,
41670
+ lintSidecarTotalFileCount,
41671
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41672
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
41673
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
41194
41674
  suppressedRuleCounts: transform.summarizeSuppressions()
41195
41675
  };
41196
41676
  }).pipe(withSpan("runInspect", { attributes: {
@@ -43570,5 +44050,5 @@ const startLanguageServer = () => {
43570
44050
  };
43571
44051
  //#endregion
43572
44052
  export { startLanguageServer };
43573
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="94a57ec3-d5a9-52ea-a743-e91d996065b8")}catch(e){}}();
43574
- //# debugId=94a57ec3-d5a9-52ea-a743-e91d996065b8
44053
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="17a3f533-8248-511a-859b-584197cefba6")}catch(e){}}();
44054
+ //# debugId=17a3f533-8248-511a-859b-584197cefba6