react-doctor 0.6.3-dev.e257a5e → 0.6.3-dev.ea9a775

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,7 +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";
32989
32992
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
32993
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
32990
32994
  const SUPPLY_CHAIN_PLUGIN = "socket";
32991
32995
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32992
32996
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -35648,6 +35652,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
35648
35652
  return true;
35649
35653
  } }) {};
35650
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
+ /**
35651
35678
  * Whether the whole-project dead-code result cache
35652
35679
  * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
35653
35680
  * whose inputs (source tree, manifests, configs, analyzer version) are
@@ -37323,38 +37350,9 @@ const failOpenReadJson = (filePath, fallback) => {
37323
37350
  return fallback;
37324
37351
  }
37325
37352
  };
37326
- const ANALYZED_FILE_EXTENSIONS = new Set([
37327
- ".ts",
37328
- ".tsx",
37329
- ".js",
37330
- ".jsx",
37331
- ".mts",
37332
- ".mjs",
37333
- ".cts",
37334
- ".cjs",
37335
- ".mdx",
37336
- ".astro",
37337
- ".graphql",
37338
- ".gql",
37339
- ".css",
37340
- ".scss",
37341
- ".vue",
37342
- ".svelte"
37343
- ]);
37344
- const ANALYZED_MANIFEST_NAMES = new Set([
37345
- "package.json",
37346
- "knip.json",
37347
- "app.json",
37348
- "pnpm-workspace.yaml",
37349
- "lerna.json",
37350
- "pnpm-lock.yaml",
37351
- "package-lock.json",
37352
- "yarn.lock",
37353
- "bun.lock",
37354
- "bun.lockb",
37355
- "deno.lock",
37356
- ".gitignore"
37357
- ]);
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]);
37358
37356
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
37359
37357
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
37360
37358
  const collectAnalyzedFileFingerprints = (rootDirectory) => {
@@ -37380,6 +37378,7 @@ const resolveDeslopVersion = () => {
37380
37378
  };
37381
37379
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
37382
37380
  schemaVersion: 1,
37381
+ coreVersion: input.coreVersion,
37383
37382
  deslopVersion: resolveDeslopVersion(),
37384
37383
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
37385
37384
  entryPatterns: input.entryPatterns,
@@ -37387,10 +37386,10 @@ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON
37387
37386
  tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
37388
37387
  files: collectAnalyzedFileFingerprints(input.rootDirectory)
37389
37388
  })).digest("hex");
37390
- const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
37389
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
37391
37390
  const decodeCachedDiagnostics = (raw) => {
37392
37391
  try {
37393
- for (const entry of raw) validateDiagnostic$1(entry);
37392
+ for (const entry of raw) validateDiagnostic$2(entry);
37394
37393
  return raw;
37395
37394
  } catch {
37396
37395
  return null;
@@ -37544,6 +37543,14 @@ process.stdin.on("end", () => {
37544
37543
  circularDependencies: result.circularDependencies.map((cycle) => ({
37545
37544
  files: cycle.files,
37546
37545
  })),
37546
+ ...(result.incrementalCacheStats
37547
+ ? {
37548
+ summaryCacheStats: {
37549
+ hits: result.incrementalCacheStats.summaryHits,
37550
+ misses: result.incrementalCacheStats.summaryMisses,
37551
+ },
37552
+ }
37553
+ : {}),
37547
37554
  });
37548
37555
 
37549
37556
  const serializeError = (error) =>
@@ -37567,6 +37574,9 @@ process.stdin.on("end", () => {
37567
37574
  ...(workerInput.ignorePatterns.length > 0
37568
37575
  ? { ignorePatterns: workerInput.ignorePatterns }
37569
37576
  : {}),
37577
+ ...(workerInput.incrementalCachePath
37578
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
37579
+ : {}),
37570
37580
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37571
37581
  // unusedDependencies, circularDependencies). Everything else deslop can compute
37572
37582
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -37577,12 +37587,18 @@ process.stdin.on("end", () => {
37577
37587
  // are the single most expensive pass — duplicate-block detection alone was
37578
37588
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37579
37589
  // speedup on a large repo.
37580
- // 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
37581
37595
  // detectors, independent of these passes (confirmed byte-identical on
37582
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37583
- // 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.
37584
37599
  semantic: { enabled: false },
37585
37600
  reportCodeQuality: false,
37601
+ reportRedundancy: false,
37586
37602
  };
37587
37603
  const result = await analyze(defineConfig(config));
37588
37604
  emit({ ok: true, result: normalizeResult(result) });
@@ -37666,13 +37682,23 @@ const parseCircularDependencies = (value) => {
37666
37682
  }
37667
37683
  return circularDependencies;
37668
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
+ };
37669
37693
  const parseDeadCodeWorkerResult = (value) => {
37670
37694
  if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
37695
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
37671
37696
  return {
37672
37697
  unusedFiles: parseUnusedFiles(value.unusedFiles),
37673
37698
  unusedExports: parseUnusedExports(value.unusedExports),
37674
37699
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
37675
- circularDependencies: parseCircularDependencies(value.circularDependencies)
37700
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
37701
+ ...summaryCacheStats ? { summaryCacheStats } : {}
37676
37702
  };
37677
37703
  };
37678
37704
  const parseDeadCodeWorkerError = (value) => {
@@ -37793,13 +37819,15 @@ const checkDeadCode = async (options) => {
37793
37819
  entryPatterns,
37794
37820
  ignorePatterns,
37795
37821
  tsConfigPath,
37796
- deslopJsModuleSpecifier
37822
+ deslopJsModuleSpecifier,
37823
+ coreVersion: CORE_PACKAGE_VERSION
37797
37824
  }) : null;
37798
37825
  if (cacheKey !== null) {
37799
37826
  const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
37800
37827
  options.onCacheOutcome?.(cachedDiagnostics !== null);
37801
37828
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
37802
37829
  }
37830
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
37803
37831
  const spawnAndRun = () => {
37804
37832
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37805
37833
  rootDirectory,
@@ -37807,10 +37835,12 @@ const checkDeadCode = async (options) => {
37807
37835
  tsConfigPath,
37808
37836
  ignorePatterns,
37809
37837
  deslopJsModuleSpecifier,
37810
- parseConcurrency: options.parseConcurrency
37838
+ parseConcurrency: options.parseConcurrency,
37839
+ incrementalCachePath
37811
37840
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37812
37841
  };
37813
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);
37814
37844
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37815
37845
  const diagnostics = [];
37816
37846
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37978,7 +38008,8 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37978
38008
  workerTimeoutMs: input.workerTimeoutMs,
37979
38009
  abortSignal: signal,
37980
38010
  cacheEnabled,
37981
- onCacheOutcome: input.onCacheOutcome
38011
+ onCacheOutcome: input.onCacheOutcome,
38012
+ onSummaryCacheStats: input.onSummaryCacheStats
37982
38013
  }),
37983
38014
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37984
38015
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -38848,7 +38879,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38848
38879
  }
38849
38880
  return enabled;
38850
38881
  };
38851
- 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 }) => {
38852
38883
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38853
38884
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38854
38885
  const jsPlugins = [];
@@ -38860,6 +38891,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38860
38891
  if (!rule) continue;
38861
38892
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38862
38893
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38894
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
38863
38895
  if (rule.scan !== void 0) continue;
38864
38896
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38865
38897
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38904,23 +38936,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38904
38936
  }
38905
38937
  };
38906
38938
  };
38907
- const validateDiagnostic = decodeUnknownSync(Diagnostic);
38939
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
38908
38940
  const decodeFileDiagnostics = (raw) => {
38909
38941
  if (!Array.isArray(raw)) return null;
38910
38942
  try {
38911
- for (const entry of raw) validateDiagnostic(entry);
38943
+ for (const entry of raw) validateDiagnostic$1(entry);
38912
38944
  return raw;
38913
38945
  } catch {
38914
38946
  return null;
38915
38947
  }
38916
38948
  };
38917
- const emptyCache = () => ({
38949
+ const emptyCache$1 = () => ({
38918
38950
  version: 1,
38919
38951
  rulesets: {}
38920
38952
  });
38921
- const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38953
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
38922
38954
  const entries = /* @__PURE__ */ new Map();
