react-doctor 0.9.13 → 0.9.14-dev.922616f

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.
Files changed (59) hide show
  1. package/dist/_react-doctor_require-typescript-DXQDYeWK.js +16 -0
  2. package/dist/{active-run-trace-dlIZeEvb.js → active-run-trace-Dewycxda.js} +3 -3
  3. package/dist/{active-scan-abort-registry-DwG6tSPy.js → active-scan-abort-registry-CCTgal0O.js} +3 -3
  4. package/dist/build-section-divider-CNFcGNYm.js +81 -0
  5. package/dist/{ci-DqnZCnrL.js → ci-CbTOIIel.js} +14 -13
  6. package/dist/cli.js +31 -30
  7. package/dist/constants-CW_854PH.js +1035 -0
  8. package/dist/constants-DNh19qg9.js +23 -0
  9. package/dist/{design-DqUidb1f.js → design-CPav17ye.js} +4 -4
  10. package/dist/{devtools-CC2PAZ85.js → devtools-D20sefkm.js} +3 -3
  11. package/dist/duplicate-jsx-worker.d.ts +1 -0
  12. package/dist/duplicate-jsx-worker.js +636 -0
  13. package/dist/{exit-gracefully-CrpeIbra.js → exit-gracefully-De_DePvj.js} +7 -7
  14. package/dist/fs-utils-C88IbqFa.js +45 -0
  15. package/dist/{get-east-asian-width-Bv2TD3Xj.js → get-east-asian-width-DRPy7dqz.js} +4 -4
  16. package/dist/{handle-error-BEYYtsCl.js → handle-error-B3GWdtbq.js} +6 -5
  17. package/dist/index.d.ts +8511 -8509
  18. package/dist/index.js +61698 -60577
  19. package/dist/{inspect-BjMaC5WV.js → inspect-vT3G27FR.js} +38 -27
  20. package/dist/{install-DxLmASQ5.js → install-C-_xdS80.js} +10 -9
  21. package/dist/{install-doctor-script-4EWviJL8.js → install-doctor-script-BFv3Iiwe.js} +17 -8
  22. package/dist/{install-github-workflow-BPznUbbo.js → install-github-workflow-C-rhKOiP.js} +10 -10
  23. package/dist/{install-react-doctor-Baq9hjoF.js → install-react-doctor-BvU99j_I.js} +15 -11
  24. package/dist/is-cache-globally-disabled-CoHiYz2i.js +10 -0
  25. package/dist/{jsx-runtime-DvynQ_wu.js → jsx-runtime-CiJj554e.js} +11 -10
  26. package/dist/oxlint-worker.d.ts +1 -0
  27. package/dist/oxlint-worker.js +225 -0
  28. package/dist/{path-format-DnHpmJwG.js → path-format-By20mo8A.js} +3 -3
  29. package/dist/{print-branded-header-DSB6i3kk.js → print-branded-header-DJMuy9Hr.js} +4 -4
  30. package/dist/project-analysis-worker.js +466 -481
  31. package/dist/{prompt-runtime-scan-url-BibDt9O1.js → prompt-runtime-scan-url-1YpMskOr.js} +6 -6
  32. package/dist/react-compiler-detection-worker.d.ts +1 -0
  33. package/dist/react-compiler-detection-worker.js +1029 -0
  34. package/dist/{read-object-file-DfWpkle4.js → read-object-file-CWvQd5ls.js} +4 -4
  35. package/dist/{record-metric-Cckscwg_.js → record-metric-DSQY-qOh.js} +57476 -57240
  36. package/dist/{report-error-C1YnoRNl.js → report-error-C1g5jtl7.js} +9 -8
  37. package/dist/{report-workflow-result-IdGRoBFN.js → report-workflow-result-it0G879R.js} +24 -11
  38. package/dist/{resolve-scope-Bb6fljxd.js → resolve-scope-DmsEUxUn.js} +7 -7
  39. package/dist/{rules-BcFekwN4.js → rules-BXkqekjc.js} +8 -7
  40. package/dist/{run-scan-app-CKHynQx8.js → run-scan-app-wCfH8LdA.js} +30 -23
  41. package/dist/{runtime-scan-7Y_mbqEa.js → runtime-scan-CXmDoQ2W.js} +16 -15
  42. package/dist/{sanitize-runtime-url-DzyBLbi3.js → sanitize-runtime-url-kA6U9dQF.js} +5 -5
  43. package/dist/{scan-DQCwBx6p.js → scan-Dd-VVq_7.js} +10 -9
  44. package/dist/{semver-DFtnEmN-.js → semver-BZIuy1gP.js} +3 -3
  45. package/dist/{should-fail-scan-gate-BYPsKIde.js → should-fail-scan-gate-YK9VAFNu.js} +1339 -1283
  46. package/dist/{should-skip-prompts-CKkbB3rC.js → should-skip-prompts-SdC04R_e.js} +4 -4
  47. package/dist/{should-use-tui-Crfoao-l.js → should-use-tui-D2fEl9q2.js} +4 -4
  48. package/dist/start-scan-preamble-D0QepYZ_.js +68 -0
  49. package/dist/start-scan-preamble-DVhXIPc0.d.ts +1 -0
  50. package/dist/start-scan-preamble.d.ts +1 -0
  51. package/dist/start-scan-preamble.js +4 -0
  52. package/dist/{telemetry-runtime-DKJyjz9o.js → telemetry-runtime-DOCJCTsL.js} +154 -5
  53. package/dist/{unref-stdin-SK3WfwxS.js → unref-stdin-D-NZBFpg.js} +3 -3
  54. package/dist/unwrap-typescript-expression-CrEPQUWz.js +14 -0
  55. package/dist/{version-CMkUAGmw.js → version-WpL8zILr.js} +5 -4
  56. package/dist/{why-By3I6_Ex.js → why-CnQMoK2D.js} +4 -4
  57. package/dist/{with-run-span-DxFGyW7k.js → with-run-span-B0SMAP_Z.js} +7 -6
  58. package/package.json +9 -7
  59. package/dist/build-section-divider-DELfSn3p.js +0 -216
@@ -1,24 +1,26 @@
1
1
 
2
- !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]="40d3b0cf-26ca-5238-9651-4a1de64ef1f0")}catch(e){}}();
3
- import { $ as Linter, A as SCAN_RESULT_CACHE_FILENAME, An as gen, At as discoverSupportedSubprojects, B as isNonInteractiveEnvironment, Bn as sync, Cn as error, Ct as collectSourceFileCountsByDirectory, Dn as Diagnostic, Dt as createNodeReadFileLinesSync, En as warn, Et as countOptInProjectRuleSelections, Fn as provide, G as isCiEnvironment, Gt as hashFileContents, Ht as groupBy, In as provideContext, J as Config, Jt as isPathInsideDirectory, K as isCiOrCodingAgentEnvironment, Kn as succeed, Kt as highlighter, Ln as provideService, Mn as map, Mt as filterPathsOutsideDirectories, Nn as orElseSucceed, Nt as filterSourceFiles, On as decodeUnknownSync, Ot as createOxlintSpawnSlots, Pn as promise, Q as LintPartialFailures, Qt as isScanComplete, Rn as runPromise, S as BASELINE_FILES_TEMP_DIR_PREFIX, Sn as Console, T as METRIC, Tn as log, Tt as computeDiagnosticDelta, U as ACTION_INPUT_ENVIRONMENT_VARIABLES, Ut as hasPublishedFixRecipe, Vn as try_, W as detectRunnerOs, Wn as mergeAll, X as Git, Xt as isReactDoctorError, Y as Files, Zn as ParentSpan, _ as VERSION, _n as shouldUseMaintainabilityLayer, _t as buildRuleSeverityControls, an as materializeSourceTree, ar as DIAGNOSTIC_CATEGORY_BUCKETS, at as Progress, bt as canonicalizeUserRuleKey, cn as resolveGithubActionsScoreMetadata, ct as Score, d as isJsonModeActive, dn as resolveScanConcurrency, dr as JSX_DUPLICATION_SOURCE_FILE_PATTERN, dt as SupplyChain, en as listSourceFiles, et as Maintainability, fn as resolveScanTarget, fr as JSX_FILE_PATTERN, gt as buildRuleDocsUrl, hr as SHARE_BASE_URL, in as mapWithConcurrency, it as PerFileLintCacheEnabled, jt as filterDiagnosticsForSurface, l as getRunId, ln as resolveLintBatchOrdering, lr as GIT_SHOW_MAX_BUFFER_BYTES, lt as SidecarLintCacheEnabled, mn as runInspect, mr as OXLINT_NODE_REQUIREMENT, mt as atomicWriteJson, n as recordDistribution, nn as listWorkspacePackages, nt as OxlintConcurrency, on as mergeReactDoctorConfigs, ot as Project, pn as restoreLegacyThrow, pr as MILLISECONDS_PER_SECOND, qt as isMonorepoRoot, r as toSpanAttributes, rt as OxlintSpawnSlots, sn as remainingDeadlineBudgetMs, sr as ENTERPRISE_CONTACT_URL, st as Reporter, t as recordCount, tn as listSourceFilesCooperative, tr as CI_URL, tt as NodeResolver, un as resolveReactDoctorCacheDir, ur as HTML_FILE_PATTERN, v as makeNoopConsole, vn as summarizeDiagnostics, vt as buildSkippedChecks, wt as computeConfigFingerprint, xn as yieldToEventLoop, xr as isFile, xt as classifyFileContext, yr as isDirectory, yt as calculateScore, z as CliInputError, zn as runSync, zt as getDiagnosticRuleIdentity } from "./record-metric-Cckscwg_.js";
4
- import { t as getTelemetryContext } from "./telemetry-runtime-DKJyjz9o.js";
5
- import { n as buildFooterLinkLines, t as buildSectionDivider } from "./build-section-divider-DELfSn3p.js";
6
- import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-DwG6tSPy.js";
7
- import { t as toForwardSlashes } from "./path-format-DnHpmJwG.js";
8
- import { C as isRecord, M as cliLogger, S as getPackageJsonPath, a as installReactDoctorScriptStep, c as spinner, k as runGit, n as findNearestPackageDirectory, o as isSpinnerSilent, s as setSpinnerSilent } from "./install-doctor-script-4EWviJL8.js";
9
- import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-BPznUbbo.js";
10
- import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-Bb6fljxd.js";
11
- import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-DxFGyW7k.js";
12
- import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-IdGRoBFN.js";
2
+ !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]="18783c0f-3bd5-5391-90cc-0fe9d78014f3")}catch(e){}}();
3
+ import { E as TRACKED_FILE_FLAGS_GIT_ARGUMENTS, Ft as HTML_FILE_PATTERN, Ht as MAINTAINABILITY_SOURCE_FILE_PATTERN, Jt as OXLINT_NODE_REQUIREMENT, K as resolveReactDoctorCacheDir, Nt as GIT_SHOW_MAX_BUFFER_BYTES, Rt as JSX_FILE_PATTERN, St as ENTERPRISE_CONTACT_URL, Wt as MILLISECONDS_PER_SECOND, _t as DIAGNOSTIC_CATEGORY_BUCKETS, c as HEAD_SHA_GIT_ARGUMENTS, dn as SHARE_BASE_URL, h as REPOSITORY_ROOT_GIT_ARGUMENTS, i as BASELINE_FILES_TEMP_DIR_PREFIX, k as WORKTREE_STATUS_GIT_ARGUMENTS, q as resolveScanConcurrency, u as METRIC, ut as CI_URL, v as SCAN_RESULT_CACHE_FILENAME } from "./constants-CW_854PH.js";
4
+ import { t as isCacheGloballyDisabled } from "./is-cache-globally-disabled-CoHiYz2i.js";
5
+ import { $ as buildSkippedChecks, A as Git, B as Progress, Bt as materializeSourceTree, C as ACTION_INPUT_ENVIRONMENT_VARIABLES, Dt as hashFileContents, E as isCiOrCodingAgentEnvironment, En as succeed, F as Maintainability, Ft as listSourceFiles, Gt as resolveScanTarget, H as Reporter, Ht as remainingDeadlineBudgetMs, I as NodeResolver, In as isDirectory, It as listSourceFilesCooperative, K as SupplyChain, Kt as restoreLegacyThrow, L as OxlintConcurrency, Lt as listWorkspacePackages, Mn as isMonorepoRoot, N as LintPartialFailures, Nn as yieldToEventLoop, Nt as isScanComplete, O as Config, Ot as highlighter, P as Linter, Q as buildRuleSeverityControls, R as OxlintSpawnSlots, Rn as isFile, T as isCiEnvironment, Tt as hasPublishedFixRecipe, U as Score, Ut as resolveGithubActionsScoreMetadata, V as Project, Vt as mergeReactDoctorConfigs, W as SidecarLintCacheEnabled, Wt as resolveLintBatchOrdering, Xt as shouldUseMaintainabilityLayer, Y as atomicWriteJson, Z as buildRuleDocsUrl, Zt as summarizeDiagnostics, _ as VERSION, _n as provideService, an as log, at as computeConfigFingerprint, b as isNonInteractiveEnvironment, bn as sync, cn as decodeUnknownSync, ct as createInvocationCaches, d as isJsonModeActive, en as warmDuplicateJsxWorker, et as calculateScore, fn as map, ft as discoverSupportedSubprojects, gn as provideContext, hn as provide, ht as filterSourceFiles, it as collectSourceFilesByDirectory, j as InvocationCaches, jn as ParentSpan, jt as isReactDoctorError, k as Files, kt as isPathInsideDirectory, l as getRunId, lt as createNodeReadFileLinesSync, mn as promise, mt as filterPathsOutsideDirectories, n as recordDistribution, nn as Console, nt as classifyFileContext, on as warn, ot as computeDiagnosticDelta, pn as orElseSucceed, pt as filterDiagnosticsForSurface, qt as runInspect, r as toSpanAttributes, rn as error, sn as Diagnostic, st as countOptInProjectRuleSelections, t as recordCount, tn as warmOxlintWorkerPool, tt as canonicalizeUserRuleKey, un as gen, ut as createOxlintSpawnSlots, v as makeNoopConsole, vn as runPromise, w as detectRunnerOs, wn as mergeAll, wt as groupBy, xn as try_, xt as getDiagnosticRuleIdentity, y as CliInputError, yn as runSync, z as PerFileLintCacheEnabled, zt as mapWithConcurrency } from "./record-metric-DSQY-qOh.js";
6
+ import { t as getTelemetryContext } from "./telemetry-runtime-DOCJCTsL.js";
7
+ import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-CCTgal0O.js";
8
+ import { n as buildFooterLinkLines, t as buildSectionDivider } from "./build-section-divider-CNFcGNYm.js";
9
+ import { t as toForwardSlashes } from "./path-format-By20mo8A.js";
10
+ import { t as typescriptBinding } from "./_react-doctor_require-typescript-DXQDYeWK.js";
11
+ import { A as runGitAsync, C as isRecord, N as cliLogger, S as getPackageJsonPath, a as installReactDoctorScriptStep, c as spinner, n as findNearestPackageDirectory, o as isSpinnerSilent, s as setSpinnerSilent } from "./install-doctor-script-BFv3Iiwe.js";
12
+ import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-C-rhKOiP.js";
13
+ import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-DmsEUxUn.js";
14
+ import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-B0SMAP_Z.js";
15
+ import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-it0G879R.js";
13
16
  import { createRequire } from "node:module";
14
- import { spawn } from "node:child_process";
15
- import * as path$1 from "node:path";
16
- import path from "node:path";
17
17
  import * as fs$2 from "node:fs";
18
18
  import fs, { mkdtempSync, rmSync } from "node:fs";
19
- import ts from "typescript";
20
- import crypto, { randomUUID } from "node:crypto";
19
+ import * as path$1 from "node:path";
20
+ import path from "node:path";
21
+ import { spawn } from "node:child_process";
21
22
  import { tmpdir } from "node:os";
23
+ import crypto, { randomUUID } from "node:crypto";
22
24
  import * as fs$1 from "node:fs/promises";
23
25
  import { performance } from "node:perf_hooks";
24
26
  //#region src/cli/utils/apply-observability.ts
@@ -102,7 +104,8 @@ const buildRuntimeLayers = (input) => {
102
104
  configSourceDirectory: input.configSourceDirectory
103
105
  }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, maintainabilityLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
104
106
  const layersWithConcurrency = input.oxlintConcurrency === void 0 ? baseLayers : mergeAll(baseLayers, succeed(OxlintConcurrency, input.oxlintConcurrency));
105
- return input.oxlintSpawnSlots === void 0 ? layersWithConcurrency : mergeAll(layersWithConcurrency, succeed(OxlintSpawnSlots, input.oxlintSpawnSlots));
107
+ const layersWithSpawnSlots = input.oxlintSpawnSlots === void 0 ? layersWithConcurrency : mergeAll(layersWithConcurrency, succeed(OxlintSpawnSlots, input.oxlintSpawnSlots));
108
+ return input.invocationCaches === void 0 ? layersWithSpawnSlots : mergeAll(layersWithSpawnSlots, succeed(InvocationCaches, input.invocationCaches));
106
109
  };
107
110
  //#endregion
108
111
  //#region src/cli/utils/diagnostic-grouping.ts
@@ -268,762 +271,347 @@ const resolveScoreUnavailableReason = (input) => {
268
271
  return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
269
272
  };
270
273
  //#endregion