38923
- const persisted = failOpenReadJson(cacheFilePath, emptyCache());
38955
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
38924
38956
  if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38925
38957
  const bucket = persisted.rulesets[rulesetHash];
38926
38958
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
@@ -38932,7 +38964,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38932
38964
  };
38933
38965
  const createFileLintCache = (cacheDirectory, rulesetHash) => {
38934
38966
  const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38935
- const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38967
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
38936
38968
  return {
38937
38969
  lookup: (fileKey) => entries.get(fileKey) ?? null,
38938
38970
  store: (fileKey, diagnostics) => {
@@ -38940,7 +38972,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38940
38972
  entries.set(fileKey, diagnostics);
38941
38973
  },
38942
38974
  persist: () => {
38943
- const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
38975
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
38944
38976
  const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38945
38977
  const existingBucket = rulesets[rulesetHash];
38946
38978
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
@@ -38961,6 +38993,142 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38961
38993
  }
38962
38994
  };
38963
38995
  };
38996
+ const hashFileContents = (filePath) => {
38997
+ try {
38998
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
38999
+ } catch {
39000
+ return null;
39001
+ }
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
+ };
39052
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
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
+ }
39067
+ try {
39068
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
39069
+ } catch {
39070
+ return null;
39071
+ }
39072
+ return {
39073
+ probes,
39074
+ diagnostics: raw.diagnostics
39075
+ };
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
+ });
39085
+ const emptyCache = () => ({
39086
+ version: 1,
39087
+ rulesets: {}
39088
+ });
39089
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39090
+ const entries = /* @__PURE__ */ new Map();
39091
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39092
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39093
+ const bucket = persisted.rulesets[rulesetHash];
39094
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39095
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
39096
+ const decoded = decodeSidecarFileEntry(rawEntry);
39097
+ if (decoded !== null) entries.set(fileKey, decoded);
39098
+ }
39099
+ return entries;
39100
+ };
39101
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39102
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
39103
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
39104
+ return {
39105
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
39106
+ store: (fileKey, entry) => {
39107
+ entries.delete(fileKey);
39108
+ entries.set(fileKey, entry);
39109
+ },
39110
+ persist: () => {
39111
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39112
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39113
+ const existingBucket = rulesets[rulesetHash];
39114
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39115
+ const ourFiles = {};
39116
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
39117
+ const cappedEntries = Object.entries({
39118
+ ...existingFiles,
39119
+ ...ourFiles
39120
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
39121
+ rulesets[rulesetHash] = {
39122
+ updatedAtMs: Date.now(),
39123
+ files: Object.fromEntries(cappedEntries)
39124
+ };
39125
+ atomicWriteJson(cacheFilePath, {
39126
+ version: 1,
39127
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39128
+ });
39129
+ }
39130
+ };
39131
+ };
38964
39132
  const bundledRequire = createRequire(import.meta.url);
38965
39133
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38966
39134
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -39960,13 +40128,6 @@ const validateRuleRegistration = () => {
39960
40128
  ].filter((entry) => entry !== null).join("; ");
39961
40129
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39962
40130
  };
39963
- const hashFileContents = (filePath) => {
39964
- try {
39965
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39966
- } catch {
39967
- return null;
39968
- }
39969
- };
39970
40131
  /**
39971
40132
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
39972
40133
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -40048,6 +40209,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
40048
40209
  NFS.closeSync(fileHandle);
40049
40210
  }
40050
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
+ };
40051
40312
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
40052
40313
  /**
40053
40314
  * Detects an oxlint config-load crash caused by the optional
@@ -40089,7 +40350,7 @@ const reactHooksJsPluginDropNote = (error) => {
40089
40350
  * 6. always restore disable directives + clean up the temp dir
40090
40351
  */
40091
40352
  const runOxlint = async (options) => {
40092
- 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;
40093
40354
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
40094
40355
  const severityControls = buildRuleSeverityControls(userConfig);
40095
40356
  validateRuleRegistration();
@@ -40107,7 +40368,8 @@ const runOxlint = async (options) => {
40107
40368
  severityControls,
40108
40369
  userPlugins,
40109
40370
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
40110
- ruleSelection: overrides.ruleSelection
40371
+ ruleSelection: overrides.ruleSelection,
40372
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
40111
40373
  });
40112
40374
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
40113
40375
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -40210,15 +40472,18 @@ const runOxlint = async (options) => {
40210
40472
  });
40211
40473
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
40212
40474
  const cacheKeyByFile = /* @__PURE__ */ new Map();
40475
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
40213
40476
  const missFiles = [];
40214
40477
  const replayedDiagnostics = [];
40215
40478
  let hashSliceStartedAt = performance.now();
40216
40479
  for (const candidateFile of candidateFiles) {
40217
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
40480
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
40481
+ const contentHash = hashFileContents(absoluteCandidatePath);
40218
40482
  if (contentHash === null) {
40219
40483
  missFiles.push(candidateFile);
40220
40484
  continue;
40221
40485
  }
40486
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
40222
40487
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
40223
40488
  cacheKeyByFile.set(candidateFile, cacheKey);
40224
40489
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -40230,43 +40495,111 @@ const runOxlint = async (options) => {
40230
40495
  }
40231
40496
  }
40232
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
+ });
40233
40526
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
40234
40527
  extendsPaths: [],
40235
40528
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
40236
40529
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
40237
40530
  const missFileSet = new Set(missFiles);