271
- //#region src/cli/utils/scan-result-cache-payload.ts
272
- const decodeDiagnostic = decodeUnknownSync(Diagnostic);
273
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
274
- const isNullableString = (value) => value === null || typeof value === "string";
275
- const isBoolean = (value) => typeof value === "boolean";
276
- const isNumber = (value) => typeof value === "number";
277
- const isString = (value) => typeof value === "string";
278
- const isDiagnosticArray = (value) => {
279
- if (!Array.isArray(value)) return false;
280
- try {
281
- for (const entry of value) decodeDiagnostic(entry);
282
- return true;
283
- } catch {
284
- return false;
285
- }
274
+ //#region src/cli/utils/should-block-ci.ts
275
+ const shouldBlockCi = (diagnostics, blockingLevel) => {
276
+ if (blockingLevel === "none") return false;
277
+ if (blockingLevel === "warning") return diagnostics.length > 0;
278
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
286
279
  };
287
- const isScoreResult = (value) => value === null || isRecord(value) && typeof value.score === "number" && typeof value.label === "string";
288
- const isProjectInfo = (value) => isRecord(value) && typeof value.rootDirectory === "string" && typeof value.projectName === "string" && typeof value.framework === "string" && typeof value.sourceFileCount === "number";
289
- const isBaselineDelta = (value) => isRecord(value) && typeof value.baseRef === "string" && typeof value.fixedCount === "number" && typeof value.baseTotalCount === "number" && (value.crossFileMatchCount === void 0 || typeof value.crossFileMatchCount === "number");
290
- const isSuppressedRuleCountArray = (value) => Array.isArray(value) && value.every((entry) => isRecord(entry) && typeof entry.rule === "string" && typeof entry.source === "string" && typeof entry.count === "number");
291
- const REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS = [
292
- {
293
- key: "diagnostics",
294
- isValid: isDiagnosticArray
295
- },
296
- {
297
- key: "score",
298
- isValid: isScoreResult
299
- },
300
- {
301
- key: "project",
302
- isValid: isProjectInfo
303
- },
304
- {
305
- key: "userConfig",
306
- isValid: (value) => value === null || isRecord(value)
307
- },
308
- {
309
- key: "didLintFail",
310
- isValid: isBoolean
311
- },
312
- {
313
- key: "lintFailureReason",
314
- isValid: isNullableString
315
- },
316
- {
317
- key: "lintPartialFailures",
318
- isValid: isStringArray
319
- },
320
- {
321
- key: "didDeadCodeFail",
322
- isValid: isBoolean
323
- },
324
- {
325
- key: "deadCodeFailureReason",
326
- isValid: isNullableString
327
- },
328
- {
329
- key: "deadCodeOverlapped",
330
- isValid: isBoolean
331
- },
332
- {
333
- key: "directory",
334
- isValid: isString
335
- },
336
- {
337
- key: "scannedFileCount",
338
- isValid: isNumber
339
- },
340
- {
341
- key: "scannedFilePaths",
342
- isValid: isStringArray
343
- },
344
- {
345
- key: "scanElapsedMilliseconds",
346
- isValid: isNumber
347
- },
348
- {
349
- key: "lintFailureReasonKind",
350
- isValid: isNullableString
351
- },
352
- {
353
- key: "supplyChainOverlapTimedOut",
354
- isValid: isBoolean
355
- }
356
- ];
357
- const OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS = [
358
- {
359
- key: "analyzedFiles",
360
- isValid: isStringArray
361
- },
362
- {
363
- key: "baselineDelta",
364
- isValid: isBaselineDelta
365
- },
366
- {
367
- key: "scanConcurrency",
368
- isValid: isNumber
369
- },
370
- {
371
- key: "securityScanFailed",
372
- isValid: isBoolean
373
- },
374
- {
375
- key: "securityScanFailureReason",
376
- isValid: isNullableString
377
- },
378
- {
379
- key: "suppressedRuleCounts",
380
- isValid: isSuppressedRuleCountArray
381
- },
382
- {
383
- key: "manifestContentHash",
384
- isValid: isNullableString
385
- }
386
- ];
387
- const hasValidRequiredCachedScanPayloadFields = (value) => REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => field.isValid(value[field.key]));
388
- const hasValidOptionalCachedScanPayloadFields = (value) => OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => value[field.key] === void 0 || field.isValid(value[field.key]));
389
- const isCachedScanPayload = (value) => {
390
- if (!isRecord(value)) return false;
391
- return hasValidRequiredCachedScanPayloadFields(value) && hasValidOptionalCachedScanPayloadFields(value);
280
+ //#endregion
281
+ //#region src/cli/utils/to-category-key.ts
282
+ /**
283
+ * Lowercase, key-safe form of a rule category for the `diag.category.*`
284
+ * telemetry attribute namespace (categories carry spaces / capitals, e.g.
285
+ * "Performance" → `performance`, "Dead Code" → `dead_code`).
286
+ */
287
+ const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
288
+ //#endregion
289
+ //#region src/cli/utils/with-namespace.ts
290
+ /**
291
+ * Prefixes every key in a flat attribute group with `namespace.`, so a logical
292
+ * group (scan config, diagnostics rollup, score, …) carries one dotted
293
+ * namespace applied in a single place rather than hand-spelled into each key —
294
+ * which is how the wide event drifted into a flat, half-namespaced soup. The
295
+ * dotted prefix is what makes the attributes tree up in Sentry's attribute
296
+ * browser and stay group-/filter-/aggregate-able in the Spans dataset.
297
+ *
298
+ * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
299
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
300
+ * so `toSpanAttributes` can drop absent signals rather than coerce them).
301
+ */
302
+ const withNamespace = (namespace, attributes) => {
303
+ const namespaced = {};
304
+ for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
305
+ return namespaced;
392
306
  };
393
- const decodeCachedScanPayload = (value) => isCachedScanPayload(value) ? value : null;
394
- const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
395
307
  //#endregion
396
- //#region src/cli/utils/scan-result-cache.ts
397
- const createScanResultCacheInvocationState = () => ({ repositoryIdentityByRoot: /* @__PURE__ */ new Map() });
398
- const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
399
- const TOOLCHAIN_PACKAGE_SPECIFIERS = [
400
- "oxlint/package.json",
401
- "oxlint-plugin-react-doctor/package.json",
402
- "eslint-plugin-react-hooks/package.json"
403
- ];
404
- const bundledRequire = createRequire(import.meta.url);
405
- const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
406
- try {
407
- return `oxlint-plugin-react-doctor#fingerprint=${hashFileContents(bundledRequire.resolve("oxlint-plugin-react-doctor")) ?? "unreadable"}`;
408
- } catch {
409
- return "oxlint-plugin-react-doctor#fingerprint=unresolved";
410
- }
411
- })();
412
- const normalizeForStableJson = (value) => {
413
- if (value === null) return null;
414
- if (value === void 0) return void 0;
415
- const valueType = typeof value;
416
- if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
417
- if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
418
- if (Array.isArray(value)) return value.map((item) => {
419
- const normalized = normalizeForStableJson(item);
420
- return normalized === void 0 ? null : normalized;
308
+ //#region src/cli/utils/build-run-event.ts
309
+ const readEnvBoolean = (name) => {
310
+ const value = process.env[name];
311
+ if (value === void 0) return null;
312
+ return value.toLowerCase() === "true" || value === "1";
313
+ };
314
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
315
+ /**
316
+ * One queryable cache temperature per scan, derived from the whole stack:
317
+ *
318
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
319
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
320
+ * branch; no scan work ran).
321
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
322
+ * replays.
323
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
324
+ * off-switch is on. Granular per-cache opt-outs
325
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
326
+ * since the other subsystems stay live.
327
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
328
+ *
329
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
330
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
331
+ * replay ratio), skipping subsystems that never consulted a
332
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
333
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
334
+ * p50/p90-able summary. Emitted only on the success path.
335
+ */
336
+ const buildCacheAttributes = (input) => {
337
+ if (input.result === void 0) return {};
338
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
339
+ wholeRepoHit: true,
340
+ temperature: "turbo",
341
+ warmth: 1
421
342
  });
422
- const prototype = Object.getPrototypeOf(value);
423
- if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
424
- if (!isRecord(value)) throw new Error("Unsupported cache key object");
425
- const normalizedRecord = {};
426
- for (const key of Object.keys(value).sort()) {
427
- const normalized = normalizeForStableJson(value[key]);
428
- if (normalized !== void 0) normalizedRecord[key] = normalized;
343
+ const result = input.result;
344
+ const subsystemReuseRatios = [ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount), ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)];
345
+ let knownSubsystemCount = 0;
346
+ let reuseRatioSum = 0;
347
+ for (const reuseRatio of subsystemReuseRatios) {
348
+ if (reuseRatio === null) continue;
349
+ knownSubsystemCount += 1;
350
+ reuseRatioSum += reuseRatio;
429
351
  }
430
- return normalizedRecord;
352
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
353
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
354
+ return withNamespace("cache", {
355
+ wholeRepoHit: input.wholeRepoCacheHit,
356
+ temperature,
357
+ warmth
358
+ });
431
359
  };
432
- const stringifyStableJson = (value) => {
433
- try {
434
- return JSON.stringify(normalizeForStableJson(value));
435
- } catch {
436
- return null;
437
- }
360
+ const resolveVersionPin = (versionInput) => {
361
+ if (versionInput === void 0 || versionInput.trim() === "") return null;
362
+ if (versionInput === "latest") return "latest";
363
+ if (/^(\.\.?\/|\/)/.test(versionInput)) return "local";
364
+ return "pinned";
438
365
  };
439
- const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
440
- const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
441
- const parseWorktreeStatusRecords = (statusOutput) => {
442
- const entries = [];
443
- const tokens = statusOutput.split("\0");
444
- for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
445
- let record = tokens[tokenIndex];
446
- if (record.length === 0) continue;
447
- if (record.length < 3 || record[2] !== " ") {
448
- if (tokenIndex !== 0) return null;
449
- record = ` ${record}`;
450
- }
451
- if (record.length < 4 || record[2] !== " ") return null;
452
- const statusCode = record.slice(0, 2);
453
- const recordPath = record.slice(3);
454
- if (statusCode.includes("R") || statusCode.includes("C")) {
455
- tokenIndex += 1;
456
- const originPath = tokens[tokenIndex];
457
- if (originPath === void 0 || originPath.length === 0) return null;
458
- entries.push({
459
- statusCode,
460
- path: recordPath,
461
- originPath
462
- });
463
- continue;
464
- }
465
- entries.push({
466
- statusCode,
467
- path: recordPath
366
+ const resolveTelemetryBlocking = (userConfig) => {
367
+ const fromAction = process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.blocking];
368
+ if (fromAction !== void 0 && isValidBlockingLevel(fromAction)) return fromAction;
369
+ return userConfig?.blocking ?? userConfig?.failOn ?? "error";
370
+ };
371
+ const buildOutcomeAttributes = (input) => {
372
+ if (input.result === void 0) {
373
+ const error = input.error;
374
+ const known = isReactDoctorError(error);
375
+ return withNamespace("outcome", {
376
+ status: "error",
377
+ exitCode: 1,
378
+ complete: false,
379
+ knownError: known,
380
+ errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
468
381
  });
469
382
  }
470
- return entries;
471
- };
472
- const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
473
- let stats;
474
- try {
475
- stats = fs.lstatSync(absolutePath);
476
- } catch {
477
- return statusCode.includes("D") ? "deleted" : null;
383
+ const result = input.result;
384
+ const summary = summarizeDiagnostics(result.diagnostics);
385
+ const blockingLevel = resolveTelemetryBlocking(input.userConfig);
386
+ const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
387
+ const gateDiagnosticSet = new Set(gateDiagnostics);
388
+ const complete = isInspectResultComplete(result);
389
+ const didLintHardFail = hasLintHardFailure(result);
390
+ const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
391
+ const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
392
+ const isClean = result.diagnostics.length === 0 && complete;
393
+ const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
394
+ const firings = summarizeRuleFirings(result.diagnostics);
395
+ const countByRule = /* @__PURE__ */ new Map();
396
+ const countByCategory = /* @__PURE__ */ new Map();
397
+ for (const firing of firings) {
398
+ countByRule.set(firing.rule, (countByRule.get(firing.rule) ?? 0) + firing.count);
399
+ countByCategory.set(firing.category, (countByCategory.get(firing.category) ?? 0) + firing.count);
478
400
  }
479
- if (stats.isSymbolicLink()) try {
480
- return `link:${fs.readlinkSync(absolutePath)}`;
481
- } catch {
482
- return null;
401
+ let topRule = null;
402
+ let topRuleCount = 0;
403
+ for (const [rule, count] of countByRule) if (count > topRuleCount) {
404
+ topRule = rule;
405
+ topRuleCount = count;
483
406
  }
484
- if (!stats.isFile()) return null;
485
- if (stats.size > 10485760) return null;
486
- return hashFileContents(absolutePath);
487
- };
488
- const buildWorktreeFingerprint = (projectDirectory, repositoryRoot) => {
489
- const statusOutput = runGit(projectDirectory, [
490
- "status",
491
- "--porcelain=v1",
492
- "-z",
493
- "--untracked-files=all"
494
- ]);
495
- if (statusOutput === null) return null;
496
- if (statusOutput.length === 0) return [];
497
- const statusEntries = parseWorktreeStatusRecords(statusOutput);
498
- if (statusEntries === null) return null;
499
- if (statusEntries.length > 300) return null;
500
- const dirtyEntries = [];
501
- for (const statusEntry of statusEntries) {
502
- const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
503
- if (contentFingerprint === null) return null;
504
- dirtyEntries.push({
505
- ...statusEntry,
506
- contentFingerprint
507
- });
407
+ const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
408
+ let diagnosticsInTestFiles = 0;
409
+ let diagnosticsInStoryFiles = 0;
410
+ let nonProductionGateExcluded = 0;
411
+ const diagnosticSiteCounts = /* @__PURE__ */ new Map();
412
+ const findingsPerFixGroup = /* @__PURE__ */ new Map();
413
+ for (const diagnostic of result.diagnostics) {
414
+ if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
415
+ if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
416
+ if (diagnostic.fileContext !== void 0 && !gateDiagnosticSet.has(diagnostic)) nonProductionGateExcluded += 1;
417
+ const diagnosticSite = JSON.stringify([
418
+ diagnostic.filePath,
419
+ diagnostic.line,
420
+ diagnostic.column,
421
+ diagnostic.plugin,
422
+ diagnostic.rule
423
+ ]);
424
+ diagnosticSiteCounts.set(diagnosticSite, (diagnosticSiteCounts.get(diagnosticSite) ?? 0) + 1);
425
+ if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
508
426
  }
509
- return dirtyEntries.sort((firstEntry, secondEntry) => {
510
- const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
511
- const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
512
- return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
513
- });
514
- };
515
- const resolveRepositoryCacheIdentity = (projectDirectory, invocationState) => {
516
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
517
- if (repositoryRoot === null) return null;
518
- const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
519
- if (cachedIdentity !== void 0) return cachedIdentity;
520
- const worktreeFingerprint = buildWorktreeFingerprint(projectDirectory, repositoryRoot);
521
- const headSha = readHeadSha(repositoryRoot);
522
- const identity = worktreeFingerprint === null || headSha === null ? null : {
523
- headSha,
524
- worktreeFingerprint
525
- };
526
- invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
527
- return identity;
528
- };
529
- const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
530
- const resolveDotenvFingerprint = (projectDirectory) => {
531
- try {
532
- return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
533
- } catch {
534
- return [];
427
+ let fixGroupedFindings = 0;
428
+ for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
429
+ let sameSiteOccurrences = 0;
430
+ for (const count of diagnosticSiteCounts.values()) sameSiteOccurrences += Math.max(0, count - 1);
431
+ const categoryRollup = {};
432
+ for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
433
+ const suppressionRollup = {};
434
+ if (input.suppressedRuleCounts) {
435
+ const countBySource = {
436
+ config: 0,
437
+ override: 0,
438
+ inline: 0,
439
+ "foreign-inline": 0
440
+ };
441
+ for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
442
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
443
+ suppressionRollup.suppressedConfig = countBySource.config;
444
+ suppressionRollup.suppressedOverride = countBySource.override;
445
+ suppressionRollup.suppressedInline = countBySource.inline;
446
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
535
447
  }
448
+ const attributes = {
449
+ ...withNamespace("outcome", {
450
+ status: outcome,
451
+ exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
452
+ wouldBlock,
453
+ blocking: blockingLevel,
454
+ clean: isClean,
455
+ complete,
456
+ skippedChecks: result.skippedChecks.length
457
+ }),
458
+ ...withNamespace("diag", {
459
+ total: summary.totalDiagnosticCount,
460
+ errors: summary.errorCount,
461
+ warnings: summary.warningCount,
462
+ affectedFiles: summary.affectedFileCount,
463
+ inTestFiles: diagnosticsInTestFiles,
464
+ inStoryFiles: diagnosticsInStoryFiles,
465
+ nonProductionGateExcluded,
466
+ distinctRules: countByRule.size,
467
+ topRule,
468
+ sameSiteOccurrences,
469
+ fixGroups: findingsPerFixGroup.size,
470
+ fixGroupedFindings,
471
+ ...categoryRollup,
472
+ ...suppressionRollup
473
+ }),
474
+ ...withNamespace("score", {
475
+ value: result.score ? result.score.score : null,
476
+ label: result.score ? result.score.label : null,
477
+ available: result.score !== null,
478
+ unavailableReason: result.score === null ? resolveScoreUnavailableReason({
479
+ isScoreDisabled: input.noScore,
480
+ isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
481
+ }) : null
482
+ }),
483
+ ...withNamespace("lint", {
484
+ failed: input.didLintFail,
485
+ failureReasonKind: input.lintFailureReasonKind,
486
+ partialFailureCount: input.lintPartialFailureCount,
487
+ droppedFileCount: input.lintDroppedFileCount,
488
+ deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
489
+ cacheHitFiles: result.lintCacheHitFileCount,
490
+ cacheTotalFiles: result.lintCacheTotalFileCount,
491
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
492
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
493
+ sidecarTotalFiles: result.lintSidecarTotalFileCount,
494
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
495
+ }),
496
+ ...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
497
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
498
+ ...withNamespace("securityScan", { failed: input.securityScanFailed }),
499
+ ...withNamespace("timing", {
500
+ elapsedMs: result.elapsedMilliseconds,
501
+ scanMs: result.scanElapsedMilliseconds
502
+ }),
503
+ ...withNamespace("migration", {
504
+ largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
505
+ largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
506
+ largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
507
+ })
508
+ };
509
+ if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
510
+ new: summary.totalDiagnosticCount,
511
+ fixed: result.baselineDelta.fixedCount,
512
+ baseTotal: result.baselineDelta.baseTotalCount,
513
+ crossFileMatches: result.baselineDelta.crossFileMatchCount,
514
+ degraded: false
515
+ }));
516
+ else if (input.gateExempt) attributes["baseline.degraded"] = true;
517
+ return attributes;
536
518
  };
537
- const isGitIdentityTrustworthy = (projectDirectory) => {
538
- const output = runGit(projectDirectory, ["ls-files", "-v"]);
539
- if (output === null) return false;
540
- const entryLines = output.split("\n").filter((line) => line.length > 0);
541
- return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
519
+ const buildActionAttributes = () => {
520
+ const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
521
+ return withNamespace("action", {
522
+ actorAssociation: githubActorAssociation,
523
+ runnerOs: detectRunnerOs(),
524
+ comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
525
+ reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
526
+ versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
527
+ });
542
528
  };
543
- const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
544
- const readPersistedCache = (cacheFilePath) => {
545
- try {
546
- const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
547
- if (!isRecord(parsed) || parsed.version !== 7) return {
548
- version: 7,
549
- entries: []
550
- };
551
- if (!Array.isArray(parsed.entries)) return {
552
- version: 7,
553
- entries: []
554
- };
555
- const entries = [];
556
- for (const entry of parsed.entries) {
557
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
558
- const payload = decodeCachedScanPayload(entry.payload);
559
- if (payload === null) continue;
560
- entries.push({
561
- key: entry.key,
562
- createdAtMs: entry.createdAtMs,
563
- payload
564
- });
565
- }
566
- return {
567
- version: 7,
568
- entries
569
- };
570
- } catch {
571
- return {
572
- version: 7,
573
- entries: []
574
- };
575
- }
529
+ const buildScanAttributes = (input) => {
530
+ const ruleOverrides = input.userConfig?.rules ?? {};
531
+ const ruleKeys = Object.keys(ruleOverrides);
532
+ return withNamespace("scan", {
533
+ mode: input.mode,
534
+ scope: input.scope,
535
+ parallel: input.parallel,
536
+ workerCount: input.workerCount,
537
+ maxDurationMs: input.maxDurationMs,
538
+ lint: input.lint,
539
+ maintainability: input.deadCode,
540
+ supplyChain: input.supplyChain,
541
+ scoreOnly: input.scoreOnly,
542
+ noScore: input.noScore,
543
+ respectInlineDisables: input.respectInlineDisables,
544
+ showWarnings: input.showWarnings,
545
+ usedOutputDir: input.usedOutputDir,
546
+ ignoredTagCount: input.ignoredTagCount,
547
+ hasCustomConfig: input.hasCustomConfig,
548
+ rulesConfigured: ruleKeys.length,
549
+ rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
550
+ projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
551
+ fileCount: input.result?.scannedFileCount,
552
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
553
+ htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
554
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
555
+ });
576
556
  };
577
557
  /**
578
- * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
579
- * cache subsystem: this whole-repo scan cache plus core's per-file lint,
580
- * sidecar caches (their `Context.Reference` defaults read the
581
- * same variable). Exported for the wide event's `cache.temperature`
582
- * derivation, which reports `"disabled"` instead of `"cold"` when the switch
583
- * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
584
- * not consulted — they leave the other subsystems live.
558
+ * Projects a scan into the namespaced attribute set for its root span — the
559
+ * canonical per-scan "wide event". Every attribute carries a dotted namespace
560
+ * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
561
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `maintainability.*`, `cache.*`,
562
+ * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
563
+ * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
564
+ * in the Spans dataset. Pure and exported so the projection (outcome
565
+ * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
566
+ * without a live Sentry client. `null`/`undefined` values are dropped so absent
567
+ * signals never become misleading `"null"`/`"undefined"` attributes. The run +
568
+ * project base context (version, command, ci/provider, framework, …) is already
569
+ * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
570
+ * adds only what those don't carry.
585
571
  */
586
- const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
587
- const resolveProjectIdentity = (projectDirectory) => {
588
- try {
589
- return fs.realpathSync.native(projectDirectory);
590
- } catch {
591
- return path.resolve(projectDirectory);
592
- }
593
- };
594
- const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
595
- const fileFingerprint = (filePath) => {
572
+ const buildRunEventAttributes = (input) => toSpanAttributes({
573
+ ...buildScanAttributes(input),
574
+ ...buildActionAttributes(),
575
+ ...buildOutcomeAttributes(input),
576
+ ...buildCacheAttributes(input)
577
+ });
578
+ /**
579
+ * Stamps the wide-event attributes onto the run's root span. A guarded no-op
580
+ * when tracing is off (no `rootSpan`) and swallow-on-throw, so telemetry can
581
+ * never break the run.
582
+ */
583
+ const recordRunEvent = (rootSpan, input) => {
584
+ if (!rootSpan) return;
596
585
  try {
597
- const stat = fs.statSync(filePath);
598
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
599
- } catch {
600
- return null;
601
- }
602
- };
603
- const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
604
- const fingerprints = [];
605
- if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
606
- for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
607
- const packageJson = bundledRequire(specifier);
608
- const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
609
- fingerprints.push(`${specifier}=${version}`);
610
- } catch {
611
- fingerprints.push(`${specifier}=missing`);
612
- }
613
- fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
614
- return fingerprints;
615
- };
616
- const buildScanResultCacheKey = (input) => {
617
- if (isCacheGloballyDisabled()) return null;
618
- if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
619
- const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
620
- if (repositoryIdentity === null) return null;
621
- if (stringifyStableJson(input.userConfig) === null) return null;
622
- const cacheKeyJson = stringifyStableJson({
623
- schemaVersion: 7,
624
- projectIdentity: resolveProjectIdentity(input.projectDirectory),
625
- headSha: repositoryIdentity.headSha,
626
- worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
627
- dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
628
- reactDoctorVersion: input.version,
629
- nodeVersion: process.version,
630
- toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
631
- configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
632
- hasConfigOverride: input.hasConfigOverride,
633
- configSourceDirectory: input.configSourceDirectory,
634
- userConfig: input.userConfig,
635
- engineOptions: {
636
- lint: input.options.lint,
637
- deadCode: input.options.deadCode,
638
- supplyChain: input.options.supplyChain,
639
- includePaths: [...input.options.includePaths].sort(),
640
- excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
641
- retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
642
- customRulesOnly: input.options.customRulesOnly,
643
- respectInlineDisables: input.options.respectInlineDisables,
644
- warnings: input.options.warnings,
645
- adoptExistingLintConfig: input.options.adoptExistingLintConfig,
646
- ignoredTags: [...input.options.ignoredTags].sort(),
647
- includedTags: [...input.options.includedTags].sort(),
648
- includeTagDefaults: input.options.includeTagDefaults,
649
- concurrency: input.options.concurrency,
650
- lintBatchOrdering: resolveLintBatchOrdering(),
651
- baselineRef: input.options.baseline?.ref,
652
- changedLineRanges: input.options.changedLineRanges ?? void 0,
653
- noScore: input.options.noScore,
654
- isCi: input.options.isCi,
655
- suppressRendering: input.options.suppressRendering,
656
- supplyChainManifestChanged: input.options.supplyChainManifestChanged
657
- }
658
- });
659
- return cacheKeyJson === null ? null : hashString(cacheKeyJson);
660
- };
661
- const createScanResultCache = (projectDirectory) => {
662
- const cacheFilePath = resolveCacheFilePath(projectDirectory);
663
- const persistedCache = readPersistedCache(cacheFilePath);
664
- const entries = /* @__PURE__ */ new Map();
665
- for (const entry of persistedCache.entries) entries.set(entry.key, entry);
666
- const persist = () => {
667
- const mergedEntries = /* @__PURE__ */ new Map();
668
- for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
669
- for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
670
- atomicWriteJson(cacheFilePath, {
671
- version: 7,
672
- entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
673
- });
674
- };
675
- return {
676
- lookup: (key) => {
677
- const payload = entries.get(key)?.payload;
678
- if (payload === void 0) return null;
679
- return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
680
- },
681
- store: (key, payload) => {
682
- entries.set(key, {
683
- key,
684
- createdAtMs: Date.now(),
685
- payload: {
686
- ...payload,
687
- manifestContentHash: resolveManifestContentHash(projectDirectory)
688
- }
689
- });
690
- persist();
691
- }
692
- };
586
+ for (const [key, value] of Object.entries(buildRunEventAttributes(input))) rootSpan.attribute(key, value);
587
+ } catch {}
693
588
  };
694
589
  //#endregion
695
- //#region src/cli/utils/should-block-ci.ts
696
- const shouldBlockCi = (diagnostics, blockingLevel) => {
697
- if (blockingLevel === "none") return false;
698
- if (blockingLevel === "warning") return diagnostics.length > 0;
699
- return diagnostics.some((diagnostic) => diagnostic.severity === "error");
590
+ //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
591
+ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
592
+ const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
593
+ return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
700
594
  };
701
595
  //#endregion
702
- //#region src/cli/utils/to-category-key.ts
703
- /**
704
- * Lowercase, key-safe form of a rule category for the `diag.category.*`
705
- * telemetry attribute namespace (categories carry spaces / capitals, e.g.
706
- * "Performance" → `performance`, "Dead Code" → `dead_code`).
707
- */
708
- const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
709
- //#endregion
710
- //#region src/cli/utils/with-namespace.ts
711
- /**
712
- * Prefixes every key in a flat attribute group with `namespace.`, so a logical
713
- * group (scan config, diagnostics rollup, score, …) carries one dotted
714
- * namespace applied in a single place rather than hand-spelled into each key —
715
- * which is how the wide event drifted into a flat, half-namespaced soup. The
716
- * dotted prefix is what makes the attributes tree up in Sentry's attribute
717
- * browser and stay group-/filter-/aggregate-able in the Spans dataset.
718
- *
719
- * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
720
- * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
721
- * so `toSpanAttributes` can drop absent signals rather than coerce them).
722
- */
723
- const withNamespace = (namespace, attributes) => {
724
- const namespaced = {};
725
- for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
726
- return namespaced;
596
+ //#region src/cli/utils/filter-diagnostics-by-changed-lines.ts
597
+ const filterDiagnosticsByChangedLines = (input) => {
598
+ const rangesByFile = /* @__PURE__ */ new Map();
599
+ for (const entry of input.changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
600
+ return input.diagnostics.filter((diagnostic) => {
601
+ const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(input.directory, diagnostic.filePath) : diagnostic.filePath);
602
+ const ranges = rangesByFile.get(relativePath);
603
+ return ranges !== void 0 && diagnosticIntersectsLineRanges(diagnostic, ranges);
604
+ });
727
605
  };
728
606
  //#endregion
729
- //#region src/cli/utils/build-run-event.ts
730
- const readEnvBoolean = (name) => {
731
- const value = process.env[name];
732
- if (value === void 0) return null;
733
- return value.toLowerCase() === "true" || value === "1";
734
- };
735
- const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
607
+ //#region src/cli/utils/resolve-invocation-oxlint-concurrency.ts
736
608
  /**
737
- * One queryable cache temperature per scan, derived from the whole stack:
738
- *
739
- * - `"turbo"` — whole-repo scan-result replay (the explicit
740
- * `wholeRepoCacheHit` flag from the CLI's cachedPayload
741
- * branch; no scan work ran).
742
- * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
743
- * replays.
744
- * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
745
- * off-switch is on. Granular per-cache opt-outs
746
- * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
747
- * since the other subsystems stay live.
748
- * - `"cold"` — caches on, zero reuse (first scan / everything changed).
749
- *
750
- * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
751
- * the subsystem reuse fractions known this run (lint hit ratio, sidecar
752
- * replay ratio), skipping subsystems that never consulted a
753
- * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
754
- * unweighted — the per-subsystem dims stay the precise signal; warmth is the
755
- * p50/p90-able summary. Emitted only on the success path.
609
+ * The invocation-wide oxlint worker count: an explicit `--concurrency` pin
610
+ * when given, else the env-seeded / auto-budgeted `OxlintConcurrency`, clamped
611
+ * to the spawn ceiling. Every project of a workspace scan shares this one
612
+ * pool, so it also sizes how many projects may be in flight at once.
756
613
  */
757
- const buildCacheAttributes = (input) => {
758
- if (input.result === void 0) return {};
759
- if (input.wholeRepoCacheHit) return withNamespace("cache", {
760
- wholeRepoHit: true,
761
- temperature: "turbo",
762
- warmth: 1
763
- });
764
- const result = input.result;
765
- const subsystemReuseRatios = [ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount), ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)];
766
- let knownSubsystemCount = 0;
767
- let reuseRatioSum = 0;
768
- for (const reuseRatio of subsystemReuseRatios) {
769
- if (reuseRatio === null) continue;
770
- knownSubsystemCount += 1;
771
- reuseRatioSum += reuseRatio;
772
- }
773
- const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
774
- const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
775
- return withNamespace("cache", {
776
- wholeRepoHit: input.wholeRepoCacheHit,
777
- temperature,
778
- warmth
779
- });
780
- };
781
- const resolveVersionPin = (versionInput) => {
782
- if (versionInput === void 0 || versionInput.trim() === "") return null;
783
- if (versionInput === "latest") return "latest";
784
- if (/^(\.\.?\/|\/)/.test(versionInput)) return "local";
785
- return "pinned";
786
- };
787
- const resolveTelemetryBlocking = (userConfig) => {
788
- const fromAction = process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.blocking];
789
- if (fromAction !== void 0 && isValidBlockingLevel(fromAction)) return fromAction;
790
- return userConfig?.blocking ?? userConfig?.failOn ?? "error";
791
- };
792
- const buildOutcomeAttributes = (input) => {
793
- if (input.result === void 0) {
794
- const error = input.error;
795
- const known = isReactDoctorError(error);
796
- return withNamespace("outcome", {
797
- status: "error",
798
- exitCode: 1,
799
- complete: false,
800
- knownError: known,
801
- errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
802
- });
803
- }
804
- const result = input.result;
805
- const summary = summarizeDiagnostics(result.diagnostics);
806
- const blockingLevel = resolveTelemetryBlocking(input.userConfig);
807
- const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
808
- const gateDiagnosticSet = new Set(gateDiagnostics);
809
- const complete = isInspectResultComplete(result);
810
- const didLintHardFail = hasLintHardFailure(result);
811
- const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
812
- const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
813
- const isClean = result.diagnostics.length === 0 && complete;
814
- const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
815
- const firings = summarizeRuleFirings(result.diagnostics);
816
- const countByRule = /* @__PURE__ */ new Map();
817
- const countByCategory = /* @__PURE__ */ new Map();
818
- for (const firing of firings) {
819
- countByRule.set(firing.rule, (countByRule.get(firing.rule) ?? 0) + firing.count);
820
- countByCategory.set(firing.category, (countByCategory.get(firing.category) ?? 0) + firing.count);
821
- }
822
- let topRule = null;
823
- let topRuleCount = 0;
824
- for (const [rule, count] of countByRule) if (count > topRuleCount) {
825
- topRule = rule;
826
- topRuleCount = count;
827
- }
828
- const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
829
- let diagnosticsInTestFiles = 0;
830
- let diagnosticsInStoryFiles = 0;
831
- let nonProductionGateExcluded = 0;
832
- const diagnosticSiteCounts = /* @__PURE__ */ new Map();
833
- const findingsPerFixGroup = /* @__PURE__ */ new Map();
834
- for (const diagnostic of result.diagnostics) {
835
- if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
836
- if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
837
- if (diagnostic.fileContext !== void 0 && !gateDiagnosticSet.has(diagnostic)) nonProductionGateExcluded += 1;
838
- const diagnosticSite = JSON.stringify([
839
- diagnostic.filePath,
840
- diagnostic.line,
841
- diagnostic.column,
842
- diagnostic.plugin,
843
- diagnostic.rule
844
- ]);
845
- diagnosticSiteCounts.set(diagnosticSite, (diagnosticSiteCounts.get(diagnosticSite) ?? 0) + 1);
846
- if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
847
- }
848
- let fixGroupedFindings = 0;
849
- for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
850
- let sameSiteOccurrences = 0;
851
- for (const count of diagnosticSiteCounts.values()) sameSiteOccurrences += Math.max(0, count - 1);
852
- const categoryRollup = {};
853
- for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
854
- const suppressionRollup = {};
855
- if (input.suppressedRuleCounts) {
856
- const countBySource = {
857
- config: 0,
858
- override: 0,
859
- inline: 0,
860
- "foreign-inline": 0
861
- };
862
- for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
863
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
864
- suppressionRollup.suppressedConfig = countBySource.config;
865
- suppressionRollup.suppressedOverride = countBySource.override;
866
- suppressionRollup.suppressedInline = countBySource.inline;
867
- suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
868
- }
869
- const attributes = {
870
- ...withNamespace("outcome", {
871
- status: outcome,
872
- exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
873
- wouldBlock,
874
- blocking: blockingLevel,
875
- clean: isClean,
876
- complete,
877
- skippedChecks: result.skippedChecks.length
878
- }),
879
- ...withNamespace("diag", {
880
- total: summary.totalDiagnosticCount,
881
- errors: summary.errorCount,
882
- warnings: summary.warningCount,
883
- affectedFiles: summary.affectedFileCount,
884
- inTestFiles: diagnosticsInTestFiles,
885
- inStoryFiles: diagnosticsInStoryFiles,
886
- nonProductionGateExcluded,
887
- distinctRules: countByRule.size,
888
- topRule,
889
- sameSiteOccurrences,
890
- fixGroups: findingsPerFixGroup.size,
891
- fixGroupedFindings,
892
- ...categoryRollup,
893
- ...suppressionRollup
894
- }),
895
- ...withNamespace("score", {
896
- value: result.score ? result.score.score : null,
897
- label: result.score ? result.score.label : null,
898
- available: result.score !== null,
899
- unavailableReason: result.score === null ? resolveScoreUnavailableReason({
900
- isScoreDisabled: input.noScore,
901
- isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
902
- }) : null
903
- }),
904
- ...withNamespace("lint", {
905
- failed: input.didLintFail,
906
- failureReasonKind: input.lintFailureReasonKind,
907
- partialFailureCount: input.lintPartialFailureCount,
908
- droppedFileCount: input.lintDroppedFileCount,
909
- deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
910
- cacheHitFiles: result.lintCacheHitFileCount,
911
- cacheTotalFiles: result.lintCacheTotalFileCount,
912
- cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
913
- sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
914
- sidecarTotalFiles: result.lintSidecarTotalFileCount,
915
- sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
916
- }),
917
- ...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
918
- ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
919
- ...withNamespace("securityScan", { failed: input.securityScanFailed }),
920
- ...withNamespace("timing", {
921
- elapsedMs: result.elapsedMilliseconds,
922
- scanMs: result.scanElapsedMilliseconds
923
- }),
924
- ...withNamespace("migration", {
925
- largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
926
- largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
927
- largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
928
- })
929
- };
930
- if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
931
- new: summary.totalDiagnosticCount,
932
- fixed: result.baselineDelta.fixedCount,
933
- baseTotal: result.baselineDelta.baseTotalCount,
934
- crossFileMatches: result.baselineDelta.crossFileMatchCount,
935
- degraded: false
936
- }));
937
- else if (input.gateExempt) attributes["baseline.degraded"] = true;
938
- return attributes;
939
- };
940
- const buildActionAttributes = () => {
941
- const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
942
- return withNamespace("action", {
943
- actorAssociation: githubActorAssociation,
944
- runnerOs: detectRunnerOs(),
945
- comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
946
- reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
947
- versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
948
- });
949
- };
950
- const buildScanAttributes = (input) => {
951
- const ruleOverrides = input.userConfig?.rules ?? {};
952
- const ruleKeys = Object.keys(ruleOverrides);
953
- return withNamespace("scan", {
954
- mode: input.mode,
955
- scope: input.scope,
956
- parallel: input.parallel,
957
- workerCount: input.workerCount,
958
- maxDurationMs: input.maxDurationMs,
959
- lint: input.lint,
960
- maintainability: input.deadCode,
961
- supplyChain: input.supplyChain,
962
- scoreOnly: input.scoreOnly,
963
- noScore: input.noScore,
964
- respectInlineDisables: input.respectInlineDisables,
965
- showWarnings: input.showWarnings,
966
- usedOutputDir: input.usedOutputDir,
967
- ignoredTagCount: input.ignoredTagCount,
968
- hasCustomConfig: input.hasCustomConfig,
969
- rulesConfigured: ruleKeys.length,
970
- rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
971
- projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
972
- fileCount: input.result?.scannedFileCount,
973
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
974
- htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
975
- multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
976
- });
977
- };
978
- /**
979
- * Projects a scan into the namespaced attribute set for its root span — the
980
- * canonical per-scan "wide event". Every attribute carries a dotted namespace
981
- * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
982
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `maintainability.*`, `cache.*`,
983
- * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
984
- * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
985
- * in the Spans dataset. Pure and exported so the projection (outcome
986
- * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
987
- * without a live Sentry client. `null`/`undefined` values are dropped so absent
988
- * signals never become misleading `"null"`/`"undefined"` attributes. The run +
989
- * project base context (version, command, ci/provider, framework, …) is already
990
- * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
991
- * adds only what those don't carry.
992
- */
993
- const buildRunEventAttributes = (input) => toSpanAttributes({
994
- ...buildScanAttributes(input),
995
- ...buildActionAttributes(),
996
- ...buildOutcomeAttributes(input),
997
- ...buildCacheAttributes(input)
998
- });
999
- /**
1000
- * Stamps the wide-event attributes onto the run's root span. A guarded no-op
1001
- * when tracing is off (no `rootSpan`) and swallow-on-throw, so telemetry can
1002
- * never break the run.
1003
- */
1004
- const recordRunEvent = (rootSpan, input) => {
1005
- if (!rootSpan) return;
1006
- try {
1007
- for (const [key, value] of Object.entries(buildRunEventAttributes(input))) rootSpan.attribute(key, value);
1008
- } catch {}
1009
- };
1010
- //#endregion
1011
- //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
1012
- const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
1013
- const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
1014
- return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
1015
- };
1016
- //#endregion
1017
- //#region src/cli/utils/filter-diagnostics-by-changed-lines.ts
1018
- const filterDiagnosticsByChangedLines = (input) => {
1019
- const rangesByFile = /* @__PURE__ */ new Map();
1020
- for (const entry of input.changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
1021
- return input.diagnostics.filter((diagnostic) => {
1022
- const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(input.directory, diagnostic.filePath) : diagnostic.filePath);
1023
- const ranges = rangesByFile.get(relativePath);
1024
- return ranges !== void 0 && diagnosticIntersectsLineRanges(diagnostic, ranges);
1025
- });
1026
- };
614
+ const resolveInvocationOxlintConcurrency = (requestedConcurrency) => resolveScanConcurrency(requestedConcurrency ?? runSync(OxlintConcurrency));
1027
615
  //#endregion