40238
- 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({
40239
40557
  extendsPaths: [],
40240
- ruleSelection: "sidecar"
40241
- }), "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
+ };
40242
40570
  const freshCacheableDiagnostics = [];
40243
40571
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
40244
40572
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
40245
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
40246
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
40247
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
40248
- let isAttributionSound = true;
40249
- for (const diagnostic of freshCacheableDiagnostics) {
40250
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
40251
- if (missFile === void 0) {
40252
- isAttributionSound = false;
40253
- break;
40254
- }
40255
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
40256
- fileDiagnostics.push(diagnostic);
40257
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
40258
- }
40259
- 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) {
40260
40576
  for (const missFile of missFiles) {
40261
40577
  const cacheKey = cacheKeyByFile.get(missFile);
40262
40578
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
40263
40579
  }
40264
40580
  cache.persist();
40265
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
+ });
40266
40597
  return dedupeDiagnostics([
40267
40598
  ...replayedDiagnostics,
40268
40599
  ...fullResult.diagnostics,
40269
- ...sidecarResult.diagnostics
40600
+ ...sidecarReplayedDiagnostics,
40601
+ ...boundedSidecarResult.diagnostics,
40602
+ ...unboundedSidecarResult.diagnostics
40270
40603
  ]);
40271
40604
  }
40272
40605
  const baseArgs = makeBaseArgs(configPath);
@@ -40364,6 +40697,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40364
40697
  const concurrency = yield* OxlintConcurrency;
40365
40698
  const lintBatchOrdering = yield* LintBatchOrdering;
40366
40699
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
40700
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
40367
40701
  const collectedFailures = [];
40368
40702
  const diagnostics = yield* tryPromise({
40369
40703
  try: (signal) => runOxlint({
@@ -40382,7 +40716,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40382
40716
  },
40383
40717
  onFileProgress: input.onFileProgress,
40384
40718
  perFileLintCacheEnabled,
40719
+ sidecarLintCacheEnabled,
40385
40720
  onCacheStats: input.onCacheStats,
40721
+ onSidecarStats: input.onSidecarStats,
40386
40722
  spawnTimeoutMs,
40387
40723
  outputMaxBytes,
40388
40724
  concurrency,
@@ -41160,6 +41496,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41160
41496
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
41161
41497
  onCacheOutcome: (didHitCache) => {
41162
41498
  deadCodeCacheHit = didHitCache;
41499
+ },
41500
+ onSummaryCacheStats: (stats) => {
41501
+ deadCodeSummaryCacheHits = stats.hits;
41502
+ deadCodeSummaryCacheMisses = stats.misses;
41163
41503
  }
41164
41504
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41165
41505
  yield* set(deadCodeFailure, {
@@ -41188,7 +41528,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41188
41528
  let lastReportedTotalFileCount = 0;
41189
41529
  let lintCacheHitFileCount = null;
41190
41530
  let lintCacheTotalFileCount = null;
41531
+ let lintSidecarReplayedFileCount = null;
41532
+ let lintSidecarTotalFileCount = null;
41191
41533
  let deadCodeCacheHit = null;
41534
+ let deadCodeSummaryCacheHits = null;
41535
+ let deadCodeSummaryCacheMisses = null;
41192
41536
  const baseLintStream = linterService.run({
41193
41537
  rootDirectory: scanDirectory,
41194
41538
  project,
@@ -41208,6 +41552,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41208
41552
  lintCacheHitFileCount = cacheHitFileCount;
41209
41553
  lintCacheTotalFileCount = totalConsideredFileCount;
41210
41554
  },
41555
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
41556
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
41557
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
41558
+ },
41211
41559
  deadlineEpochMs: input.deadlineEpochMs
41212
41560
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
41213
41561
  yield* set(lintFailure, {
@@ -41318,7 +41666,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
41318
41666
  securityScanFailed,
41319
41667
  lintCacheHitFileCount,
41320
41668
  lintCacheTotalFileCount,
41669
+ lintSidecarReplayedFileCount,
41670
+ lintSidecarTotalFileCount,
41321
41671
  deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
41672
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
41673
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
41322
41674
  suppressedRuleCounts: transform.summarizeSuppressions()
41323
41675
  };
41324
41676
  }).pipe(withSpan("runInspect", { attributes: {
@@ -43698,5 +44050,5 @@ const startLanguageServer = () => {
43698
44050
  };
43699
44051
  //#endregion
43700
44052
  export { startLanguageServer };
43701
- !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]="46e851cc-7b86-5c73-afb0-e09296beab28")}catch(e){}}();
43702
- //# debugId=46e851cc-7b86-5c73-afb0-e09296beab28
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