1028
616
  //#region src/cli/utils/resolve-oxlint-node.ts
1029
617
  const consoleWarn = (message) => warn(highlighter.warn(message));
@@ -1138,7 +726,8 @@ const resolveInspectOptions = (inputOptions, userConfig) => {
1138
726
  supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
1139
727
  excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
1140
728
  retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
1141
- precomputedSourceFileCount: inputOptions.precomputedSourceFileCount
729
+ precomputedSourceFileCount: inputOptions.precomputedSourceFileCount,
730
+ precomputedSourceFiles: inputOptions.precomputedSourceFiles
1142
731
  };
1143
732
  };
1144
733
  //#endregion
@@ -1605,575 +1194,1019 @@ const copyUnchangedBaselineSources = async (input) => {
1605
1194
  })).every(Boolean);
1606
1195
  };
1607
1196
  //#endregion
1608
- //#region src/cli/utils/read-diagnostic-evidence.ts
1609
- const SOURCE_EXTENSIONS = [
1610
- ".ts",
1611
- ".tsx",
1612
- ".js",
1613
- ".jsx",
1614
- ".mts",
1615
- ".cts",
1616
- ".mjs",
1617
- ".cjs"
1197
+ //#region src/cli/utils/read-diagnostic-evidence.ts
1198
+ const SOURCE_EXTENSIONS = [
1199
+ ".ts",
1200
+ ".tsx",
1201
+ ".js",
1202
+ ".jsx",
1203
+ ".mts",
1204
+ ".cts",
1205
+ ".mjs",
1206
+ ".cjs"
1207
+ ];
1208
+ const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
1209
+ const getScriptKind = (filePath) => {
1210
+ const extension = path.extname(filePath).toLowerCase();
1211
+ if (extension === ".tsx") return typescriptBinding.ScriptKind.TSX;
1212
+ if (extension === ".jsx") return typescriptBinding.ScriptKind.JSX;
1213
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return typescriptBinding.ScriptKind.JS;
1214
+ return typescriptBinding.ScriptKind.TS;
1215
+ };
1216
+ const resolveSafeSourcePath = (rootDirectory, filePath) => {
1217
+ try {
1218
+ const resolvedRootDirectory = fs.realpathSync(rootDirectory);
1219
+ const absolutePath = path.resolve(resolvedRootDirectory, filePath);
1220
+ const relativePath = path.relative(resolvedRootDirectory, absolutePath);
1221
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
1222
+ const stats = fs.lstatSync(absolutePath);
1223
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
1224
+ const realPath = fs.realpathSync(absolutePath);
1225
+ const realRelativePath = path.relative(resolvedRootDirectory, realPath);
1226
+ return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
1227
+ } catch {
1228
+ return null;
1229
+ }
1230
+ };
1231
+ const readSourceRecord = (rootDirectory, filePath, state) => {
1232
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
1233
+ if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
1234
+ const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
1235
+ if (absolutePath === null) {
1236
+ state.recordByFilePath.set(normalizedFilePath, null);
1237
+ return null;
1238
+ }
1239
+ try {
1240
+ const sourceText = fs.readFileSync(absolutePath, "utf-8");
1241
+ const record = {
1242
+ absolutePath,
1243
+ filePath: normalizedFilePath,
1244
+ sourceFile: typescriptBinding.createSourceFile(absolutePath, sourceText, typescriptBinding.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
1245
+ sourceText
1246
+ };
1247
+ state.recordByFilePath.set(normalizedFilePath, record);
1248
+ return record;
1249
+ } catch {
1250
+ state.recordByFilePath.set(normalizedFilePath, null);
1251
+ return null;
1252
+ }
1253
+ };
1254
+ const listSourceRecords = (rootDirectory, state) => {
1255
+ if (state.sourceRecords !== null) return state.sourceRecords;
1256
+ state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
1257
+ const record = readSourceRecord(rootDirectory, filePath, state);
1258
+ return record === null ? [] : [record];
1259
+ });
1260
+ return state.sourceRecords;
1261
+ };
1262
+ const unwrapExpression = (expression) => {
1263
+ let currentExpression = expression;
1264
+ while (typescriptBinding.isParenthesizedExpression(currentExpression) || typescriptBinding.isAsExpression(currentExpression) || typescriptBinding.isTypeAssertionExpression(currentExpression) || typescriptBinding.isNonNullExpression(currentExpression) || typescriptBinding.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
1265
+ return currentExpression;
1266
+ };
1267
+ const getCalleeName = (expression) => {
1268
+ const unwrappedExpression = unwrapExpression(expression);
1269
+ if (typescriptBinding.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
1270
+ if (typescriptBinding.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
1271
+ return null;
1272
+ };
1273
+ const isComponentWrapperCall = (node) => {
1274
+ const wrapperName = getCalleeName(node.expression);
1275
+ return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
1276
+ };
1277
+ const getFunctionName = (node) => {
1278
+ if (typescriptBinding.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
1279
+ if (typescriptBinding.isArrowFunction(node) || typescriptBinding.isFunctionExpression(node)) {
1280
+ let componentExpression = node;
1281
+ while (true) {
1282
+ const parent = componentExpression.parent;
1283
+ if (typescriptBinding.isParenthesizedExpression(parent) || typescriptBinding.isAsExpression(parent) || typescriptBinding.isTypeAssertionExpression(parent) || typescriptBinding.isNonNullExpression(parent) || typescriptBinding.isSatisfiesExpression(parent)) {
1284
+ componentExpression = parent;
1285
+ continue;
1286
+ }
1287
+ if (typescriptBinding.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
1288
+ componentExpression = parent;
1289
+ continue;
1290
+ }
1291
+ if (typescriptBinding.isVariableDeclaration(parent) && typescriptBinding.isIdentifier(parent.name)) return parent.name.text;
1292
+ return node.name?.text ?? null;
1293
+ }
1294
+ }
1295
+ return null;
1296
+ };
1297
+ const isComponentFunction = (node) => typescriptBinding.isFunctionDeclaration(node) || typescriptBinding.isFunctionExpression(node) || typescriptBinding.isArrowFunction(node);
1298
+ const isDefaultExportedComponent = (record, node, componentName) => {
1299
+ if (typescriptBinding.canHaveModifiers(node) && typescriptBinding.getModifiers(node)?.some((modifier) => modifier.kind === typescriptBinding.SyntaxKind.DefaultKeyword)) return true;
1300
+ return record.sourceFile.statements.some((statement) => typescriptBinding.isExportAssignment(statement) && typescriptBinding.isIdentifier(statement.expression) && statement.expression.text === componentName);
1301
+ };
1302
+ const findEnclosingComponent = (record, diagnostic) => {
1303
+ if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
1304
+ const lineIndex = diagnostic.line - 1;
1305
+ const lineStarts = record.sourceFile.getLineStarts();
1306
+ const lineStart = lineStarts[lineIndex];
1307
+ if (lineStart === void 0) return null;
1308
+ const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
1309
+ const lineEnd = Math.max(lineStart, nextLineStart - 1);
1310
+ const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
1311
+ const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
1312
+ let component = null;
1313
+ let componentWidth = Number.POSITIVE_INFINITY;
1314
+ const visit = (node) => {
1315
+ if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
1316
+ if (isComponentFunction(node)) {
1317
+ const name = getFunctionName(node);
1318
+ const width = node.end - node.getStart(record.sourceFile);
1319
+ if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
1320
+ component = {
1321
+ isDefaultExport: isDefaultExportedComponent(record, node, name),
1322
+ name,
1323
+ node,
1324
+ record
1325
+ };
1326
+ componentWidth = width;
1327
+ }
1328
+ }
1329
+ typescriptBinding.forEachChild(node, visit);
1330
+ };
1331
+ visit(record.sourceFile);
1332
+ return component;
1333
+ };
1334
+ const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
1335
+ if (!moduleSpecifier.startsWith(".")) return false;
1336
+ const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
1337
+ const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
1338
+ const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
1339
+ return [
1340
+ unresolvedPath,
1341
+ ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
1342
+ ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
1343
+ ].includes(componentRecord.absolutePath);
1344
+ };
1345
+ const getComponentTagNames = (record, component) => {
1346
+ if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
1347
+ const tagNames = /* @__PURE__ */ new Set();
1348
+ for (const statement of record.sourceFile.statements) {
1349
+ if (!typescriptBinding.isImportDeclaration(statement) || !typescriptBinding.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
1350
+ const importClause = statement.importClause;
1351
+ if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
1352
+ const namedBindings = importClause?.namedBindings;
1353
+ if (namedBindings !== void 0 && typescriptBinding.isNamedImports(namedBindings)) {
1354
+ for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
1355
+ }
1356
+ }
1357
+ return tagNames;
1358
+ };
1359
+ const getTransparentCall = (functionNode) => {
1360
+ if (functionNode.body === void 0) return null;
1361
+ if (!typescriptBinding.isBlock(functionNode.body)) {
1362
+ const expression = unwrapExpression(functionNode.body);
1363
+ return typescriptBinding.isCallExpression(expression) ? expression : null;
1364
+ }
1365
+ if (functionNode.body.statements.length !== 1) return null;
1366
+ const statement = functionNode.body.statements[0];
1367
+ if (statement === void 0) return null;
1368
+ const expression = typescriptBinding.isExpressionStatement(statement) || typescriptBinding.isReturnStatement(statement) ? statement.expression : void 0;
1369
+ if (expression === void 0) return null;
1370
+ const unwrappedExpression = unwrapExpression(expression);
1371
+ return typescriptBinding.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
1372
+ };
1373
+ const resolveTransparentTarget = (functionNode) => {
1374
+ const call = getTransparentCall(functionNode);
1375
+ if (call === null) return null;
1376
+ const target = unwrapExpression(call.expression);
1377
+ if (!typescriptBinding.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
1378
+ for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
1379
+ const argument = call.arguments[parameterIndex];
1380
+ const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
1381
+ if (!typescriptBinding.isIdentifier(parameter.name) || unwrappedArgument === null || !typescriptBinding.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
1382
+ }
1383
+ return target.text;
1384
+ };
1385
+ const getForwardingFunction = (declaration) => {
1386
+ if (typescriptBinding.isFunctionDeclaration(declaration)) return declaration;
1387
+ if (!typescriptBinding.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
1388
+ const initializer = unwrapExpression(declaration.initializer);
1389
+ if (typescriptBinding.isArrowFunction(initializer) || typescriptBinding.isFunctionExpression(initializer)) return initializer;
1390
+ if (typescriptBinding.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
1391
+ const callback = initializer.arguments[0];
1392
+ if (callback !== void 0 && (typescriptBinding.isArrowFunction(callback) || typescriptBinding.isFunctionExpression(callback))) return callback;
1393
+ }
1394
+ return null;
1395
+ };
1396
+ const getDeclaredBindingName = (node) => {
1397
+ if ((typescriptBinding.isVariableDeclaration(node) || typescriptBinding.isParameter(node) || typescriptBinding.isBindingElement(node) || typescriptBinding.isFunctionDeclaration(node)) && node.name !== void 0 && typescriptBinding.isIdentifier(node.name)) return node.name.text;
1398
+ if (typescriptBinding.isImportClause(node)) return node.name?.text ?? null;
1399
+ if (typescriptBinding.isImportSpecifier(node) || typescriptBinding.isNamespaceImport(node)) return node.name.text;
1400
+ return null;
1401
+ };
1402
+ const findBindingScope = (node) => {
1403
+ let currentNode = node;
1404
+ while (currentNode !== void 0) {
1405
+ if (typescriptBinding.isParameter(currentNode) && typescriptBinding.isFunctionLike(currentNode.parent)) return currentNode.parent;
1406
+ if (typescriptBinding.isBlock(currentNode) || typescriptBinding.isModuleBlock(currentNode) || typescriptBinding.isSourceFile(currentNode)) return currentNode;
1407
+ currentNode = currentNode.parent;
1408
+ }
1409
+ return node.getSourceFile();
1410
+ };
1411
+ const findBindingDeclaration = (record, binding) => {
1412
+ const usagePosition = binding.getStart(record.sourceFile);
1413
+ const candidates = [];
1414
+ const visit = (node) => {
1415
+ if (getDeclaredBindingName(node) === binding.text) {
1416
+ const scope = findBindingScope(node);
1417
+ const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
1418
+ const isVisibleDeclaration = typescriptBinding.isFunctionDeclaration(node) || typescriptBinding.isParameter(node) || typescriptBinding.isBindingElement(node) || typescriptBinding.isImportClause(node) || typescriptBinding.isImportSpecifier(node) || typescriptBinding.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
1419
+ if (isVisibleScope && isVisibleDeclaration) candidates.push({
1420
+ declaration: node,
1421
+ scope
1422
+ });
1423
+ }
1424
+ typescriptBinding.forEachChild(node, visit);
1425
+ };
1426
+ visit(record.sourceFile);
1427
+ let nearestCandidate = null;
1428
+ let nearestScopeWidth = Number.POSITIVE_INFINITY;
1429
+ let isNearestScopeAmbiguous = false;
1430
+ for (const candidate of candidates) {
1431
+ const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
1432
+ if (scopeWidth < nearestScopeWidth) {
1433
+ nearestCandidate = candidate;
1434
+ nearestScopeWidth = scopeWidth;
1435
+ isNearestScopeAmbiguous = false;
1436
+ } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
1437
+ }
1438
+ if (isNearestScopeAmbiguous) return null;
1439
+ const declaration = nearestCandidate?.declaration;
1440
+ return declaration !== void 0 && (typescriptBinding.isVariableDeclaration(declaration) || typescriptBinding.isFunctionDeclaration(declaration)) ? declaration : null;
1441
+ };
1442
+ const resolveHandlerBinding = (record, binding) => {
1443
+ const declaration = findBindingDeclaration(record, binding);
1444
+ if (declaration === null) return binding.text;
1445
+ const forwardingFunction = getForwardingFunction(declaration);
1446
+ const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
1447
+ return target === null || target === binding.text ? binding.text : target;
1448
+ };
1449
+ const getJsxTagName = (node) => typescriptBinding.isIdentifier(node.tagName) ? node.tagName.text : null;
1450
+ const getHandlerBinding = (node, propName) => {
1451
+ for (const property of node.attributes.properties) {
1452
+ if (!typescriptBinding.isJsxAttribute(property) || property.name.getText() !== propName) continue;
1453
+ const initializer = property.initializer;
1454
+ if (initializer === void 0 || !typescriptBinding.isJsxExpression(initializer)) return null;
1455
+ if (initializer.expression === void 0) return null;
1456
+ const expression = unwrapExpression(initializer.expression);
1457
+ return typescriptBinding.isIdentifier(expression) ? expression : null;
1458
+ }
1459
+ return null;
1460
+ };
1461
+ const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
1462
+ const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
1463
+ const cachedAliases = state.aliasesByComponent.get(cacheKey);
1464
+ if (cachedAliases !== void 0) return cachedAliases;
1465
+ const bindingsByPropName = /* @__PURE__ */ new Map();
1466
+ const unresolvedPropNames = /* @__PURE__ */ new Set();
1467
+ let callsiteCount = 0;
1468
+ for (const record of listSourceRecords(rootDirectory, state)) {
1469
+ const tagNames = getComponentTagNames(record, component);
1470
+ if (tagNames.size === 0) continue;
1471
+ const visit = (node) => {
1472
+ if (record === component.record && node === component.node) return;
1473
+ if (typescriptBinding.isJsxOpeningElement(node) || typescriptBinding.isJsxSelfClosingElement(node)) {
1474
+ const tagName = getJsxTagName(node);
1475
+ if (tagName !== null && tagNames.has(tagName)) {
1476
+ callsiteCount += 1;
1477
+ for (const propName of propNames) {
1478
+ const binding = getHandlerBinding(node, propName);
1479
+ if (binding === null) {
1480
+ unresolvedPropNames.add(propName);
1481
+ continue;
1482
+ }
1483
+ const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
1484
+ bindings.add(resolveHandlerBinding(record, binding));
1485
+ bindingsByPropName.set(propName, bindings);
1486
+ }
1487
+ }
1488
+ }
1489
+ typescriptBinding.forEachChild(node, visit);
1490
+ };
1491
+ visit(record.sourceFile);
1492
+ }
1493
+ const aliases = /* @__PURE__ */ new Map();
1494
+ if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
1495
+ if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
1496
+ const [bindingName] = bindings;
1497
+ if (bindingName !== void 0) aliases.set(propName, bindingName);
1498
+ }
1499
+ state.aliasesByComponent.set(cacheKey, aliases);
1500
+ return aliases;
1501
+ };
1502
+ const normalizeForwardedHandlers = (evidence, aliases) => {
1503
+ let normalizedEvidence = evidence;
1504
+ for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
1505
+ return normalizedEvidence;
1506
+ };
1507
+ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
1508
+ const state = {
1509
+ aliasesByComponent: /* @__PURE__ */ new Map(),
1510
+ recordByFilePath: /* @__PURE__ */ new Map(),
1511
+ sourceRecords: null
1512
+ };
1513
+ return (diagnostic) => {
1514
+ const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
1515
+ if (record === null || !Number.isInteger(diagnostic.line)) return null;
1516
+ const sourceLines = record.sourceText.split(/\r?\n/);
1517
+ const startLineIndex = Math.max(0, diagnostic.line - 1);
1518
+ const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
1519
+ const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
1520
+ if (!options.resolveForwardedHandlers) return evidence;
1521
+ const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
1522
+ if (propNames.size === 0) return evidence;
1523
+ const component = findEnclosingComponent(record, diagnostic);
1524
+ if (component === null) return evidence;
1525
+ return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
1526
+ };
1527
+ };
1528
+ //#endregion
1529
+ //#region src/cli/utils/read-source-line.ts
1530
+ /**
1531
+ * Builds a cached `(filePath, line) -> source text` reader rooted at
1532
+ * `rootDirectory`, used as the fallback when construct-level diagnostic
1533
+ * evidence cannot be read. Wraps core's
1534
+ * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
1535
+ * lookup; unreadable files / out-of-range lines return `null`.
1536
+ */
1537
+ const createSourceLineReader = (rootDirectory) => {
1538
+ const readFileLines = createNodeReadFileLinesSync(rootDirectory);
1539
+ const fileLinesCache = /* @__PURE__ */ new Map();
1540
+ return (filePath, line) => {
1541
+ if (!fileLinesCache.has(filePath)) fileLinesCache.set(filePath, readFileLines(filePath));
1542
+ const lines = fileLinesCache.get(filePath) ?? null;
1543
+ if (lines === null || line < 1 || line > lines.length) return null;
1544
+ return lines[line - 1] ?? null;
1545
+ };
1546
+ };
1547
+ //#endregion
1548
+ //#region src/cli/utils/is-materializable-git-source.ts
1549
+ const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
1550
+ const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
1551
+ //#endregion
1552
+ //#region src/cli/utils/materialize-baseline-files.ts
1553
+ /**
1554
+ * Materializes the base side of the `ref` → worktree diff into
1555
+ * `tempDirectory`, mirroring the project layout plus head's config files so
1556
+ * both sides lint under the same rules. Git rename heuristics are disabled by
1557
+ * the diff planner, so an old path is retained as a base deletion while its
1558
+ * new path is a head addition. Missing head-only additions are expected, and a
1559
+ * base-only deletion that cannot be read has nothing at head to compare
1560
+ * against; only a missing path that the plan says exists on both sides makes
1561
+ * `isComplete` false.
1562
+ */
1563
+ const materializeBaselineFiles = (input) => runPromise(gen(function* () {
1564
+ const git = yield* Git;
1565
+ const diffPlan = yield* git.baselineDiffPlan({
1566
+ directory: input.directory,
1567
+ ref: input.ref
1568
+ });
1569
+ const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
1570
+ const headFiles = input.headFiles ?? diffPlan?.headFiles;
1571
+ const untrackedFiles = diffPlan?.untrackedFiles ?? [];
1572
+ if (baseFiles === void 0 || headFiles === void 0) return null;
1573
+ const files = [...new Set([...input.files, ...baseFiles])];
1574
+ const tree = yield* materializeSourceTree({
1575
+ directory: input.directory,
1576
+ files,
1577
+ tempDirectory: input.tempDirectory,
1578
+ readContent: (relativePath) => git.showRefContent({
1579
+ directory: input.directory,
1580
+ ref: input.ref,
1581
+ relativePath,
1582
+ options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
1583
+ }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
1584
+ });
1585
+ const materializedFiles = new Set(tree.materializedFiles);
1586
+ const headFileSet = new Set(headFiles);
1587
+ return {
1588
+ ...tree,
1589
+ baseFiles,
1590
+ headFiles,
1591
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
1592
+ untrackedFiles
1593
+ };
1594
+ }).pipe(provide(Git.layerNode)));
1595
+ /**
1596
+ * Resolves the commit a baseline scan should read base content from — the
1597
+ * merge-base of `ref` and HEAD, so "introduced" is measured against the branch
1598
+ * point. `null` when unresolvable (ref missing/unsafe, no merge-base).
1599
+ */
1600
+ const resolveMergeBaseRef = async (directory, ref) => {
1601
+ try {
1602
+ return await runPromise(gen(function* () {
1603
+ return yield* (yield* Git).mergeBase({
1604
+ directory,
1605
+ ref
1606
+ });
1607
+ }).pipe(provide(Git.layerNode)));
1608
+ } catch {
1609
+ return null;
1610
+ }
1611
+ };
1612
+ //#endregion
1613
+ //#region src/cli/utils/has-unlinted-surviving-base-file.ts
1614
+ const hasUnlintedSurvivingBaseFile = (input) => {
1615
+ const analyzedBaseFiles = new Set(input.analyzedBaseFiles.map(toForwardSlashes));
1616
+ return filterSourceFiles(input.baseLintPaths.map(toForwardSlashes)).some((filePath) => input.headFiles.has(filePath) && !analyzedBaseFiles.has(filePath));
1617
+ };
1618
+ //#endregion
1619
+ //#region src/cli/utils/run-baseline-comparison.ts
1620
+ const silentConsole$1 = makeNoopConsole();
1621
+ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
1622
+ const runBaselineComparison = async (input) => {
1623
+ const baselineIncludePaths = filterPathsOutsideDirectories({
1624
+ rootDirectory: input.directory,
1625
+ relativePaths: input.options.includePaths,
1626
+ excludedDirectories: input.options.excludedProjectDirectories
1627
+ });
1628
+ const baselineBaseFiles = input.baseFiles ? filterPathsOutsideDirectories({
1629
+ rootDirectory: input.directory,
1630
+ relativePaths: input.baseFiles,
1631
+ excludedDirectories: input.options.excludedProjectDirectories
1632
+ }) : void 0;
1633
+ const baselineHeadFiles = input.headFiles ? filterPathsOutsideDirectories({
1634
+ rootDirectory: input.directory,
1635
+ relativePaths: input.headFiles,
1636
+ excludedDirectories: input.options.excludedProjectDirectories
1637
+ }) : void 0;
1638
+ const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
1639
+ if (remainingBaselineBudgetMs === 0) return null;
1640
+ const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
1641
+ const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
1642
+ let maintainabilitySourceFiles = [];
1643
+ if (input.options.deadCode) try {
1644
+ maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
1645
+ } catch (error) {
1646
+ if (baselineDeadlineSignal?.aborted) return null;
1647
+ throw error;
1648
+ }
1649
+ const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
1650
+ const snapshot = await materializeBaselineFiles({
1651
+ directory: input.directory,
1652
+ ref: input.baselineRef,
1653
+ files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
1654
+ baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
1655
+ headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
1656
+ tempDirectory: temporaryDirectory
1657
+ }).catch((error) => {
1658
+ rmSync(temporaryDirectory, {
1659
+ recursive: true,
1660
+ force: true
1661
+ });
1662
+ throw error;
1663
+ });
1664
+ if (snapshot === null) {
1665
+ rmSync(temporaryDirectory, {
1666
+ recursive: true,
1667
+ force: true
1668
+ });
1669
+ return null;
1670
+ }
1671
+ try {
1672
+ if (!snapshot.isComplete) return null;
1673
+ if (input.options.deadCode && !await copyUnchangedBaselineSources({
1674
+ directory: input.directory,
1675
+ sourceFiles: maintainabilitySourceFiles,
1676
+ baseMaterializedFiles: snapshot.materializedFiles,
1677
+ headChangedFiles: snapshot.headFiles,
1678
+ untrackedFiles: snapshot.untrackedFiles,
1679
+ tempDirectory: snapshot.tempDirectory,
1680
+ deadlineEpochMs: input.deadlineEpochMs,
1681
+ signal: input.oxlintRuntime.abortSignal
1682
+ })) return null;
1683
+ const filteredSnapshotBaseFiles = filterPathsOutsideDirectories({
1684
+ rootDirectory: input.directory,
1685
+ relativePaths: snapshot.baseFiles,
1686
+ excludedDirectories: input.options.excludedProjectDirectories
1687
+ });
1688
+ const filteredSnapshotHeadFiles = filterPathsOutsideDirectories({
1689
+ rootDirectory: input.directory,
1690
+ relativePaths: snapshot.headFiles,
1691
+ excludedDirectories: input.options.excludedProjectDirectories
1692
+ });
1693
+ const analyzedHeadFiles = new Set(input.headAnalyzedFiles.map(toForwardSlashes));
1694
+ const baseFiles = new Set((baselineBaseFiles ?? filteredSnapshotBaseFiles).map(toForwardSlashes));
1695
+ const expectedHeadFiles = new Set((baselineHeadFiles ?? filteredSnapshotHeadFiles).map(toForwardSlashes));
1696
+ for (const filePath of baselineIncludePaths) {
1697
+ const normalizedFilePath = toForwardSlashes(filePath);
1698
+ if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
1699
+ }
1700
+ if (input.options.lint && filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
1701
+ const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
1702
+ const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
1703
+ const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
1704
+ const baselineExcludedProjectDirectories = input.options.excludedProjectDirectories.map((excludedDirectory) => path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.directory)).map((excludedDirectory) => path.resolve(snapshot.tempDirectory, path.relative(input.directory, excludedDirectory)));
1705
+ const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
1706
+ const runtimeLayers = buildRuntimeLayers({
1707
+ directory: snapshot.tempDirectory,
1708
+ hasConfigOverride: true,
1709
+ userConfig: input.userConfig,
1710
+ configSourceDirectory: input.configSourceDirectory,
1711
+ projectInfoOverride: input.headProjectInfo,
1712
+ shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
1713
+ shouldRunDeadCode: input.options.deadCode,
1714
+ shouldRunSupplyChain: input.options.supplyChain,
1715
+ shouldComputeScore: false,
1716
+ shouldShowProgressSpinners: false,
1717
+ oxlintConcurrency: input.oxlintRuntime.concurrency,
1718
+ oxlintSpawnSlots: input.oxlintRuntime.spawnSlots
1719
+ });
1720
+ const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
1721
+ directory: snapshot.tempDirectory,
1722
+ includePaths: baseIncludePaths,
1723
+ maintainabilityFocusPaths,
1724
+ customRulesOnly: input.options.customRulesOnly,
1725
+ respectInlineDisables: input.options.respectInlineDisables,
1726
+ warnings: input.options.warnings,
1727
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
1728
+ ignoredTags: input.options.ignoredTags,
1729
+ includedTags: input.options.includedTags,
1730
+ includeTagDefaults: input.options.includeTagDefaults,
1731
+ nodeBinaryPath: input.resolvedNodeBinaryPath ?? void 0,
1732
+ runDeadCode: input.options.deadCode,
1733
+ isCi: input.options.isCi,
1734
+ doctorVersion: VERSION,
1735
+ runId: getRunId(),
1736
+ resolveLocalGithubViewerPermission: false,
1737
+ suppressScanSummary: true,
1738
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged,
1739
+ deadlineEpochMs: input.deadlineEpochMs ?? void 0,
1740
+ signal: input.oxlintRuntime.abortSignal,
1741
+ excludedProjectDirectories: baselineExcludedProjectDirectories,
1742
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
1743
+ }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
1744
+ if (baseOutput.didLintFail || baseOutput.didDeadCodeFail) return null;
1745
+ if (countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0 && hasUnlintedSurvivingBaseFile({
1746
+ baseLintPaths: materializedLintPaths,
1747
+ headFiles: expectedHeadFiles,
1748
+ analyzedBaseFiles: baseOutput.analyzedFiles
1749
+ })) return null;
1750
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
1751
+ rootDirectory: input.directory,
1752
+ relativePaths: snapshot.untrackedFiles,
1753
+ excludedDirectories: input.options.excludedProjectDirectories
1754
+ }).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
1755
+ const diagnosticDelta = computeDiagnosticDelta({
1756
+ headDiagnostics: input.headDiagnostics,
1757
+ baseDiagnostics: baseOutput.diagnostics,
1758
+ readHeadLine: createSourceLineReader(input.directory),
1759
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
1760
+ readHeadEvidence: createDiagnosticEvidenceReader(input.directory, { resolveForwardedHandlers: true }),
1761
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
1762
+ });
1763
+ return {
1764
+ displayDiagnostics: diagnosticDelta.newDiagnostics,
1765
+ baselineDelta: {
1766
+ baseRef: input.baselineRef,
1767
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : diagnosticDelta.fixedCount,
1768
+ baseTotalCount: baseOutput.diagnostics.length,
1769
+ crossFileMatchCount: diagnosticDelta.crossFileMatchCount
1770
+ }
1771
+ };
1772
+ } finally {
1773
+ snapshot.cleanup();
1774
+ }
1775
+ };
1776
+ //#endregion
1777
+ //#region src/cli/utils/scan-result-cache-payload.ts
1778
+ const decodeDiagnostic = decodeUnknownSync(Diagnostic);
1779
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1780
+ const isNullableString = (value) => value === null || typeof value === "string";
1781
+ const isBoolean = (value) => typeof value === "boolean";
1782
+ const isNumber = (value) => typeof value === "number";
1783
+ const isString = (value) => typeof value === "string";
1784
+ const isDiagnosticArray = (value) => {
1785
+ if (!Array.isArray(value)) return false;
1786
+ try {
1787
+ for (const entry of value) decodeDiagnostic(entry);
1788
+ return true;
1789
+ } catch {
1790
+ return false;
1791
+ }
1792
+ };
1793
+ const isScoreResult = (value) => value === null || isRecord(value) && typeof value.score === "number" && typeof value.label === "string";
1794
+ const isProjectInfo = (value) => isRecord(value) && typeof value.rootDirectory === "string" && typeof value.projectName === "string" && typeof value.framework === "string" && typeof value.sourceFileCount === "number";
1795
+ const isBaselineDelta = (value) => isRecord(value) && typeof value.baseRef === "string" && typeof value.fixedCount === "number" && typeof value.baseTotalCount === "number" && (value.crossFileMatchCount === void 0 || typeof value.crossFileMatchCount === "number");
1796
+ const isSuppressedRuleCountArray = (value) => Array.isArray(value) && value.every((entry) => isRecord(entry) && typeof entry.rule === "string" && typeof entry.source === "string" && typeof entry.count === "number");
1797
+ const REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS = [
1798
+ {
1799
+ key: "diagnostics",
1800
+ isValid: isDiagnosticArray
1801
+ },
1802
+ {
1803
+ key: "score",
1804
+ isValid: isScoreResult
1805
+ },
1806
+ {
1807
+ key: "project",
1808
+ isValid: isProjectInfo
1809
+ },
1810
+ {
1811
+ key: "userConfig",
1812
+ isValid: (value) => value === null || isRecord(value)
1813
+ },
1814
+ {
1815
+ key: "didLintFail",
1816
+ isValid: isBoolean
1817
+ },
1818
+ {
1819
+ key: "lintFailureReason",
1820
+ isValid: isNullableString
1821
+ },
1822
+ {
1823
+ key: "lintPartialFailures",
1824
+ isValid: isStringArray
1825
+ },
1826
+ {
1827
+ key: "didDeadCodeFail",
1828
+ isValid: isBoolean
1829
+ },
1830
+ {
1831
+ key: "deadCodeFailureReason",
1832
+ isValid: isNullableString
1833
+ },
1834
+ {
1835
+ key: "deadCodeOverlapped",
1836
+ isValid: isBoolean
1837
+ },
1838
+ {
1839
+ key: "directory",
1840
+ isValid: isString
1841
+ },
1842
+ {
1843
+ key: "scannedFileCount",
1844
+ isValid: isNumber
1845
+ },
1846
+ {
1847
+ key: "scannedFilePaths",
1848
+ isValid: isStringArray
1849
+ },
1850
+ {
1851
+ key: "scanElapsedMilliseconds",
1852
+ isValid: isNumber
1853
+ },
1854
+ {
1855
+ key: "lintFailureReasonKind",
1856
+ isValid: isNullableString
1857
+ },
1858
+ {
1859
+ key: "supplyChainOverlapTimedOut",
1860
+ isValid: isBoolean
1861
+ }
1862
+ ];
1863
+ const OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS = [
1864
+ {
1865
+ key: "analyzedFiles",
1866
+ isValid: isStringArray
1867
+ },
1868
+ {
1869
+ key: "baselineDelta",
1870
+ isValid: isBaselineDelta
1871
+ },
1872
+ {
1873
+ key: "scanConcurrency",
1874
+ isValid: isNumber
1875
+ },
1876
+ {
1877
+ key: "securityScanFailed",
1878
+ isValid: isBoolean
1879
+ },
1880
+ {
1881
+ key: "securityScanFailureReason",
1882
+ isValid: isNullableString
1883
+ },
1884
+ {
1885
+ key: "suppressedRuleCounts",
1886
+ isValid: isSuppressedRuleCountArray
1887
+ },
1888
+ {
1889
+ key: "manifestContentHash",
1890
+ isValid: isNullableString
1891
+ }
1892
+ ];
1893
+ const hasValidRequiredCachedScanPayloadFields = (value) => REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => field.isValid(value[field.key]));
1894
+ const hasValidOptionalCachedScanPayloadFields = (value) => OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => value[field.key] === void 0 || field.isValid(value[field.key]));
1895
+ const isCachedScanPayload = (value) => {
1896
+ if (!isRecord(value)) return false;
1897
+ return hasValidRequiredCachedScanPayloadFields(value) && hasValidOptionalCachedScanPayloadFields(value);
1898
+ };
1899
+ const decodeCachedScanPayload = (value) => isCachedScanPayload(value) ? value : null;
1900
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
1901
+ //#endregion
1902
+ //#region src/cli/utils/scan-result-cache.ts
1903
+ const createScanResultCacheInvocationState = () => ({
1904
+ repositoryIdentityByRoot: /* @__PURE__ */ new Map(),
1905
+ firstProjectIdentity: null
1906
+ });
1907
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
1908
+ "oxlint/package.json",
1909
+ "oxlint-plugin-react-doctor/package.json",
1910
+ "eslint-plugin-react-hooks/package.json"
1618
1911
  ];
1619
- const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
1620
- const getScriptKind = (filePath) => {
1621
- const extension = path.extname(filePath).toLowerCase();
1622
- if (extension === ".tsx") return ts.ScriptKind.TSX;
1623
- if (extension === ".jsx") return ts.ScriptKind.JSX;
1624
- if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return ts.ScriptKind.JS;
1625
- return ts.ScriptKind.TS;
1626
- };
1627
- const resolveSafeSourcePath = (rootDirectory, filePath) => {
1912
+ const bundledRequire = createRequire(import.meta.url);
1913
+ const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
1628
1914
  try {
1629
- const resolvedRootDirectory = fs.realpathSync(rootDirectory);
1630
- const absolutePath = path.resolve(resolvedRootDirectory, filePath);
1631
- const relativePath = path.relative(resolvedRootDirectory, absolutePath);
1632
- if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
1633
- const stats = fs.lstatSync(absolutePath);
1634
- if (!stats.isFile() || stats.isSymbolicLink()) return null;
1635
- const realPath = fs.realpathSync(absolutePath);
1636
- const realRelativePath = path.relative(resolvedRootDirectory, realPath);
1637
- return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
1915
+ return `oxlint-plugin-react-doctor#fingerprint=${hashFileContents(bundledRequire.resolve("oxlint-plugin-react-doctor")) ?? "unreadable"}`;
1638
1916
  } catch {
1639
- return null;
1917
+ return "oxlint-plugin-react-doctor#fingerprint=unresolved";
1640
1918
  }
1641
- };
1642
- const readSourceRecord = (rootDirectory, filePath, state) => {
1643
- const normalizedFilePath = filePath.replace(/\\/g, "/");
1644
- if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
1645
- const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
1646
- if (absolutePath === null) {
1647
- state.recordByFilePath.set(normalizedFilePath, null);
1648
- return null;
1919
+ })();
1920
+ const normalizeForStableJson = (value) => {
1921
+ if (value === null) return null;
1922
+ if (value === void 0) return void 0;
1923
+ const valueType = typeof value;
1924
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
1925
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
1926
+ if (Array.isArray(value)) return value.map((item) => {
1927
+ const normalized = normalizeForStableJson(item);
1928
+ return normalized === void 0 ? null : normalized;
1929
+ });
1930
+ const prototype = Object.getPrototypeOf(value);
1931
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
1932
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
1933
+ const normalizedRecord = {};
1934
+ for (const key of Object.keys(value).sort()) {
1935
+ const normalized = normalizeForStableJson(value[key]);
1936
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
1649
1937
  }
1938
+ return normalizedRecord;
1939
+ };
1940
+ const stringifyStableJson = (value) => {
1650
1941
  try {
1651
- const sourceText = fs.readFileSync(absolutePath, "utf-8");
1652
- const record = {
1653
- absolutePath,
1654
- filePath: normalizedFilePath,
1655
- sourceFile: ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
1656
- sourceText
1657
- };
1658
- state.recordByFilePath.set(normalizedFilePath, record);
1659
- return record;
1942
+ return JSON.stringify(normalizeForStableJson(value));
1660
1943
  } catch {
1661
- state.recordByFilePath.set(normalizedFilePath, null);
1662
1944
  return null;
1663
1945
  }
1664
1946
  };
1665
- const listSourceRecords = (rootDirectory, state) => {
1666
- if (state.sourceRecords !== null) return state.sourceRecords;
1667
- state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
1668
- const record = readSourceRecord(rootDirectory, filePath, state);
1669
- return record === null ? [] : [record];
1670
- });
1671
- return state.sourceRecords;
1672
- };
1673
- const unwrapExpression = (expression) => {
1674
- let currentExpression = expression;
1675
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
1676
- return currentExpression;
1677
- };
1678
- const getCalleeName = (expression) => {
1679
- const unwrappedExpression = unwrapExpression(expression);
1680
- if (ts.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
1681
- if (ts.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
1682
- return null;
1683
- };
1684
- const isComponentWrapperCall = (node) => {
1685
- const wrapperName = getCalleeName(node.expression);
1686
- return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
1687
- };
1688
- const getFunctionName = (node) => {
1689
- if (ts.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
1690
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1691
- let componentExpression = node;
1692
- while (true) {
1693
- const parent = componentExpression.parent;
1694
- if (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) {
1695
- componentExpression = parent;
1696
- continue;
1697
- }
1698
- if (ts.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
1699
- componentExpression = parent;
1700
- continue;
1701
- }
1702
- if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
1703
- return node.name?.text ?? null;
1704
- }
1705
- }
1706
- return null;
1707
- };
1708
- const isComponentFunction = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
1709
- const isDefaultExportedComponent = (record, node, componentName) => {
1710
- if (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true;
1711
- return record.sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression) && statement.expression.text === componentName);
1712
- };
1713
- const findEnclosingComponent = (record, diagnostic) => {
1714
- if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
1715
- const lineIndex = diagnostic.line - 1;
1716
- const lineStarts = record.sourceFile.getLineStarts();
1717
- const lineStart = lineStarts[lineIndex];
1718
- if (lineStart === void 0) return null;
1719
- const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
1720
- const lineEnd = Math.max(lineStart, nextLineStart - 1);
1721
- const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
1722
- const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
1723
- let component = null;
1724
- let componentWidth = Number.POSITIVE_INFINITY;
1725
- const visit = (node) => {
1726
- if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
1727
- if (isComponentFunction(node)) {
1728
- const name = getFunctionName(node);
1729
- const width = node.end - node.getStart(record.sourceFile);
1730
- if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
1731
- component = {
1732
- isDefaultExport: isDefaultExportedComponent(record, node, name),
1733
- name,
1734
- node,
1735
- record
1736
- };
1737
- componentWidth = width;
1738
- }
1947
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
1948
+ const readHeadSha = (projectDirectory) => runGitAsync(projectDirectory, HEAD_SHA_GIT_ARGUMENTS);
1949
+ const parseWorktreeStatusRecords = (statusOutput) => {
1950
+ const entries = [];
1951
+ const tokens = statusOutput.split("\0");
1952
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
1953
+ let record = tokens[tokenIndex];
1954
+ if (record.length === 0) continue;
1955
+ if (record.length < 3 || record[2] !== " ") {
1956
+ if (tokenIndex !== 0) return null;
1957
+ record = ` ${record}`;
1739
1958
  }
1740
- ts.forEachChild(node, visit);
1741
- };
1742
- visit(record.sourceFile);
1743
- return component;
1744
- };
1745
- const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
1746
- if (!moduleSpecifier.startsWith(".")) return false;
1747
- const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
1748
- const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
1749
- const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
1750
- return [
1751
- unresolvedPath,
1752
- ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
1753
- ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
1754
- ].includes(componentRecord.absolutePath);
1755
- };
1756
- const getComponentTagNames = (record, component) => {
1757
- if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
1758
- const tagNames = /* @__PURE__ */ new Set();
1759
- for (const statement of record.sourceFile.statements) {
1760
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
1761
- const importClause = statement.importClause;
1762
- if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
1763
- const namedBindings = importClause?.namedBindings;
1764
- if (namedBindings !== void 0 && ts.isNamedImports(namedBindings)) {
1765
- for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
1959
+ if (record.length < 4 || record[2] !== " ") return null;
1960
+ const statusCode = record.slice(0, 2);
1961
+ const recordPath = record.slice(3);
1962
+ if (statusCode.includes("R") || statusCode.includes("C")) {
1963
+ tokenIndex += 1;
1964
+ const originPath = tokens[tokenIndex];
1965
+ if (originPath === void 0 || originPath.length === 0) return null;
1966
+ entries.push({
1967
+ statusCode,
1968
+ path: recordPath,
1969
+ originPath
1970
+ });
1971
+ continue;
1766
1972
  }
1973
+ entries.push({
1974
+ statusCode,
1975
+ path: recordPath
1976
+ });
1767
1977
  }
1768
- return tagNames;
1769
- };
1770
- const getTransparentCall = (functionNode) => {
1771
- if (functionNode.body === void 0) return null;
1772
- if (!ts.isBlock(functionNode.body)) {
1773
- const expression = unwrapExpression(functionNode.body);
1774
- return ts.isCallExpression(expression) ? expression : null;
1775
- }
1776
- if (functionNode.body.statements.length !== 1) return null;
1777
- const statement = functionNode.body.statements[0];
1778
- if (statement === void 0) return null;
1779
- const expression = ts.isExpressionStatement(statement) || ts.isReturnStatement(statement) ? statement.expression : void 0;
1780
- if (expression === void 0) return null;
1781
- const unwrappedExpression = unwrapExpression(expression);
1782
- return ts.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
1783
- };
1784
- const resolveTransparentTarget = (functionNode) => {
1785
- const call = getTransparentCall(functionNode);
1786
- if (call === null) return null;
1787
- const target = unwrapExpression(call.expression);
1788
- if (!ts.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
1789
- for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
1790
- const argument = call.arguments[parameterIndex];
1791
- const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
1792
- if (!ts.isIdentifier(parameter.name) || unwrappedArgument === null || !ts.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
1793
- }
1794
- return target.text;
1978
+ return entries;
1795
1979
  };
1796
- const getForwardingFunction = (declaration) => {
1797
- if (ts.isFunctionDeclaration(declaration)) return declaration;
1798
- if (!ts.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
1799
- const initializer = unwrapExpression(declaration.initializer);
1800
- if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer;
1801
- if (ts.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
1802
- const callback = initializer.arguments[0];
1803
- if (callback !== void 0 && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return callback;
1980
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
1981
+ let stats;
1982
+ try {
1983
+ stats = fs.lstatSync(absolutePath);
1984
+ } catch {
1985
+ return statusCode.includes("D") ? "deleted" : null;
1804
1986
  }
1805
- return null;
1806
- };
1807
- const getDeclaredBindingName = (node) => {
1808
- if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isFunctionDeclaration(node)) && node.name !== void 0 && ts.isIdentifier(node.name)) return node.name.text;
1809
- if (ts.isImportClause(node)) return node.name?.text ?? null;
1810
- if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) return node.name.text;
1811
- return null;
1987
+ if (stats.isSymbolicLink()) try {
1988
+ return `link:${fs.readlinkSync(absolutePath)}`;
1989
+ } catch {
1990
+ return null;
1991
+ }
1992
+ if (!stats.isFile()) return null;
1993
+ if (stats.size > 10485760) return null;
1994
+ return hashFileContents(absolutePath);
1812
1995
  };
1813
- const findBindingScope = (node) => {
1814
- let currentNode = node;
1815
- while (currentNode !== void 0) {
1816
- if (ts.isParameter(currentNode) && ts.isFunctionLike(currentNode.parent)) return currentNode.parent;
1817
- if (ts.isBlock(currentNode) || ts.isModuleBlock(currentNode) || ts.isSourceFile(currentNode)) return currentNode;
1818
- currentNode = currentNode.parent;
1996
+ const buildWorktreeFingerprint = (repositoryRoot, statusOutput) => {
1997
+ if (statusOutput === null) return null;
1998
+ if (statusOutput.length === 0) return [];
1999
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
2000
+ if (statusEntries === null) return null;
2001
+ if (statusEntries.length > 300) return null;
2002
+ const dirtyEntries = [];
2003
+ for (const statusEntry of statusEntries) {
2004
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
2005
+ if (contentFingerprint === null) return null;
2006
+ dirtyEntries.push({
2007
+ ...statusEntry,
2008
+ contentFingerprint
2009
+ });
1819
2010
  }
1820
- return node.getSourceFile();
2011
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
2012
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
2013
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
2014
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
2015
+ });
1821
2016
  };
1822
- const findBindingDeclaration = (record, binding) => {
1823
- const usagePosition = binding.getStart(record.sourceFile);
1824
- const candidates = [];
1825
- const visit = (node) => {
1826
- if (getDeclaredBindingName(node) === binding.text) {
1827
- const scope = findBindingScope(node);
1828
- const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
1829
- const isVisibleDeclaration = ts.isFunctionDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isImportClause(node) || ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
1830
- if (isVisibleScope && isVisibleDeclaration) candidates.push({
1831
- declaration: node,
1832
- scope
1833
- });
1834
- }
1835
- ts.forEachChild(node, visit);
2017
+ const resolveIdentityFromProbes = async (repositoryRoot, statusPromise, headShaPromise, invocationState) => {
2018
+ const [statusOutput, headSha] = await Promise.all([statusPromise, headShaPromise]);
2019
+ const worktreeFingerprint = buildWorktreeFingerprint(repositoryRoot, statusOutput);
2020
+ const identity = worktreeFingerprint === null || headSha === null ? null : {
2021
+ headSha,
2022
+ worktreeFingerprint
1836
2023
  };
1837
- visit(record.sourceFile);
1838
- let nearestCandidate = null;
1839
- let nearestScopeWidth = Number.POSITIVE_INFINITY;
1840
- let isNearestScopeAmbiguous = false;
1841
- for (const candidate of candidates) {
1842
- const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
1843
- if (scopeWidth < nearestScopeWidth) {
1844
- nearestCandidate = candidate;
1845
- nearestScopeWidth = scopeWidth;
1846
- isNearestScopeAmbiguous = false;
1847
- } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
1848
- }
1849
- if (isNearestScopeAmbiguous) return null;
1850
- const declaration = nearestCandidate?.declaration;
1851
- return declaration !== void 0 && (ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration)) ? declaration : null;
2024
+ invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
2025
+ return identity;
1852
2026
  };
1853
- const resolveHandlerBinding = (record, binding) => {
1854
- const declaration = findBindingDeclaration(record, binding);
1855
- if (declaration === null) return binding.text;
1856
- const forwardingFunction = getForwardingFunction(declaration);
1857
- const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
1858
- return target === null || target === binding.text ? binding.text : target;
2027
+ const resolveSiblingIdentity = async (projectDirectory, invocationState, repositoryRootPromise, firstProjectIdentity) => {
2028
+ const [repositoryRoot] = await Promise.all([repositoryRootPromise, firstProjectIdentity]);
2029
+ if (repositoryRoot === null) return null;
2030
+ const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
2031
+ if (cachedIdentity !== void 0) return cachedIdentity;
2032
+ return resolveIdentityFromProbes(repositoryRoot, runGitAsync(projectDirectory, WORKTREE_STATUS_GIT_ARGUMENTS), readHeadSha(repositoryRoot), invocationState);
1859
2033
  };
1860
- const getJsxTagName = (node) => ts.isIdentifier(node.tagName) ? node.tagName.text : null;
1861
- const getHandlerBinding = (node, propName) => {
1862
- for (const property of node.attributes.properties) {
1863
- if (!ts.isJsxAttribute(property) || property.name.getText() !== propName) continue;
1864
- const initializer = property.initializer;
1865
- if (initializer === void 0 || !ts.isJsxExpression(initializer)) return null;
1866
- if (initializer.expression === void 0) return null;
1867
- const expression = unwrapExpression(initializer.expression);
1868
- return ts.isIdentifier(expression) ? expression : null;
1869
- }
1870
- return null;
2034
+ const resolveRepositoryCacheIdentity = (projectDirectory, invocationState, repositoryRootPromise) => {
2035
+ if (invocationState.firstProjectIdentity !== null) return resolveSiblingIdentity(projectDirectory, invocationState, repositoryRootPromise, invocationState.firstProjectIdentity);
2036
+ const speculativeStatus = runGitAsync(projectDirectory, WORKTREE_STATUS_GIT_ARGUMENTS);
2037
+ const speculativeHeadSha = readHeadSha(projectDirectory);
2038
+ invocationState.firstProjectIdentity = repositoryRootPromise.then((repositoryRoot) => repositoryRoot === null ? null : resolveIdentityFromProbes(repositoryRoot, speculativeStatus, speculativeHeadSha, invocationState));
2039
+ return invocationState.firstProjectIdentity;
1871
2040
  };
1872
- const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
1873
- const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
1874
- const cachedAliases = state.aliasesByComponent.get(cacheKey);
1875
- if (cachedAliases !== void 0) return cachedAliases;
1876
- const bindingsByPropName = /* @__PURE__ */ new Map();
1877
- const unresolvedPropNames = /* @__PURE__ */ new Set();
1878
- let callsiteCount = 0;
1879
- for (const record of listSourceRecords(rootDirectory, state)) {
1880
- const tagNames = getComponentTagNames(record, component);
1881
- if (tagNames.size === 0) continue;
1882
- const visit = (node) => {
1883
- if (record === component.record && node === component.node) return;
1884
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
1885
- const tagName = getJsxTagName(node);
1886
- if (tagName !== null && tagNames.has(tagName)) {
1887
- callsiteCount += 1;
1888
- for (const propName of propNames) {
1889
- const binding = getHandlerBinding(node, propName);
1890
- if (binding === null) {
1891
- unresolvedPropNames.add(propName);
1892
- continue;
1893
- }
1894
- const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
1895
- bindings.add(resolveHandlerBinding(record, binding));
1896
- bindingsByPropName.set(propName, bindings);
1897
- }
1898
- }
1899
- }
1900
- ts.forEachChild(node, visit);
1901
- };
1902
- visit(record.sourceFile);
1903
- }
1904
- const aliases = /* @__PURE__ */ new Map();
1905
- if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
1906
- if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
1907
- const [bindingName] = bindings;
1908
- if (bindingName !== void 0) aliases.set(propName, bindingName);
2041
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
2042
+ const resolveDotenvFingerprint = (projectDirectory) => {
2043
+ try {
2044
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
2045
+ } catch {
2046
+ return [];
1909
2047
  }
1910
- state.aliasesByComponent.set(cacheKey, aliases);
1911
- return aliases;
1912
- };
1913
- const normalizeForwardedHandlers = (evidence, aliases) => {
1914
- let normalizedEvidence = evidence;
1915
- for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
1916
- return normalizedEvidence;
1917
- };
1918
- const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
1919
- const state = {
1920
- aliasesByComponent: /* @__PURE__ */ new Map(),
1921
- recordByFilePath: /* @__PURE__ */ new Map(),
1922
- sourceRecords: null
1923
- };
1924
- return (diagnostic) => {
1925
- const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
1926
- if (record === null || !Number.isInteger(diagnostic.line)) return null;
1927
- const sourceLines = record.sourceText.split(/\r?\n/);
1928
- const startLineIndex = Math.max(0, diagnostic.line - 1);
1929
- const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
1930
- const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
1931
- if (!options.resolveForwardedHandlers) return evidence;
1932
- const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
1933
- if (propNames.size === 0) return evidence;
1934
- const component = findEnclosingComponent(record, diagnostic);
1935
- if (component === null) return evidence;
1936
- return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
1937
- };
1938
2048
  };
1939
- //#endregion
1940
- //#region src/cli/utils/read-source-line.ts
1941
- /**
1942
- * Builds a cached `(filePath, line) -> source text` reader rooted at
1943
- * `rootDirectory`, used as the fallback when construct-level diagnostic
1944
- * evidence cannot be read. Wraps core's
1945
- * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
1946
- * lookup; unreadable files / out-of-range lines return `null`.
1947
- */
1948
- const createSourceLineReader = (rootDirectory) => {
1949
- const readFileLines = createNodeReadFileLinesSync(rootDirectory);
1950
- const fileLinesCache = /* @__PURE__ */ new Map();
1951
- return (filePath, line) => {
1952
- if (!fileLinesCache.has(filePath)) fileLinesCache.set(filePath, readFileLines(filePath));
1953
- const lines = fileLinesCache.get(filePath) ?? null;
1954
- if (lines === null || line < 1 || line > lines.length) return null;
1955
- return lines[line - 1] ?? null;
1956
- };
2049
+ const isGitIdentityTrustworthy = async (projectDirectory) => {
2050
+ const output = await runGitAsync(projectDirectory, TRACKED_FILE_FLAGS_GIT_ARGUMENTS);
2051
+ if (output === null) return false;
2052
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
2053
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
2054
+ };
2055
+ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
2056
+ const readPersistedCache = (cacheFilePath) => {
2057
+ try {
2058
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
2059
+ if (!isRecord(parsed) || parsed.version !== 7) return {
2060
+ version: 7,
2061
+ entries: []
2062
+ };
2063
+ if (!Array.isArray(parsed.entries)) return {
2064
+ version: 7,
2065
+ entries: []
2066
+ };
2067
+ const entries = [];
2068
+ for (const entry of parsed.entries) {
2069
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
2070
+ const payload = decodeCachedScanPayload(entry.payload);
2071
+ if (payload === null) continue;
2072
+ entries.push({
2073
+ key: entry.key,
2074
+ createdAtMs: entry.createdAtMs,
2075
+ payload
2076
+ });
2077
+ }
2078
+ return {
2079
+ version: 7,
2080
+ entries
2081
+ };
2082
+ } catch {
2083
+ return {
2084
+ version: 7,
2085
+ entries: []
2086
+ };
2087
+ }
1957
2088
  };
1958
- //#endregion
1959
- //#region src/cli/utils/is-materializable-git-source.ts
1960
- const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
1961
- const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
1962
- //#endregion
1963
- //#region src/cli/utils/materialize-baseline-files.ts
1964
- /**
1965
- * Materializes the base side of the `ref` → worktree diff into
1966
- * `tempDirectory`, mirroring the project layout plus head's config files so
1967
- * both sides lint under the same rules. Git rename heuristics are disabled by
1968
- * the diff planner, so an old path is retained as a base deletion while its
1969
- * new path is a head addition. Missing head-only additions are expected;
1970
- * missing paths that the plan says must exist at base make `isComplete` false.
1971
- */
1972
- const materializeBaselineFiles = (input) => runPromise(gen(function* () {
1973
- const git = yield* Git;
1974
- const diffPlan = yield* git.baselineDiffPlan({
1975
- directory: input.directory,
1976
- ref: input.ref
1977
- });
1978
- const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
1979
- const headFiles = input.headFiles ?? diffPlan?.headFiles;
1980
- const untrackedFiles = diffPlan?.untrackedFiles ?? [];
1981
- if (baseFiles === void 0 || headFiles === void 0) return null;
1982
- const files = [...new Set([...input.files, ...baseFiles])];
1983
- const tree = yield* materializeSourceTree({
1984
- directory: input.directory,
1985
- files,
1986
- tempDirectory: input.tempDirectory,
1987
- readContent: (relativePath) => git.showRefContent({
1988
- directory: input.directory,
1989
- ref: input.ref,
1990
- relativePath,
1991
- options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
1992
- }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
1993
- });
1994
- const materializedFiles = new Set(tree.materializedFiles);
1995
- return {
1996
- ...tree,
1997
- baseFiles,
1998
- headFiles,
1999
- isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
2000
- untrackedFiles
2001
- };
2002
- }).pipe(provide(Git.layerNode)));
2003
2089
  /**
2004
- * Resolves the commit a baseline scan should read base content from — the
2005
- * merge-base of `ref` and HEAD, so "introduced" is measured against the branch
2006
- * point. `null` when unresolvable (ref missing/unsafe, no merge-base).
2090
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
2091
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
2092
+ * sidecar caches (their `Context.Reference` defaults read the
2093
+ * same variable). Exported for the wide event's `cache.temperature`
2094
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
2095
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
2096
+ * not consulted — they leave the other subsystems live.
2007
2097
  */
2008
- const resolveMergeBaseRef = async (directory, ref) => {
2098
+ const resolveProjectIdentity = (projectDirectory) => {
2009
2099
  try {
2010
- return await runPromise(gen(function* () {
2011
- return yield* (yield* Git).mergeBase({
2012
- directory,
2013
- ref
2014
- });
2015
- }).pipe(provide(Git.layerNode)));
2100
+ return fs.realpathSync.native(projectDirectory);
2016
2101
  } catch {
2017
- return null;
2102
+ return path.resolve(projectDirectory);
2018
2103
  }
2019
2104
  };
2020
- //#endregion
2021
- //#region src/cli/utils/run-baseline-comparison.ts
2022
- const silentConsole$1 = makeNoopConsole();
2023
- const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
2024
- const runBaselineComparison = async (input) => {
2025
- const baselineIncludePaths = filterPathsOutsideDirectories({
2026
- rootDirectory: input.directory,
2027
- relativePaths: input.options.includePaths,
2028
- excludedDirectories: input.options.excludedProjectDirectories
2029
- });
2030
- const baselineBaseFiles = input.baseFiles ? filterPathsOutsideDirectories({
2031
- rootDirectory: input.directory,
2032
- relativePaths: input.baseFiles,
2033
- excludedDirectories: input.options.excludedProjectDirectories
2034
- }) : void 0;
2035
- const baselineHeadFiles = input.headFiles ? filterPathsOutsideDirectories({
2036
- rootDirectory: input.directory,
2037
- relativePaths: input.headFiles,
2038
- excludedDirectories: input.options.excludedProjectDirectories
2039
- }) : void 0;
2040
- const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
2041
- if (remainingBaselineBudgetMs === 0) return null;
2042
- const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
2043
- const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
2044
- let maintainabilitySourceFiles = [];
2045
- if (input.options.deadCode) try {
2046
- maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => JSX_DUPLICATION_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
2047
- } catch (error) {
2048
- if (baselineDeadlineSignal?.aborted) return null;
2049
- throw error;
2050
- }
2051
- const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
2052
- const snapshot = await materializeBaselineFiles({
2053
- directory: input.directory,
2054
- ref: input.baselineRef,
2055
- files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
2056
- baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
2057
- headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
2058
- tempDirectory: temporaryDirectory
2059
- }).catch((error) => {
2060
- rmSync(temporaryDirectory, {
2061
- recursive: true,
2062
- force: true
2063
- });
2064
- throw error;
2065
- });
2066
- if (snapshot === null) {
2067
- rmSync(temporaryDirectory, {
2068
- recursive: true,
2069
- force: true
2070
- });
2105
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
2106
+ const fileFingerprint = (filePath) => {
2107
+ try {
2108
+ const stat = fs.statSync(filePath);
2109
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
2110
+ } catch {
2071
2111
  return null;
2072
2112
  }
2073
- try {
2074
- if (!snapshot.isComplete) return null;
2075
- if (input.options.deadCode && !await copyUnchangedBaselineSources({
2076
- directory: input.directory,
2077
- sourceFiles: maintainabilitySourceFiles,
2078
- baseMaterializedFiles: snapshot.materializedFiles,
2079
- headChangedFiles: snapshot.headFiles,
2080
- untrackedFiles: snapshot.untrackedFiles,
2081
- tempDirectory: snapshot.tempDirectory,
2082
- deadlineEpochMs: input.deadlineEpochMs,
2083
- signal: input.oxlintRuntime.abortSignal
2084
- })) return null;
2085
- const filteredSnapshotBaseFiles = filterPathsOutsideDirectories({
2086
- rootDirectory: input.directory,
2087
- relativePaths: snapshot.baseFiles,
2088
- excludedDirectories: input.options.excludedProjectDirectories
2089
- });
2090
- const filteredSnapshotHeadFiles = filterPathsOutsideDirectories({
2091
- rootDirectory: input.directory,
2092
- relativePaths: snapshot.headFiles,
2093
- excludedDirectories: input.options.excludedProjectDirectories
2094
- });
2095
- const analyzedHeadFiles = new Set(input.headAnalyzedFiles.map(toForwardSlashes));
2096
- const baseFiles = new Set((baselineBaseFiles ?? filteredSnapshotBaseFiles).map(toForwardSlashes));
2097
- const expectedHeadFiles = new Set((baselineHeadFiles ?? filteredSnapshotHeadFiles).map(toForwardSlashes));
2098
- for (const filePath of baselineIncludePaths) {
2099
- const normalizedFilePath = toForwardSlashes(filePath);
2100
- if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
2101
- }
2102
- if (input.options.lint && filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
2103
- const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
2104
- const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
2105
- const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
2106
- const baselineExcludedProjectDirectories = input.options.excludedProjectDirectories.map((excludedDirectory) => path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.directory)).map((excludedDirectory) => path.resolve(snapshot.tempDirectory, path.relative(input.directory, excludedDirectory)));
2107
- const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
2108
- const runtimeLayers = buildRuntimeLayers({
2109
- directory: snapshot.tempDirectory,
2110
- hasConfigOverride: true,
2111
- userConfig: input.userConfig,
2112
- configSourceDirectory: input.configSourceDirectory,
2113
- projectInfoOverride: input.headProjectInfo,
2114
- shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
2115
- shouldRunDeadCode: input.options.deadCode,
2116
- shouldRunSupplyChain: input.options.supplyChain,
2117
- shouldComputeScore: false,
2118
- shouldShowProgressSpinners: false,
2119
- oxlintConcurrency: input.oxlintRuntime.concurrency,
2120
- oxlintSpawnSlots: input.oxlintRuntime.spawnSlots
2121
- });
2122
- const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
2123
- directory: snapshot.tempDirectory,
2124
- includePaths: baseIncludePaths,
2125
- maintainabilityFocusPaths,
2113
+ };
2114
+ const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
2115
+ const fingerprints = [];
2116
+ if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
2117
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
2118
+ const packageJson = bundledRequire(specifier);
2119
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
2120
+ fingerprints.push(`${specifier}=${version}`);
2121
+ } catch {
2122
+ fingerprints.push(`${specifier}=missing`);
2123
+ }
2124
+ fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
2125
+ return fingerprints;
2126
+ };
2127
+ const buildScanResultCacheKey = async (input) => {
2128
+ if (isCacheGloballyDisabled()) return null;
2129
+ const isTrustworthyPromise = isGitIdentityTrustworthy(input.projectDirectory);
2130
+ const repositoryIdentityPromise = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState, runGitAsync(input.projectDirectory, REPOSITORY_ROOT_GIT_ARGUMENTS));
2131
+ const [isTrustworthy, repositoryIdentity] = await Promise.all([isTrustworthyPromise, repositoryIdentityPromise]);
2132
+ if (!isTrustworthy || repositoryIdentity === null) return null;
2133
+ if (stringifyStableJson(input.userConfig) === null) return null;
2134
+ const cacheKeyJson = stringifyStableJson({
2135
+ schemaVersion: 7,
2136
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
2137
+ headSha: repositoryIdentity.headSha,
2138
+ worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
2139
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
2140
+ reactDoctorVersion: input.version,
2141
+ nodeVersion: process.version,
2142
+ toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
2143
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
2144
+ hasConfigOverride: input.hasConfigOverride,
2145
+ configSourceDirectory: input.configSourceDirectory,
2146
+ userConfig: input.userConfig,
2147
+ engineOptions: {
2148
+ lint: input.options.lint,
2149
+ deadCode: input.options.deadCode,
2150
+ supplyChain: input.options.supplyChain,
2151
+ includePaths: [...input.options.includePaths].sort(),
2152
+ excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
2153
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
2126
2154
  customRulesOnly: input.options.customRulesOnly,
2127
2155
  respectInlineDisables: input.options.respectInlineDisables,
2128
2156
  warnings: input.options.warnings,
2129
2157
  adoptExistingLintConfig: input.options.adoptExistingLintConfig,
2130
- ignoredTags: input.options.ignoredTags,
2131
- includedTags: input.options.includedTags,
2158
+ ignoredTags: [...input.options.ignoredTags].sort(),
2159
+ includedTags: [...input.options.includedTags].sort(),
2132
2160
  includeTagDefaults: input.options.includeTagDefaults,
2133
- nodeBinaryPath: input.resolvedNodeBinaryPath ?? void 0,
2134
- runDeadCode: input.options.deadCode,
2161
+ concurrency: input.options.concurrency,
2162
+ lintBatchOrdering: resolveLintBatchOrdering(),
2163
+ baselineRef: input.options.baseline?.ref,
2164
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
2165
+ noScore: input.options.noScore,
2135
2166
  isCi: input.options.isCi,
2136
- doctorVersion: VERSION,
2137
- runId: getRunId(),
2138
- resolveLocalGithubViewerPermission: false,
2139
- suppressScanSummary: true,
2140
- supplyChainManifestChanged: input.options.supplyChainManifestChanged,
2141
- deadlineEpochMs: input.deadlineEpochMs ?? void 0,
2142
- signal: input.oxlintRuntime.abortSignal,
2143
- excludedProjectDirectories: baselineExcludedProjectDirectories,
2144
- retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
2145
- }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
2146
- if (baseOutput.didLintFail || baseOutput.didDeadCodeFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
2147
- const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
2148
- rootDirectory: input.directory,
2149
- relativePaths: snapshot.untrackedFiles,
2150
- excludedDirectories: input.options.excludedProjectDirectories
2151
- }).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
2152
- const diagnosticDelta = computeDiagnosticDelta({
2153
- headDiagnostics: input.headDiagnostics,
2154
- baseDiagnostics: baseOutput.diagnostics,
2155
- readHeadLine: createSourceLineReader(input.directory),
2156
- readBaseLine: createSourceLineReader(snapshot.tempDirectory),
2157
- readHeadEvidence: createDiagnosticEvidenceReader(input.directory, { resolveForwardedHandlers: true }),
2158
- readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
2167
+ suppressRendering: input.options.suppressRendering,
2168
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
2169
+ }
2170
+ });
2171
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
2172
+ };
2173
+ const createScanResultCache = (projectDirectory) => {
2174
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
2175
+ const persistedCache = readPersistedCache(cacheFilePath);
2176
+ const entries = /* @__PURE__ */ new Map();
2177
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
2178
+ const persist = () => {
2179
+ const mergedEntries = /* @__PURE__ */ new Map();
2180
+ for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
2181
+ for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
2182
+ atomicWriteJson(cacheFilePath, {
2183
+ version: 7,
2184
+ entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
2159
2185
  });
2160
- return {
2161
- displayDiagnostics: diagnosticDelta.newDiagnostics,
2162
- baselineDelta: {
2163
- baseRef: input.baselineRef,
2164
- fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : diagnosticDelta.fixedCount,
2165
- baseTotalCount: baseOutput.diagnostics.length,
2166
- crossFileMatchCount: diagnosticDelta.crossFileMatchCount
2167
- }
2168
- };
2169
- } finally {
2170
- snapshot.cleanup();
2171
- }
2186
+ };
2187
+ return {
2188
+ lookup: (key) => {
2189
+ const payload = entries.get(key)?.payload;
2190
+ if (payload === void 0) return null;
2191
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
2192
+ },
2193
+ store: (key, payload) => {
2194
+ entries.set(key, {
2195
+ key,
2196
+ createdAtMs: Date.now(),
2197
+ payload: {
2198
+ ...payload,
2199
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
2200
+ }
2201
+ });
2202
+ persist();
2203
+ }
2204
+ };
2172
2205
  };
2173
2206
  //#endregion
2174
2207
  //#region src/cli/utils/scan-result-cache-lifecycle.ts
2175
- const createScanResultCacheLifecycle = (input) => {
2176
- const cacheKey = buildScanResultCacheKey({
2208
+ const createScanResultCacheLifecycle = async (input) => {
2209
+ const cacheKey = await buildScanResultCacheKey({
2177
2210
  projectDirectory: input.directory,
2178
2211
  version: VERSION,
2179
2212
  nodeBinaryPath: input.resolvedNodeBinaryPath,
@@ -2240,7 +2273,10 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
2240
2273
  userConfig = scanTarget.userConfig;
2241
2274
  configSourceDirectory = scanTarget.configSourceDirectory;
2242
2275
  }
2243
- const options = resolveInspectOptions(inputOptions, userConfig);
2276
+ const options = resolveInspectOptions(path$1.resolve(directory) === path$1.resolve(scanDirectory) ? inputOptions : {
2277
+ ...inputOptions,
2278
+ precomputedSourceFiles: void 0
2279
+ }, userConfig);
2244
2280
  const ownsSpinnerSilence = options.silent && !isConcurrentScan;
2245
2281
  const wasSpinnerSilent = isSpinnerSilent();
2246
2282
  if (ownsSpinnerSilence) setSpinnerSilent(true);
@@ -2264,9 +2300,10 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
2264
2300
  }
2265
2301
  };
2266
2302
  const createInvocationInspect = (requestedOxlintConcurrency) => {
2267
- const concurrency = resolveScanConcurrency(requestedOxlintConcurrency ?? runSync(OxlintConcurrency));
2303
+ const concurrency = resolveInvocationOxlintConcurrency(requestedOxlintConcurrency);
2268
2304
  const spawnSlots = createOxlintSpawnSlots(concurrency);
2269
2305
  const scanResultCacheInvocationState = createScanResultCacheInvocationState();
2306
+ const invocationCaches = createInvocationCaches();
2270
2307
  return async (directory, inputOptions = {}) => {
2271
2308
  const abortController = new AbortController();
2272
2309
  const unregisterAbortController = activeScanAbortRegistry.register(abortController);
@@ -2275,7 +2312,8 @@ const createInvocationInspect = (requestedOxlintConcurrency) => {
2275
2312
  concurrency,
2276
2313
  spawnSlots,
2277
2314
  abortSignal: abortController.signal,
2278
- scanResultCacheInvocationState
2315
+ scanResultCacheInvocationState,
2316
+ invocationCaches
2279
2317
  });
2280
2318
  } finally {
2281
2319
  unregisterAbortController();
@@ -2288,7 +2326,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2288
2326
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
2289
2327
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
2290
2328
  await yieldToEventLoop();
2291
- const scanResultCacheLifecycle = createScanResultCacheLifecycle({
2329
+ const scanResultCacheLifecycle = await createScanResultCacheLifecycle({
2292
2330
  directory,
2293
2331
  options,
2294
2332
  userConfig,
@@ -2301,6 +2339,11 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2301
2339
  });
2302
2340
  const cachedResult = scanResultCacheLifecycle.replay();
2303
2341
  if (cachedResult !== null) return cachedResult;
2342
+ if (options.lint && resolvedNodeBinaryPath) warmOxlintWorkerPool(resolvedNodeBinaryPath, oxlintRuntime.concurrency);
2343
+ if (options.deadCode && shouldUseMaintainabilityLayer({
2344
+ shouldRunDuplicateJsx: options.deadCode,
2345
+ userConfig
2346
+ })) warmDuplicateJsxWorker();
2304
2347
  const shouldShowProgressSpinners = !options.isCiOrCodingAgentEnvironment && !options.silent && !options.scoreOnly && !options.suppressRendering && options.lint && Boolean(resolvedNodeBinaryPath);
2305
2348
  const layers = buildRuntimeLayers({
2306
2349
  directory,
@@ -2314,12 +2357,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2314
2357
  shouldShowProgressSpinners,
2315
2358
  oxlintConcurrency: oxlintRuntime.concurrency,
2316
2359
  oxlintSpawnSlots: oxlintRuntime.spawnSlots,
2360
+ invocationCaches: oxlintRuntime.invocationCaches,
2317
2361
  reporterLayer: options.uiLayers?.reporter,
2318
2362
  progressLayer: options.uiLayers?.progress
2319
2363
  });
2320
2364
  const program = runInspect({
2321
2365
  directory,
2322
2366
  precomputedSourceFileCount: options.precomputedSourceFileCount,
2367
+ precomputedSourceFiles: options.precomputedSourceFiles,
2323
2368
  includePaths: options.includePaths,
2324
2369
  changedLineRanges: options.changedLineRanges ?? void 0,
2325
2370
  customRulesOnly: options.customRulesOnly,
@@ -2431,12 +2476,12 @@ const deduplicateProjectScans = (projectScans) => {
2431
2476
  return uniqueProjectScans;
2432
2477
  };
2433
2478
  //#endregion
2434
- //#region src/cli/utils/collect-project-source-file-counts.ts
2435
- const collectProjectSourceFileCounts = async (rootDirectory, projectDirectories) => {
2479
+ //#region src/cli/utils/collect-project-source-files.ts
2480
+ const collectProjectSourceFiles = async (rootDirectory, projectDirectories) => {
2436
2481
  const abortController = new AbortController();
2437
2482
  const unregisterAbortController = activeScanAbortRegistry.register(abortController);
2438
2483
  try {
2439
- return await collectSourceFileCountsByDirectory(rootDirectory, projectDirectories, abortController.signal);
2484
+ return await collectSourceFilesByDirectory(rootDirectory, projectDirectories, abortController.signal);
2440
2485
  } finally {
2441
2486
  unregisterAbortController();
2442
2487
  }
@@ -2965,6 +3010,17 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
2965
3010
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
2966
3011
  };
2967
3012
  //#endregion
3013
+ //#region src/cli/utils/resolve-excluded-project-directories.ts
3014
+ const resolveExcludedProjectDirectories = (scanDirectory, projectDirectories) => {
3015
+ const excludedDirectories = /* @__PURE__ */ new Map();
3016
+ for (const projectDirectory of projectDirectories) {
3017
+ if (!isPathInsideDirectory(projectDirectory, scanDirectory)) continue;
3018
+ const resolvedDirectory = path$1.resolve(projectDirectory);
3019
+ if (!excludedDirectories.has(resolvedDirectory)) excludedDirectories.set(resolvedDirectory, projectDirectory);
3020
+ }
3021
+ return [...excludedDirectories.values()];
3022
+ };
3023
+ //#endregion
2968
3024
  //#region src/cli/utils/validate-mode-flags.ts
2969
3025
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
2970
3026
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
@@ -3002,7 +3058,7 @@ const shouldFailScanGate = (input) => {
3002
3058
  return shouldBlockCi(filterScansForSurface(input.scans, "ciFailure"), input.blockingLevel);
3003
3059
  };
3004
3060
  //#endregion
3005
- export { inspect as A, filterDiagnosticsByCategories as B, CLI_AGENT_BINARIES as C, collectProjectSourceFileCounts as D, buildHandoffPayload as E, formatElapsedTime as F, buildSortedRuleGroups as G, buildEmptyReportMessage as H, printFooter as I, formatFixRecipeLine as K, printDiagnosticsDump as L, printHeadlessReport as M, pluralize as N, deduplicateProjectScans as O, formatSkippedCheckLabel as P, formatDiagnosticSite as R, detectLaunchableAgents as S, launchCliAgent as T, hasLintHardFailure as U, buildNoScoreMessage as V, buildRulePriorityMap as W, resolveWorkspaceDeadCodeOwner as _, validateModeFlags as a, countUniqueScannedFiles as b, discoverWorkspacePackages as c, resolveProjectScan as d, resolveProjectChangedLineRanges as f, retryMissingProjectScores as g, resolveProjectRelativeDirectory as h, validateIncludeUntrackedScope as i, resolveMergeBaseRef as j, createInvocationInspect as k, selectProjects as l, resolveProjectSourceFilePaths as m, formatSkippedProjectsMessage as n, projectManifestChanged as o, resolveProjectDiffIncludePaths as p, formatLearnMoreLine as q, validateFilePathSelectionFlags as r, partitionProjectScanOutcomes as s, shouldFailScanGate as t, buildCodeFrame as u, resolveCliInspectOptions as v, copyToClipboard as w, setUpGitHubActions as x, filterScansForSurface as y, hasIncompleteScoreAnalysis as z };
3061
+ export { createInvocationInspect as A, hasIncompleteScoreAnalysis as B, detectLaunchableAgents as C, buildHandoffPayload as D, launchCliAgent as E, formatSkippedCheckLabel as F, hasLintHardFailure as G, buildNoScoreMessage as H, formatElapsedTime as I, formatFixRecipeLine as J, buildRulePriorityMap as K, printFooter as L, resolveMergeBaseRef as M, printHeadlessReport as N, collectProjectSourceFiles as O, pluralize as P, printDiagnosticsDump as R, setUpGitHubActions as S, copyToClipboard as T, buildEmptyReportMessage as U, filterDiagnosticsByCategories as V, resolveInvocationOxlintConcurrency as W, formatLearnMoreLine as Y, retryMissingProjectScores as _, validateModeFlags as a, filterScansForSurface as b, partitionProjectScanOutcomes as c, buildCodeFrame as d, resolveProjectScan as f, resolveProjectRelativeDirectory as g, resolveProjectSourceFilePaths as h, validateIncludeUntrackedScope as i, inspect as j, deduplicateProjectScans as k, discoverWorkspacePackages as l, resolveProjectDiffIncludePaths as m, formatSkippedProjectsMessage as n, resolveExcludedProjectDirectories as o, resolveProjectChangedLineRanges as p, buildSortedRuleGroups as q, validateFilePathSelectionFlags as r, projectManifestChanged as s, shouldFailScanGate as t, selectProjects as u, resolveWorkspaceDeadCodeOwner as v, CLI_AGENT_BINARIES as w, countUniqueScannedFiles as x, resolveCliInspectOptions as y, formatDiagnosticSite as z };
3006
3062
 
3007
- //# sourceMappingURL=should-fail-scan-gate-BYPsKIde.js.map
3008
- //# debugId=40d3b0cf-26ca-5238-9651-4a1de64ef1f0
3063
+ //# sourceMappingURL=should-fail-scan-gate-YK9VAFNu.js.map
3064
+ //# debugId=18783c0f-3bd5-5391-90cc-0fe9d78014f3