react-doctor 0.9.14 → 0.9.15-dev.6313667
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/_react-doctor_require-typescript-DXQDYeWK.js +16 -0
- package/dist/{active-run-trace-dlIZeEvb.js → active-run-trace-Dewycxda.js} +3 -3
- package/dist/{active-scan-abort-registry-B72ASBUh.js → active-scan-abort-registry-CCTgal0O.js} +3 -3
- package/dist/{build-section-divider-8DpL5BjN.js → build-section-divider-FMwsfCjR.js} +5 -4
- package/dist/{ci-DH_jkNS4.js → ci-DpEdS9xF.js} +14 -13
- package/dist/cli.js +32 -29
- package/dist/constants-DJ3RtEMu.js +1035 -0
- package/dist/{constants-CKOk_Fa9.js → constants-DNh19qg9.js} +3 -3
- package/dist/{design-DR72z3tB.js → design-BAnNluAv.js} +4 -4
- package/dist/{devtools-DOdrAPkZ.js → devtools-D20sefkm.js} +3 -3
- package/dist/duplicate-jsx-worker.js +62 -62
- package/dist/{exit-gracefully-BP0lJPao.js → exit-gracefully-BOuD5SPS.js} +7 -7
- package/dist/fs-utils-C88IbqFa.js +45 -0
- package/dist/{get-east-asian-width-CM5BOEC-.js → get-east-asian-width-DRPy7dqz.js} +4 -4
- package/dist/{handle-error-BO7AUOeO.js → handle-error-B_f3lyYO.js} +6 -5
- package/dist/index.d.ts +8511 -8509
- package/dist/index.js +12734 -12338
- package/dist/{inspect-CtwjUwiF.js → inspect-BOJOQFH5.js} +28 -21
- package/dist/{install-DAwHDunN.js → install-DXOthHDF.js} +10 -9
- package/dist/{install-doctor-script-DousNjwa.js → install-doctor-script-DDk3Zz0c.js} +17 -8
- package/dist/{install-github-workflow-DNfyCbSd.js → install-github-workflow-DcD8c71b.js} +10 -10
- package/dist/{install-react-doctor-CofgPrzS.js → install-react-doctor-BF79Vawz.js} +15 -11
- package/dist/is-cache-globally-disabled-De19063F.js +10 -0
- package/dist/{jsx-runtime-Dv_yWxfg.js → jsx-runtime-BfhcIiPm.js} +11 -10
- package/dist/oxlint-worker.js +95 -5
- package/dist/{path-format-DnHpmJwG.js → path-format-By20mo8A.js} +3 -3
- package/dist/{print-branded-header-CFoO5ZHN.js → print-branded-header-BFOYMfiy.js} +4 -4
- package/dist/project-analysis-worker.js +459 -461
- package/dist/{prompt-runtime-scan-url-9tmVyRA2.js → prompt-runtime-scan-url-DutYdTq7.js} +6 -6
- package/dist/react-compiler-detection-worker.d.ts +1 -0
- package/dist/react-compiler-detection-worker.js +1029 -0
- package/dist/{read-object-file-C8gGTrwo.js → read-object-file-Cejq_ul8.js} +4 -4
- package/dist/{record-metric-DMuxA69u.js → record-metric-DS3AjkQ3.js} +12589 -13129
- package/dist/{report-error-DynYEdrW.js → report-error-DwDD47K-.js} +7 -6
- package/dist/{report-workflow-result-C0nwk5Eq.js → report-workflow-result-UemdAwLM.js} +24 -11
- package/dist/{resolve-scope-CtDh9FtN.js → resolve-scope-CfZzkot8.js} +7 -7
- package/dist/{rules-Dtp6I51W.js → rules-RjqWk309.js} +8 -7
- package/dist/{run-scan-app-DTjrGFyP.js → run-scan-app-ChwoL1RF.js} +12 -11
- package/dist/{runtime-scan-DylzMW6d.js → runtime-scan-C-qXmEpn.js} +13 -12
- package/dist/{sanitize-runtime-url-CtosBgav.js → sanitize-runtime-url-DcLyAxpE.js} +4 -4
- package/dist/{scan-i9quJrxG.js → scan-BGKVuwQa.js} +10 -9
- package/dist/{semver-DlmXks31.js → semver-BZIuy1gP.js} +3 -3
- package/dist/{should-fail-scan-gate-uW8_hnTc.js → should-fail-scan-gate-BC9hJSI-.js} +1848 -1825
- package/dist/{should-skip-prompts-fmi7PPp5.js → should-skip-prompts-CxY5MlEp.js} +4 -4
- package/dist/{should-use-tui-DaLPPhMi.js → should-use-tui-Cgsu0Mow.js} +4 -4
- package/dist/start-scan-preamble-DVhXIPc0.d.ts +1 -0
- package/dist/start-scan-preamble-DZknzhHU.js +68 -0
- package/dist/start-scan-preamble.d.ts +1 -0
- package/dist/start-scan-preamble.js +4 -0
- package/dist/{telemetry-runtime-CxBC0gEj.js → telemetry-runtime-rWUZdMHt.js} +7 -6
- package/dist/{unref-stdin-SK3WfwxS.js → unref-stdin-D-NZBFpg.js} +3 -3
- package/dist/unwrap-typescript-expression-CrEPQUWz.js +14 -0
- package/dist/{version-Bti3M8BN.js → version-AmrbbM3P.js} +5 -4
- package/dist/{why-dcQaGoJz.js → why-0cWz29hR.js} +4 -4
- package/dist/{with-run-span-Lynvloyt.js → with-run-span-CJp_LacB.js} +7 -6
- package/package.json +7 -5
- package/dist/_react-doctor_require-typescript-DnwIzXJn.js +0 -7
- package/dist/unwrap-typescript-expression-CbO_6kmC.js +0 -14
|
@@ -1,25 +1,27 @@
|
|
|
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]="
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
7
|
-
import { t as
|
|
8
|
-
import { t as
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { a as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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]="24fb46c9-b471-5762-a502-488724c08e84")}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-DJ3RtEMu.js";
|
|
4
|
+
import { t as isCacheGloballyDisabled } from "./is-cache-globally-disabled-De19063F.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-DS3AjkQ3.js";
|
|
6
|
+
import { t as getTelemetryContext } from "./telemetry-runtime-rWUZdMHt.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-FMwsfCjR.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-DDk3Zz0c.js";
|
|
12
|
+
import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-DcD8c71b.js";
|
|
13
|
+
import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-CfZzkot8.js";
|
|
14
|
+
import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-CJp_LacB.js";
|
|
15
|
+
import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-UemdAwLM.js";
|
|
14
16
|
import { createRequire } from "node:module";
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
|
-
import * as path$1 from "node:path";
|
|
17
|
-
import path from "node:path";
|
|
18
17
|
import * as fs$2 from "node:fs";
|
|
19
18
|
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
19
|
+
import * as path$1 from "node:path";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
20
23
|
import crypto, { randomUUID } from "node:crypto";
|
|
21
24
|
import * as fs$1 from "node:fs/promises";
|
|
22
|
-
import { tmpdir } from "node:os";
|
|
23
25
|
import { performance } from "node:perf_hooks";
|
|
24
26
|
//#region src/cli/utils/apply-observability.ts
|
|
25
27
|
/**
|
|
@@ -269,1936 +271,1942 @@ const resolveScoreUnavailableReason = (input) => {
|
|
|
269
271
|
return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
|
|
270
272
|
};
|
|
271
273
|
//#endregion
|
|
272
|
-
//#region src/cli/utils/
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const isNumber = (value) => typeof value === "number";
|
|
278
|
-
const isString = (value) => typeof value === "string";
|
|
279
|
-
const isDiagnosticArray = (value) => {
|
|
280
|
-
if (!Array.isArray(value)) return false;
|
|
281
|
-
try {
|
|
282
|
-
for (const entry of value) decodeDiagnostic(entry);
|
|
283
|
-
return true;
|
|
284
|
-
} catch {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
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");
|
|
287
279
|
};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
key: "lintFailureReason",
|
|
315
|
-
isValid: isNullableString
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
key: "lintPartialFailures",
|
|
319
|
-
isValid: isStringArray
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
key: "didDeadCodeFail",
|
|
323
|
-
isValid: isBoolean
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
key: "deadCodeFailureReason",
|
|
327
|
-
isValid: isNullableString
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
key: "deadCodeOverlapped",
|
|
331
|
-
isValid: isBoolean
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
key: "directory",
|
|
335
|
-
isValid: isString
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
key: "scannedFileCount",
|
|
339
|
-
isValid: isNumber
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
key: "scannedFilePaths",
|
|
343
|
-
isValid: isStringArray
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
key: "scanElapsedMilliseconds",
|
|
347
|
-
isValid: isNumber
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
key: "lintFailureReasonKind",
|
|
351
|
-
isValid: isNullableString
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
key: "supplyChainOverlapTimedOut",
|
|
355
|
-
isValid: isBoolean
|
|
356
|
-
}
|
|
357
|
-
];
|
|
358
|
-
const OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS = [
|
|
359
|
-
{
|
|
360
|
-
key: "analyzedFiles",
|
|
361
|
-
isValid: isStringArray
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
key: "baselineDelta",
|
|
365
|
-
isValid: isBaselineDelta
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
key: "scanConcurrency",
|
|
369
|
-
isValid: isNumber
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
key: "securityScanFailed",
|
|
373
|
-
isValid: isBoolean
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
key: "securityScanFailureReason",
|
|
377
|
-
isValid: isNullableString
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
key: "suppressedRuleCounts",
|
|
381
|
-
isValid: isSuppressedRuleCountArray
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
key: "manifestContentHash",
|
|
385
|
-
isValid: isNullableString
|
|
386
|
-
}
|
|
387
|
-
];
|
|
388
|
-
const hasValidRequiredCachedScanPayloadFields = (value) => REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => field.isValid(value[field.key]));
|
|
389
|
-
const hasValidOptionalCachedScanPayloadFields = (value) => OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => value[field.key] === void 0 || field.isValid(value[field.key]));
|
|
390
|
-
const isCachedScanPayload = (value) => {
|
|
391
|
-
if (!isRecord(value)) return false;
|
|
392
|
-
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;
|
|
393
306
|
};
|
|
394
|
-
const decodeCachedScanPayload = (value) => isCachedScanPayload(value) ? value : null;
|
|
395
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
|
|
396
307
|
//#endregion
|
|
397
|
-
//#region src/cli/utils/
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
422
342
|
});
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
for (const
|
|
428
|
-
|
|
429
|
-
|
|
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;
|
|
430
351
|
}
|
|
431
|
-
|
|
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
|
+
});
|
|
432
359
|
};
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
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";
|
|
439
365
|
};
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
456
|
-
tokenIndex += 1;
|
|
457
|
-
const originPath = tokens[tokenIndex];
|
|
458
|
-
if (originPath === void 0 || originPath.length === 0) return null;
|
|
459
|
-
entries.push({
|
|
460
|
-
statusCode,
|
|
461
|
-
path: recordPath,
|
|
462
|
-
originPath
|
|
463
|
-
});
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
entries.push({
|
|
467
|
-
statusCode,
|
|
468
|
-
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
|
|
469
381
|
});
|
|
470
382
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
479
400
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
401
|
+
let topRule = null;
|
|
402
|
+
let topRuleCount = 0;
|
|
403
|
+
for (const [rule, count] of countByRule) if (count > topRuleCount) {
|
|
404
|
+
topRule = rule;
|
|
405
|
+
topRuleCount = count;
|
|
484
406
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (contentFingerprint === null) return null;
|
|
505
|
-
dirtyEntries.push({
|
|
506
|
-
...statusEntry,
|
|
507
|
-
contentFingerprint
|
|
508
|
-
});
|
|
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);
|
|
509
426
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
|
|
531
|
-
const resolveDotenvFingerprint = (projectDirectory) => {
|
|
532
|
-
try {
|
|
533
|
-
return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
|
|
534
|
-
} catch {
|
|
535
|
-
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"];
|
|
536
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;
|
|
537
518
|
};
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
});
|
|
543
528
|
};
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
} catch {
|
|
572
|
-
return {
|
|
573
|
-
version: 7,
|
|
574
|
-
entries: []
|
|
575
|
-
};
|
|
576
|
-
}
|
|
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
|
+
});
|
|
577
556
|
};
|
|
578
557
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
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.
|
|
586
571
|
*/
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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;
|
|
597
585
|
try {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
} catch {
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
|
|
605
|
-
const fingerprints = [];
|
|
606
|
-
if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
|
|
607
|
-
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
608
|
-
const packageJson = bundledRequire(specifier);
|
|
609
|
-
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
610
|
-
fingerprints.push(`${specifier}=${version}`);
|
|
611
|
-
} catch {
|
|
612
|
-
fingerprints.push(`${specifier}=missing`);
|
|
613
|
-
}
|
|
614
|
-
fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
|
|
615
|
-
return fingerprints;
|
|
616
|
-
};
|
|
617
|
-
const buildScanResultCacheKey = (input) => {
|
|
618
|
-
if (isCacheGloballyDisabled()) return null;
|
|
619
|
-
if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
|
|
620
|
-
const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
|
|
621
|
-
if (repositoryIdentity === null) return null;
|
|
622
|
-
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
623
|
-
const cacheKeyJson = stringifyStableJson({
|
|
624
|
-
schemaVersion: 7,
|
|
625
|
-
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
626
|
-
headSha: repositoryIdentity.headSha,
|
|
627
|
-
worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
|
|
628
|
-
dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
|
|
629
|
-
reactDoctorVersion: input.version,
|
|
630
|
-
nodeVersion: process.version,
|
|
631
|
-
toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
|
|
632
|
-
configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
|
|
633
|
-
hasConfigOverride: input.hasConfigOverride,
|
|
634
|
-
configSourceDirectory: input.configSourceDirectory,
|
|
635
|
-
userConfig: input.userConfig,
|
|
636
|
-
engineOptions: {
|
|
637
|
-
lint: input.options.lint,
|
|
638
|
-
deadCode: input.options.deadCode,
|
|
639
|
-
supplyChain: input.options.supplyChain,
|
|
640
|
-
includePaths: [...input.options.includePaths].sort(),
|
|
641
|
-
excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
|
|
642
|
-
retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
|
|
643
|
-
customRulesOnly: input.options.customRulesOnly,
|
|
644
|
-
respectInlineDisables: input.options.respectInlineDisables,
|
|
645
|
-
warnings: input.options.warnings,
|
|
646
|
-
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
647
|
-
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
648
|
-
includedTags: [...input.options.includedTags].sort(),
|
|
649
|
-
includeTagDefaults: input.options.includeTagDefaults,
|
|
650
|
-
concurrency: input.options.concurrency,
|
|
651
|
-
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
652
|
-
baselineRef: input.options.baseline?.ref,
|
|
653
|
-
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
654
|
-
noScore: input.options.noScore,
|
|
655
|
-
isCi: input.options.isCi,
|
|
656
|
-
suppressRendering: input.options.suppressRendering,
|
|
657
|
-
supplyChainManifestChanged: input.options.supplyChainManifestChanged
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
return cacheKeyJson === null ? null : hashString(cacheKeyJson);
|
|
661
|
-
};
|
|
662
|
-
const createScanResultCache = (projectDirectory) => {
|
|
663
|
-
const cacheFilePath = resolveCacheFilePath(projectDirectory);
|
|
664
|
-
const persistedCache = readPersistedCache(cacheFilePath);
|
|
665
|
-
const entries = /* @__PURE__ */ new Map();
|
|
666
|
-
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
667
|
-
const persist = () => {
|
|
668
|
-
const mergedEntries = /* @__PURE__ */ new Map();
|
|
669
|
-
for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
|
|
670
|
-
for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
|
|
671
|
-
atomicWriteJson(cacheFilePath, {
|
|
672
|
-
version: 7,
|
|
673
|
-
entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
674
|
-
});
|
|
675
|
-
};
|
|
676
|
-
return {
|
|
677
|
-
lookup: (key) => {
|
|
678
|
-
const payload = entries.get(key)?.payload;
|
|
679
|
-
if (payload === void 0) return null;
|
|
680
|
-
return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
|
|
681
|
-
},
|
|
682
|
-
store: (key, payload) => {
|
|
683
|
-
entries.set(key, {
|
|
684
|
-
key,
|
|
685
|
-
createdAtMs: Date.now(),
|
|
686
|
-
payload: {
|
|
687
|
-
...payload,
|
|
688
|
-
manifestContentHash: resolveManifestContentHash(projectDirectory)
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
persist();
|
|
692
|
-
}
|
|
693
|
-
};
|
|
586
|
+
for (const [key, value] of Object.entries(buildRunEventAttributes(input))) rootSpan.attribute(key, value);
|
|
587
|
+
} catch {}
|
|
694
588
|
};
|
|
695
589
|
//#endregion
|
|
696
|
-
//#region src/cli/utils/
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
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);
|
|
701
594
|
};
|
|
702
595
|
//#endregion
|
|
703
|
-
//#region src/cli/utils/
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Prefixes every key in a flat attribute group with `namespace.`, so a logical
|
|
714
|
-
* group (scan config, diagnostics rollup, score, …) carries one dotted
|
|
715
|
-
* namespace applied in a single place rather than hand-spelled into each key —
|
|
716
|
-
* which is how the wide event drifted into a flat, half-namespaced soup. The
|
|
717
|
-
* dotted prefix is what makes the attributes tree up in Sentry's attribute
|
|
718
|
-
* browser and stay group-/filter-/aggregate-able in the Spans dataset.
|
|
719
|
-
*
|
|
720
|
-
* Value types are preserved verbatim (numbers stay numbers so Sentry infers a
|
|
721
|
-
* numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
|
|
722
|
-
* so `toSpanAttributes` can drop absent signals rather than coerce them).
|
|
723
|
-
*/
|
|
724
|
-
const withNamespace = (namespace, attributes) => {
|
|
725
|
-
const namespaced = {};
|
|
726
|
-
for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
|
|
727
|
-
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
|
+
});
|
|
728
605
|
};
|
|
729
606
|
//#endregion
|
|
730
|
-
//#region src/cli/utils/
|
|
731
|
-
const readEnvBoolean = (name) => {
|
|
732
|
-
const value = process.env[name];
|
|
733
|
-
if (value === void 0) return null;
|
|
734
|
-
return value.toLowerCase() === "true" || value === "1";
|
|
735
|
-
};
|
|
736
|
-
const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
|
|
607
|
+
//#region src/cli/utils/resolve-invocation-oxlint-concurrency.ts
|
|
737
608
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* branch; no scan work ran).
|
|
743
|
-
* - `"warm"` — any incremental reuse: per-file lint hits, sidecar
|
|
744
|
-
* replays.
|
|
745
|
-
* - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
|
|
746
|
-
* off-switch is on. Granular per-cache opt-outs
|
|
747
|
-
* (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
|
|
748
|
-
* since the other subsystems stay live.
|
|
749
|
-
* - `"cold"` — caches on, zero reuse (first scan / everything changed).
|
|
750
|
-
*
|
|
751
|
-
* `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
|
|
752
|
-
* the subsystem reuse fractions known this run (lint hit ratio, sidecar
|
|
753
|
-
* replay ratio), skipping subsystems that never consulted a
|
|
754
|
-
* cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
|
|
755
|
-
* unweighted — the per-subsystem dims stay the precise signal; warmth is the
|
|
756
|
-
* 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.
|
|
757
613
|
*/
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
knownSubsystemCount += 1;
|
|
772
|
-
reuseRatioSum += reuseRatio;
|
|
773
|
-
}
|
|
774
|
-
const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
|
|
775
|
-
const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
|
|
776
|
-
return withNamespace("cache", {
|
|
777
|
-
wholeRepoHit: input.wholeRepoCacheHit,
|
|
778
|
-
temperature,
|
|
779
|
-
warmth
|
|
614
|
+
const resolveInvocationOxlintConcurrency = (requestedConcurrency) => resolveScanConcurrency(requestedConcurrency ?? runSync(OxlintConcurrency));
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/cli/utils/resolve-oxlint-node.ts
|
|
617
|
+
const consoleWarn = (message) => warn(highlighter.warn(message));
|
|
618
|
+
const consoleBreak = log("");
|
|
619
|
+
const consoleDim = (message) => log(highlighter.gray(message));
|
|
620
|
+
const consoleSuccess = (message) => log(highlighter.success(message));
|
|
621
|
+
const promptShouldInstallNode = () => promise(async () => {
|
|
622
|
+
const { shouldInstallNode } = await prompts({
|
|
623
|
+
type: "confirm",
|
|
624
|
+
name: "shouldInstallNode",
|
|
625
|
+
message: `Install Node 24 via nvm to enable lint checks?`,
|
|
626
|
+
initial: true
|
|
780
627
|
});
|
|
628
|
+
return Boolean(shouldInstallNode);
|
|
629
|
+
});
|
|
630
|
+
const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => gen(function* () {
|
|
631
|
+
if (!isLintEnabled) return null;
|
|
632
|
+
const resolver = yield* NodeResolver;
|
|
633
|
+
const initial = yield* resolver.resolve();
|
|
634
|
+
if (initial !== null) {
|
|
635
|
+
if (!initial.isCurrentNode && !isQuiet) {
|
|
636
|
+
yield* consoleWarn(`Node ${process.version} is unsupported by oxlint. Using Node ${initial.version} from nvm.`);
|
|
637
|
+
yield* consoleBreak;
|
|
638
|
+
}
|
|
639
|
+
return initial.binaryPath;
|
|
640
|
+
}
|
|
641
|
+
if (isQuiet) return null;
|
|
642
|
+
yield* consoleWarn(`Node ${process.version} is not compatible with oxlint (requires ${OXLINT_NODE_REQUIREMENT}). Lint checks will be skipped.`);
|
|
643
|
+
const isNvmInstalled = yield* resolver.isNvmInstalled();
|
|
644
|
+
if (isNvmInstalled && process.stdin.isTTY) {
|
|
645
|
+
if (yield* promptShouldInstallNode()) {
|
|
646
|
+
yield* consoleBreak;
|
|
647
|
+
const fresh = (yield* resolver.installViaNvm()) ? yield* resolver.resolve() : null;
|
|
648
|
+
if (fresh) {
|
|
649
|
+
yield* consoleBreak;
|
|
650
|
+
yield* consoleSuccess(`Node ${fresh.version} installed. Using it for lint checks.`);
|
|
651
|
+
yield* consoleBreak;
|
|
652
|
+
return fresh.binaryPath;
|
|
653
|
+
}
|
|
654
|
+
yield* consoleBreak;
|
|
655
|
+
yield* consoleWarn("Failed to install Node via nvm. Skipping lint checks.");
|
|
656
|
+
yield* consoleBreak;
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
} else if (isNvmInstalled) yield* consoleDim(` Run: nvm install 24`);
|
|
660
|
+
else yield* consoleDim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
|
|
661
|
+
yield* consoleBreak;
|
|
662
|
+
return null;
|
|
663
|
+
});
|
|
664
|
+
const resolveOxlintNode = (isLintEnabled, isQuiet) => runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(provide(NodeResolver.layerNode)));
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/cli/utils/resolve-cli-categories.ts
|
|
667
|
+
const categoryByLowercase = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category) => [category.toLowerCase(), category]));
|
|
668
|
+
const knownCategoryList = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
|
|
669
|
+
const splitCategoryFlagValue = (value) => value.split(",").map((category) => category.trim()).filter((category) => category.length > 0);
|
|
670
|
+
const resolveCliCategories = (categoryFlag) => {
|
|
671
|
+
const rawCategoryValues = categoryFlag === void 0 ? [] : Array.isArray(categoryFlag) ? categoryFlag : [categoryFlag];
|
|
672
|
+
const resolvedCategories = [];
|
|
673
|
+
const seenCategories = /* @__PURE__ */ new Set();
|
|
674
|
+
for (const rawCategoryValue of rawCategoryValues) for (const categoryQuery of splitCategoryFlagValue(rawCategoryValue)) {
|
|
675
|
+
const matchedCategory = categoryByLowercase.get(categoryQuery.toLowerCase());
|
|
676
|
+
if (matchedCategory === void 0) throw new CliInputError(`Unknown category "${categoryQuery}". Expected one of: ${knownCategoryList}.`);
|
|
677
|
+
if (!seenCategories.has(matchedCategory)) {
|
|
678
|
+
seenCategories.add(matchedCategory);
|
|
679
|
+
resolvedCategories.push(matchedCategory);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (rawCategoryValues.length > 0 && resolvedCategories.length === 0) throw new CliInputError(`--category requires one of: ${knownCategoryList}.`);
|
|
683
|
+
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
781
684
|
};
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/cli/utils/resolve-inspect-options.ts
|
|
687
|
+
const resolveIgnoredTags = (userConfig, includedTags) => {
|
|
688
|
+
const ignoredTags = new Set(userConfig?.ignore?.tags ?? []);
|
|
689
|
+
for (const includedTag of includedTags) ignoredTags.delete(includedTag);
|
|
690
|
+
return ignoredTags;
|
|
787
691
|
};
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
return
|
|
692
|
+
const resolveInspectOptions = (inputOptions, userConfig) => {
|
|
693
|
+
const includedTags = inputOptions.includedTags ?? /* @__PURE__ */ new Set();
|
|
694
|
+
const hasIncludedTags = includedTags.size > 0;
|
|
695
|
+
return {
|
|
696
|
+
lint: inputOptions.lint ?? userConfig?.lint ?? true,
|
|
697
|
+
deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
|
|
698
|
+
supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
|
|
699
|
+
verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
|
|
700
|
+
outputDirectory: inputOptions.outputDirectory || null,
|
|
701
|
+
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
702
|
+
noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
|
|
703
|
+
isCi: inputOptions.isCi ?? false,
|
|
704
|
+
isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
|
|
705
|
+
isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
|
|
706
|
+
silent: inputOptions.silent ?? false,
|
|
707
|
+
includePaths: inputOptions.includePaths ?? [],
|
|
708
|
+
customRulesOnly: hasIncludedTags ? false : userConfig?.customRulesOnly ?? false,
|
|
709
|
+
share: userConfig?.share ?? true,
|
|
710
|
+
respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
|
|
711
|
+
warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
|
|
712
|
+
categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
|
|
713
|
+
adoptExistingLintConfig: hasIncludedTags ? false : userConfig?.adoptExistingLintConfig ?? true,
|
|
714
|
+
ignoredTags: resolveIgnoredTags(userConfig, includedTags),
|
|
715
|
+
includedTags,
|
|
716
|
+
includeTagDefaults: inputOptions.includeTagDefaults ?? false,
|
|
717
|
+
scoreDisabledMessage: inputOptions.scoreDisabledMessage,
|
|
718
|
+
outputSurface: inputOptions.outputSurface ?? "cli",
|
|
719
|
+
suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
|
|
720
|
+
uiLayers: inputOptions.uiLayers ?? null,
|
|
721
|
+
concurrentScan: inputOptions.concurrentScan ?? false,
|
|
722
|
+
concurrency: inputOptions.concurrency,
|
|
723
|
+
maxDurationMs: inputOptions.maxDurationMs ?? null,
|
|
724
|
+
baseline: inputOptions.baseline ?? null,
|
|
725
|
+
changedLineRanges: inputOptions.changedLineRanges ?? null,
|
|
726
|
+
supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
|
|
727
|
+
excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
|
|
728
|
+
retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
|
|
729
|
+
precomputedSourceFileCount: inputOptions.precomputedSourceFileCount,
|
|
730
|
+
precomputedSourceFiles: inputOptions.precomputedSourceFiles
|
|
731
|
+
};
|
|
792
732
|
};
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/cli/utils/sum-file-counts-matching.ts
|
|
735
|
+
const sumFileCountsMatching = (messages, countPattern) => messages.reduce((total, message) => {
|
|
736
|
+
const match = countPattern.exec(message);
|
|
737
|
+
return match ? total + Number(match[1]) : total;
|
|
738
|
+
}, 0);
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/cli/utils/count-deadline-skipped-files.ts
|
|
741
|
+
const DEADLINE_SKIPPED_MESSAGE_PATTERN = /^(\d+) file\(s\) skipped — max scan duration reached/;
|
|
742
|
+
const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DEADLINE_SKIPPED_MESSAGE_PATTERN);
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
745
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
746
|
+
const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/cli/utils/build-empty-report-message.ts
|
|
749
|
+
const buildEmptyReportMessage = (input) => {
|
|
750
|
+
const categoryFilters = [...input.categoryFilters];
|
|
751
|
+
if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
|
|
752
|
+
if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
|
|
753
|
+
return "No issues found!";
|
|
754
|
+
};
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/cli/utils/build-inspect-result.ts
|
|
757
|
+
const buildInspectResult = (input) => {
|
|
758
|
+
const { payload, cacheStats } = input;
|
|
759
|
+
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
760
|
+
didLintFail: payload.didLintFail,
|
|
761
|
+
lintFailureReason: payload.lintFailureReason,
|
|
762
|
+
lintPartialFailures: payload.lintPartialFailures,
|
|
763
|
+
didDeadCodeFail: payload.didDeadCodeFail,
|
|
764
|
+
deadCodeFailureReason: payload.deadCodeFailureReason,
|
|
765
|
+
supplyChainOverlapTimedOut: payload.supplyChainOverlapTimedOut,
|
|
766
|
+
securityScanFailed: payload.securityScanFailed ?? false,
|
|
767
|
+
securityScanFailureReason: payload.securityScanFailureReason ?? null
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
diagnostics: [...payload.diagnostics],
|
|
771
|
+
score: payload.score,
|
|
772
|
+
skippedChecks,
|
|
773
|
+
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
774
|
+
project: payload.project,
|
|
775
|
+
elapsedMilliseconds: input.elapsedMilliseconds,
|
|
776
|
+
scannedFileCount: payload.scannedFileCount,
|
|
777
|
+
scannedFilePaths: payload.scannedFilePaths,
|
|
778
|
+
analyzedFiles: payload.analyzedFiles ?? [],
|
|
779
|
+
scanElapsedMilliseconds: payload.scanElapsedMilliseconds,
|
|
780
|
+
...cacheStats.lintCacheTotalFileCount === null ? {} : {
|
|
781
|
+
lintCacheHitFileCount: cacheStats.lintCacheHitFileCount,
|
|
782
|
+
lintCacheTotalFileCount: cacheStats.lintCacheTotalFileCount
|
|
783
|
+
},
|
|
784
|
+
...cacheStats.lintSidecarTotalFileCount === null ? {} : {
|
|
785
|
+
lintSidecarReplayedFileCount: cacheStats.lintSidecarReplayedFileCount,
|
|
786
|
+
lintSidecarTotalFileCount: cacheStats.lintSidecarTotalFileCount
|
|
787
|
+
},
|
|
788
|
+
...cacheStats.deadCodeCacheHit === null ? {} : { deadCodeCacheHit: cacheStats.deadCodeCacheHit },
|
|
789
|
+
...cacheStats.deadCodeSummaryCacheHits === null || cacheStats.deadCodeSummaryCacheMisses === null ? {} : {
|
|
790
|
+
deadCodeSummaryCacheHits: cacheStats.deadCodeSummaryCacheHits,
|
|
791
|
+
deadCodeSummaryCacheMisses: cacheStats.deadCodeSummaryCacheMisses
|
|
792
|
+
},
|
|
793
|
+
...payload.baselineDelta ? { baselineDelta: payload.baselineDelta } : {}
|
|
794
|
+
};
|
|
795
|
+
};
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/cli/utils/build-no-score-message.ts
|
|
798
|
+
const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
|
|
799
|
+
const buildNoScoreMessage = (input) => {
|
|
800
|
+
const unavailableReason = resolveScoreUnavailableReason({
|
|
801
|
+
isScoreDisabled: input.isScoreDisabled,
|
|
802
|
+
isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
|
|
803
|
+
});
|
|
804
|
+
let reason;
|
|
805
|
+
switch (unavailableReason) {
|
|
806
|
+
case "disabled":
|
|
807
|
+
reason = input.disabledMessage ?? "Score disabled by --no-score.";
|
|
808
|
+
break;
|
|
809
|
+
case "analysis-incomplete":
|
|
810
|
+
reason = "Score not shown because lint or maintainability analysis could not complete.";
|
|
811
|
+
break;
|
|
812
|
+
case "api-unavailable":
|
|
813
|
+
reason = "Score unavailable (could not reach the score API).";
|
|
814
|
+
break;
|
|
822
815
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
816
|
+
return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
|
|
817
|
+
};
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
820
|
+
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/cli/utils/has-incomplete-score-analysis.ts
|
|
823
|
+
const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/cli/utils/format-diagnostic-site.ts
|
|
826
|
+
const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/cli/utils/format-rule-summary.ts
|
|
829
|
+
const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
|
|
830
|
+
const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
831
|
+
const firstDiagnostic = ruleDiagnostics[0];
|
|
832
|
+
if (firstDiagnostic === void 0) return "";
|
|
833
|
+
const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
|
|
834
|
+
const sections = [
|
|
835
|
+
`Rule: ${ruleKey}`,
|
|
836
|
+
`Severity: ${firstDiagnostic.severity}`,
|
|
837
|
+
`Category: ${firstDiagnostic.category}`,
|
|
838
|
+
`Count: ${ruleDiagnostics.length}`,
|
|
839
|
+
"",
|
|
840
|
+
distinctMessages.join("\n\n")
|
|
841
|
+
];
|
|
842
|
+
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
843
|
+
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
844
|
+
const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
|
|
845
|
+
if (fixRecipeLine) sections.push("", fixRecipeLine);
|
|
846
|
+
sections.push("", "Files:");
|
|
847
|
+
for (const diagnostic of ruleDiagnostics) {
|
|
848
|
+
sections.push(` ${formatDiagnosticSite(diagnostic)}${formatFileContextTag(diagnostic)}`);
|
|
849
|
+
if (diagnostic.suppressionHint) sections.push(` ${diagnostic.suppressionHint}`);
|
|
828
850
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
diagnostic.filePath,
|
|
841
|
-
diagnostic.line,
|
|
842
|
-
diagnostic.column,
|
|
843
|
-
diagnostic.plugin,
|
|
844
|
-
diagnostic.rule
|
|
845
|
-
]);
|
|
846
|
-
diagnosticSiteCounts.set(diagnosticSite, (diagnosticSiteCounts.get(diagnosticSite) ?? 0) + 1);
|
|
847
|
-
if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
|
|
851
|
+
return `${sections.join("\n")}\n`;
|
|
852
|
+
};
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/cli/utils/write-diagnostics-directory.ts
|
|
855
|
+
const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
|
|
856
|
+
const readPreviousRuleDumpFileNames = (directory) => {
|
|
857
|
+
try {
|
|
858
|
+
const previous = JSON.parse(fs$2.readFileSync(path$1.join(directory, "diagnostics.json"), "utf8"));
|
|
859
|
+
return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
|
|
860
|
+
} catch {
|
|
861
|
+
return /* @__PURE__ */ new Set();
|
|
848
862
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
for (const
|
|
853
|
-
|
|
854
|
-
for (const [
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
863
|
+
};
|
|
864
|
+
const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
|
|
865
|
+
const directory = outputDirectory ? path$1.resolve(outputDirectory) : path$1.join(tmpdir(), `react-doctor-${randomUUID()}`);
|
|
866
|
+
if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) fs$2.rmSync(path$1.join(directory, fileName), { force: true });
|
|
867
|
+
fs$2.mkdirSync(directory, { recursive: true });
|
|
868
|
+
for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) fs$2.writeFileSync(path$1.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
|
|
869
|
+
fs$2.writeFileSync(path$1.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
|
|
870
|
+
return directory;
|
|
871
|
+
};
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/cli/utils/print-diagnostics-dump.ts
|
|
874
|
+
const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
|
|
875
|
+
const writtenDirectory = yield* try_({
|
|
876
|
+
try: () => writeDiagnosticsDirectory(diagnostics, outputDirectory),
|
|
877
|
+
catch: (cause) => cause
|
|
878
|
+
}).pipe(orElseSucceed(() => null));
|
|
879
|
+
if (writtenDirectory === null || !verbose && !outputDirectory) return;
|
|
880
|
+
const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
|
|
881
|
+
yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
|
|
882
|
+
});
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/cli/utils/collect-affected-files.ts
|
|
885
|
+
const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/cli/utils/print-footer.ts
|
|
888
|
+
const buildShareUrl = (input) => {
|
|
889
|
+
const errorCount = input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
890
|
+
const warningCount = input.diagnostics.length - errorCount;
|
|
891
|
+
const affectedFileCount = collectAffectedFiles(input.diagnostics).size;
|
|
892
|
+
const parameters = new URLSearchParams({ p: input.projectName });
|
|
893
|
+
if (input.scoreResult) parameters.set("s", String(input.scoreResult.score));
|
|
894
|
+
if (errorCount > 0) parameters.set("e", String(errorCount));
|
|
895
|
+
if (warningCount > 0) parameters.set("w", String(warningCount));
|
|
896
|
+
if (affectedFileCount > 0) parameters.set("f", String(affectedFileCount));
|
|
897
|
+
return `${SHARE_BASE_URL}?${parameters.toString()}`;
|
|
898
|
+
};
|
|
899
|
+
const printFooter = (input) => gen(function* () {
|
|
900
|
+
yield* log("");
|
|
901
|
+
yield* log(buildSectionDivider());
|
|
902
|
+
yield* log("");
|
|
903
|
+
const shareUrl = input.isOffline ? null : buildShareUrl(input);
|
|
904
|
+
for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
|
|
905
|
+
});
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/cli/utils/format-elapsed-time.ts
|
|
908
|
+
const formatElapsedTime = (elapsedMilliseconds) => {
|
|
909
|
+
if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
|
|
910
|
+
return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
911
|
+
};
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/cli/utils/format-skipped-check-label.ts
|
|
914
|
+
const formatSkippedCheckLabel = (skippedCheck) => skippedCheck === "dead-code" ? "maintainability" : skippedCheck;
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/cli/utils/pluralize.ts
|
|
917
|
+
const pluralize = (count, singularNoun) => `${count} ${singularNoun}${count === 1 ? "" : "s"}`;
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/cli/utils/print-headless-report.ts
|
|
920
|
+
const formatContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
|
|
921
|
+
const printHeadlessReport = (input) => gen(function* () {
|
|
922
|
+
yield* log(`${highlighter.success("✔")} Scanned ${pluralize(input.scannedFileCount, "file")} in ${formatElapsedTime(input.elapsedMilliseconds)}`);
|
|
923
|
+
yield* log("");
|
|
924
|
+
yield* log(highlighter.bold(`React Doctor — ${input.projectName}`));
|
|
925
|
+
if (input.scoreResult) yield* log(`Score: ${input.scoreResult.score} / 100 ${input.scoreResult.label}`);
|
|
926
|
+
else yield* log(highlighter.gray(input.noScoreMessage));
|
|
927
|
+
if (input.diagnostics.length === 0) {
|
|
928
|
+
yield* log("");
|
|
929
|
+
yield* log(highlighter.success(`✔ ${input.emptyStateMessage}`));
|
|
930
|
+
} else {
|
|
931
|
+
yield* log("");
|
|
932
|
+
yield* log(highlighter.bold(pluralize(input.diagnostics.length, "issue")));
|
|
933
|
+
const categoryCounts = /* @__PURE__ */ new Map();
|
|
934
|
+
for (const diagnostic of input.diagnostics) {
|
|
935
|
+
const counts = categoryCounts.get(diagnostic.category) ?? {
|
|
936
|
+
error: 0,
|
|
937
|
+
warning: 0
|
|
938
|
+
};
|
|
939
|
+
categoryCounts.set(diagnostic.category, {
|
|
940
|
+
error: counts.error + Number(diagnostic.severity === "error"),
|
|
941
|
+
warning: counts.warning + Number(diagnostic.severity === "warning")
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
for (const [category, counts] of categoryCounts) {
|
|
945
|
+
const countParts = [];
|
|
946
|
+
if (counts.error > 0) countParts.push(pluralize(counts.error, "error"));
|
|
947
|
+
if (counts.warning > 0) countParts.push(pluralize(counts.warning, "warning"));
|
|
948
|
+
yield* log(`${category}: ${countParts.join(", ")}`);
|
|
949
|
+
}
|
|
950
|
+
yield* log("");
|
|
951
|
+
for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(input.diagnostics)) {
|
|
952
|
+
const representativeDiagnostic = ruleDiagnostics[0];
|
|
953
|
+
if (representativeDiagnostic === void 0) continue;
|
|
954
|
+
const severityIcon = representativeDiagnostic.severity === "error" ? "✖" : "⚠";
|
|
955
|
+
const colorize = representativeDiagnostic.severity === "error" ? highlighter.error : highlighter.warn;
|
|
956
|
+
const occurrenceSuffix = ruleDiagnostics.length > 1 ? ` ×${ruleDiagnostics.length}` : "";
|
|
957
|
+
yield* log(colorize(`${severityIcon} ${representativeDiagnostic.title ?? ruleKey}${occurrenceSuffix}`));
|
|
958
|
+
yield* log(highlighter.gray(` ${ruleKey}`));
|
|
959
|
+
for (const diagnostic of ruleDiagnostics) yield* log(` ${formatDiagnosticSite(diagnostic)}${formatContextTag(diagnostic)}`);
|
|
960
|
+
}
|
|
869
961
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}),
|
|
905
|
-
...withNamespace("lint", {
|
|
906
|
-
failed: input.didLintFail,
|
|
907
|
-
failureReasonKind: input.lintFailureReasonKind,
|
|
908
|
-
partialFailureCount: input.lintPartialFailureCount,
|
|
909
|
-
droppedFileCount: input.lintDroppedFileCount,
|
|
910
|
-
deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
|
|
911
|
-
cacheHitFiles: result.lintCacheHitFileCount,
|
|
912
|
-
cacheTotalFiles: result.lintCacheTotalFileCount,
|
|
913
|
-
cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
|
|
914
|
-
sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
|
|
915
|
-
sidecarTotalFiles: result.lintSidecarTotalFileCount,
|
|
916
|
-
sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
|
|
917
|
-
}),
|
|
918
|
-
...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
|
|
919
|
-
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
|
|
920
|
-
...withNamespace("securityScan", { failed: input.securityScanFailed }),
|
|
921
|
-
...withNamespace("timing", {
|
|
922
|
-
elapsedMs: result.elapsedMilliseconds,
|
|
923
|
-
scanMs: result.scanElapsedMilliseconds
|
|
924
|
-
}),
|
|
925
|
-
...withNamespace("migration", {
|
|
926
|
-
largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
927
|
-
largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
928
|
-
largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
|
|
929
|
-
})
|
|
962
|
+
if (input.skippedChecks.length > 0) {
|
|
963
|
+
yield* log("");
|
|
964
|
+
yield* warn(highlighter.warn(`Results are incomplete: ${input.skippedChecks.map(formatSkippedCheckLabel).join(" and ")} checks failed.`));
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/cli/utils/render-agent-guidance.ts
|
|
969
|
+
const AGENT_GUIDANCE_LINES = [
|
|
970
|
+
"Treat React Doctor diagnostics as starting hypotheses. Read the relevant code before confirming or suppressing each finding.",
|
|
971
|
+
"For each group, decide true positive, false positive, or needs-human-review, then assign high/medium/low confidence.",
|
|
972
|
+
"Do not suppress a finding without evidence from the file in question. Confidence requires code context.",
|
|
973
|
+
"Understand the root cause before editing. Fix the underlying code instead of changing react-doctor config or suppressing rules unless explicitly asked.",
|
|
974
|
+
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
975
|
+
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
976
|
+
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
977
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
978
|
+
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
979
|
+
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
980
|
+
"When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
|
|
981
|
+
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
982
|
+
"If a fix needs an API, UX, or architecture decision, stop and ask before editing."
|
|
983
|
+
];
|
|
984
|
+
const printAgentGuidance = () => gen(function* () {
|
|
985
|
+
yield* log(`${highlighter.bold("Agent guidance")}`);
|
|
986
|
+
for (const line of AGENT_GUIDANCE_LINES) yield* log(highlighter.gray(` - ${line}`));
|
|
987
|
+
yield* log("");
|
|
988
|
+
});
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region src/cli/utils/finalize-inspect-result.ts
|
|
991
|
+
const buildInspectPresentation = ({ options, payload }) => {
|
|
992
|
+
const surfaceDiagnostics = filterDiagnosticsForSurface([...payload.diagnostics], options.outputSurface, payload.userConfig);
|
|
993
|
+
return {
|
|
994
|
+
diagnostics: filterDiagnosticsByCategories(surfaceDiagnostics, options.categoryFilters),
|
|
995
|
+
demotedDiagnosticCount: payload.diagnostics.length - surfaceDiagnostics.length
|
|
930
996
|
};
|
|
931
|
-
if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
|
|
932
|
-
new: summary.totalDiagnosticCount,
|
|
933
|
-
fixed: result.baselineDelta.fixedCount,
|
|
934
|
-
baseTotal: result.baselineDelta.baseTotalCount,
|
|
935
|
-
crossFileMatches: result.baselineDelta.crossFileMatchCount,
|
|
936
|
-
degraded: false
|
|
937
|
-
}));
|
|
938
|
-
else if (input.gateExempt) attributes["baseline.degraded"] = true;
|
|
939
|
-
return attributes;
|
|
940
997
|
};
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
998
|
+
const printScoreOnlyInspectResult = ({ options, payload, diagnostics, noScoreMessage }) => gen(function* () {
|
|
999
|
+
if (options.outputDirectory !== null) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, false, "stderr");
|
|
1000
|
+
if (payload.score) {
|
|
1001
|
+
yield* log(`${payload.score.score}`);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
yield* error(highlighter.gray(noScoreMessage));
|
|
1005
|
+
});
|
|
1006
|
+
const printFullInspectResultDetails = ({ options, payload, diagnostics, demotedDiagnosticCount }) => gen(function* () {
|
|
1007
|
+
if (options.outputDirectory !== null || options.verbose) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, options.verbose);
|
|
1008
|
+
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
1009
|
+
yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
|
|
1010
|
+
yield* log("");
|
|
1011
|
+
}
|
|
1012
|
+
yield* printFooter({
|
|
1013
|
+
diagnostics,
|
|
1014
|
+
scoreResult: payload.score,
|
|
1015
|
+
projectName: payload.project.projectName,
|
|
1016
|
+
isOffline: options.isCi || !options.share || payload.score === null
|
|
949
1017
|
});
|
|
950
|
-
};
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
showWarnings: input.showWarnings,
|
|
967
|
-
usedOutputDir: input.usedOutputDir,
|
|
968
|
-
ignoredTagCount: input.ignoredTagCount,
|
|
969
|
-
hasCustomConfig: input.hasCustomConfig,
|
|
970
|
-
rulesConfigured: ruleKeys.length,
|
|
971
|
-
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
|
|
972
|
-
projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
|
|
973
|
-
fileCount: input.result?.scannedFileCount,
|
|
974
|
-
nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
|
|
975
|
-
htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
|
|
976
|
-
multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
|
|
1018
|
+
});
|
|
1019
|
+
const printFullInspectResult = ({ options, elapsedMilliseconds, payload, result, diagnostics, demotedDiagnosticCount, noScoreMessage }) => gen(function* () {
|
|
1020
|
+
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
1021
|
+
yield* printHeadlessReport({
|
|
1022
|
+
diagnostics,
|
|
1023
|
+
elapsedMilliseconds,
|
|
1024
|
+
emptyStateMessage: buildEmptyReportMessage({
|
|
1025
|
+
categoryFilters: options.categoryFilters,
|
|
1026
|
+
demotedDiagnosticCount,
|
|
1027
|
+
outputSurface: options.outputSurface
|
|
1028
|
+
}),
|
|
1029
|
+
noScoreMessage,
|
|
1030
|
+
projectName: payload.project.projectName,
|
|
1031
|
+
scannedFileCount: payload.scannedFileCount,
|
|
1032
|
+
scoreResult: result.skippedChecks.length > 0 ? null : payload.score,
|
|
1033
|
+
skippedChecks: result.skippedChecks
|
|
977
1034
|
});
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1035
|
+
yield* printFullInspectResultDetails({
|
|
1036
|
+
options,
|
|
1037
|
+
payload,
|
|
1038
|
+
diagnostics,
|
|
1039
|
+
demotedDiagnosticCount
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
const finalizeInspectResult = (input) => gen(function* () {
|
|
1043
|
+
const { payload } = input;
|
|
1044
|
+
const result = buildInspectResult(input);
|
|
1045
|
+
const noScoreMessage = buildNoScoreMessage({
|
|
1046
|
+
isScoreDisabled: input.options.noScore,
|
|
1047
|
+
isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
|
|
1048
|
+
disabledMessage: input.options.scoreDisabledMessage
|
|
1049
|
+
});
|
|
1050
|
+
if (input.options.suppressRendering) return result;
|
|
1051
|
+
const presentation = buildInspectPresentation(input);
|
|
1052
|
+
if (input.options.scoreOnly) {
|
|
1053
|
+
yield* printScoreOnlyInspectResult({
|
|
1054
|
+
options: input.options,
|
|
1055
|
+
payload,
|
|
1056
|
+
diagnostics: presentation.diagnostics,
|
|
1057
|
+
noScoreMessage
|
|
1058
|
+
});
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
yield* printFullInspectResult({
|
|
1062
|
+
...input,
|
|
1063
|
+
...presentation,
|
|
1064
|
+
result,
|
|
1065
|
+
noScoreMessage
|
|
1066
|
+
});
|
|
1067
|
+
return result;
|
|
999
1068
|
});
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
1000
1071
|
/**
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1072
|
+
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
1073
|
+
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
1074
|
+
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
1075
|
+
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
1076
|
+
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
1077
|
+
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
1078
|
+
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
1079
|
+
* parallelism correctly instead of always reading `false`.
|
|
1004
1080
|
*/
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1081
|
+
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
1082
|
+
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
1083
|
+
return {
|
|
1084
|
+
workerCount,
|
|
1085
|
+
parallel: workerCount !== void 0 && workerCount > 1
|
|
1086
|
+
};
|
|
1010
1087
|
};
|
|
1011
1088
|
//#endregion
|
|
1012
|
-
//#region src/cli/utils/
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1089
|
+
//#region src/cli/utils/render-and-record-scan.ts
|
|
1090
|
+
const silentConsole$2 = makeNoopConsole();
|
|
1091
|
+
const deriveScope = (options) => {
|
|
1092
|
+
if (options.baseline) return "changed";
|
|
1093
|
+
if (options.changedLineRanges !== null) return "lines";
|
|
1094
|
+
return options.includePaths.length > 0 ? "files" : "full";
|
|
1016
1095
|
};
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1096
|
+
const buildRunEventConfig = (options, userConfig, resolvedWorkerCount) => {
|
|
1097
|
+
const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
|
|
1098
|
+
return {
|
|
1099
|
+
scope: deriveScope(options),
|
|
1100
|
+
parallel,
|
|
1101
|
+
workerCount,
|
|
1102
|
+
maxDurationMs: options.maxDurationMs,
|
|
1103
|
+
lint: options.lint,
|
|
1104
|
+
deadCode: options.deadCode,
|
|
1105
|
+
supplyChain: options.supplyChain,
|
|
1106
|
+
scoreOnly: options.scoreOnly,
|
|
1107
|
+
noScore: options.noScore,
|
|
1108
|
+
respectInlineDisables: options.respectInlineDisables,
|
|
1109
|
+
showWarnings: options.warnings,
|
|
1110
|
+
usedOutputDir: options.outputDirectory !== null,
|
|
1111
|
+
ignoredTagCount: options.ignoredTags.size,
|
|
1112
|
+
hasCustomConfig: userConfig !== null,
|
|
1113
|
+
userConfig
|
|
1114
|
+
};
|
|
1115
|
+
};
|
|
1116
|
+
const renderAndRecordScan = async (input) => {
|
|
1117
|
+
const cacheStats = {
|
|
1118
|
+
lintCacheHitFileCount: input.cacheStats?.lintCacheHitFileCount ?? null,
|
|
1119
|
+
lintCacheTotalFileCount: input.cacheStats?.lintCacheTotalFileCount ?? null,
|
|
1120
|
+
lintSidecarReplayedFileCount: input.cacheStats?.lintSidecarReplayedFileCount ?? null,
|
|
1121
|
+
lintSidecarTotalFileCount: input.cacheStats?.lintSidecarTotalFileCount ?? null,
|
|
1122
|
+
deadCodeCacheHit: input.cacheStats?.deadCodeCacheHit ?? null,
|
|
1123
|
+
deadCodeSummaryCacheHits: input.cacheStats?.deadCodeSummaryCacheHits ?? null,
|
|
1124
|
+
deadCodeSummaryCacheMisses: input.cacheStats?.deadCodeSummaryCacheMisses ?? null
|
|
1125
|
+
};
|
|
1126
|
+
const finalizeEffect = finalizeInspectResult({
|
|
1127
|
+
options: input.options,
|
|
1128
|
+
elapsedMilliseconds: performance.now() - input.startTime,
|
|
1129
|
+
payload: input.payload,
|
|
1130
|
+
cacheStats
|
|
1131
|
+
});
|
|
1132
|
+
const result = await runPromise(input.options.silent ? finalizeEffect.pipe(provideService(Console, silentConsole$2)) : finalizeEffect);
|
|
1133
|
+
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
1134
|
+
recordScanMetrics({
|
|
1135
|
+
result,
|
|
1136
|
+
mode: input.scanMode,
|
|
1137
|
+
baselineDegraded: input.baselineDegraded,
|
|
1138
|
+
parallel,
|
|
1139
|
+
workerCount: resolvedWorkerCount,
|
|
1140
|
+
lint: input.options.lint,
|
|
1141
|
+
deadCode: input.options.deadCode,
|
|
1142
|
+
scoreOnly: input.options.scoreOnly,
|
|
1143
|
+
noScore: input.options.noScore,
|
|
1144
|
+
didLintFail: input.payload.didLintFail,
|
|
1145
|
+
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
1146
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
1147
|
+
userConfig: input.payload.userConfig,
|
|
1148
|
+
suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
|
|
1149
|
+
});
|
|
1150
|
+
recordRunEvent(input.rootSpan, {
|
|
1151
|
+
...buildRunEventConfig(input.options, input.payload.userConfig, resolvedWorkerCount),
|
|
1152
|
+
result,
|
|
1153
|
+
mode: input.scanMode,
|
|
1154
|
+
gateExempt: input.baselineDegraded,
|
|
1155
|
+
wholeRepoCacheHit: input.wholeRepoCacheHit,
|
|
1156
|
+
didLintFail: input.payload.didLintFail,
|
|
1157
|
+
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
1158
|
+
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
1159
|
+
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
1160
|
+
lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
|
|
1161
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
1162
|
+
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
1163
|
+
securityScanFailed: input.payload.securityScanFailed,
|
|
1164
|
+
suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
|
|
1026
1165
|
});
|
|
1166
|
+
return result;
|
|
1027
1167
|
};
|
|
1028
1168
|
//#endregion
|
|
1029
|
-
//#region src/cli/utils/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (!isLintEnabled) return null;
|
|
1054
|
-
const resolver = yield* NodeResolver;
|
|
1055
|
-
const initial = yield* resolver.resolve();
|
|
1056
|
-
if (initial !== null) {
|
|
1057
|
-
if (!initial.isCurrentNode && !isQuiet) {
|
|
1058
|
-
yield* consoleWarn(`Node ${process.version} is unsupported by oxlint. Using Node ${initial.version} from nvm.`);
|
|
1059
|
-
yield* consoleBreak;
|
|
1060
|
-
}
|
|
1061
|
-
return initial.binaryPath;
|
|
1062
|
-
}
|
|
1063
|
-
if (isQuiet) return null;
|
|
1064
|
-
yield* consoleWarn(`Node ${process.version} is not compatible with oxlint (requires ${OXLINT_NODE_REQUIREMENT}). Lint checks will be skipped.`);
|
|
1065
|
-
const isNvmInstalled = yield* resolver.isNvmInstalled();
|
|
1066
|
-
if (isNvmInstalled && process.stdin.isTTY) {
|
|
1067
|
-
if (yield* promptShouldInstallNode()) {
|
|
1068
|
-
yield* consoleBreak;
|
|
1069
|
-
const fresh = (yield* resolver.installViaNvm()) ? yield* resolver.resolve() : null;
|
|
1070
|
-
if (fresh) {
|
|
1071
|
-
yield* consoleBreak;
|
|
1072
|
-
yield* consoleSuccess(`Node ${fresh.version} installed. Using it for lint checks.`);
|
|
1073
|
-
yield* consoleBreak;
|
|
1074
|
-
return fresh.binaryPath;
|
|
1075
|
-
}
|
|
1076
|
-
yield* consoleBreak;
|
|
1077
|
-
yield* consoleWarn("Failed to install Node via nvm. Skipping lint checks.");
|
|
1078
|
-
yield* consoleBreak;
|
|
1079
|
-
return null;
|
|
1169
|
+
//#region src/cli/utils/copy-unchanged-baseline-sources.ts
|
|
1170
|
+
const copyUnchangedBaselineSources = async (input) => {
|
|
1171
|
+
const baseMaterializedFiles = new Set(input.baseMaterializedFiles.map(toForwardSlashes));
|
|
1172
|
+
const headChangedFiles = new Set(input.headChangedFiles.map(toForwardSlashes));
|
|
1173
|
+
const untrackedFiles = new Set(input.untrackedFiles.map(toForwardSlashes));
|
|
1174
|
+
const sourceDirectory = path$1.resolve(input.directory);
|
|
1175
|
+
const targetDirectory = path$1.resolve(input.tempDirectory);
|
|
1176
|
+
return (await mapWithConcurrency(input.sourceFiles.filter((filePath) => {
|
|
1177
|
+
const normalizedPath = toForwardSlashes(filePath);
|
|
1178
|
+
return !baseMaterializedFiles.has(normalizedPath) && !headChangedFiles.has(normalizedPath) && !untrackedFiles.has(normalizedPath);
|
|
1179
|
+
}), 32, async (filePath) => {
|
|
1180
|
+
input.signal?.throwIfAborted();
|
|
1181
|
+
if (input.deadlineEpochMs !== null && Date.now() >= input.deadlineEpochMs) return false;
|
|
1182
|
+
const sourcePath = path$1.resolve(sourceDirectory, filePath);
|
|
1183
|
+
const targetPath = path$1.resolve(targetDirectory, filePath);
|
|
1184
|
+
if (!isPathInsideDirectory(sourcePath, sourceDirectory) || !isPathInsideDirectory(targetPath, targetDirectory)) return false;
|
|
1185
|
+
try {
|
|
1186
|
+
if (!(await fs$1.lstat(sourcePath)).isFile()) return false;
|
|
1187
|
+
await fs$1.mkdir(path$1.dirname(targetPath), { recursive: true });
|
|
1188
|
+
await fs$1.copyFile(sourcePath, targetPath);
|
|
1189
|
+
return true;
|
|
1190
|
+
} catch {
|
|
1191
|
+
input.signal?.throwIfAborted();
|
|
1192
|
+
return false;
|
|
1080
1193
|
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
yield* consoleBreak;
|
|
1084
|
-
return null;
|
|
1085
|
-
});
|
|
1086
|
-
const resolveOxlintNode = (isLintEnabled, isQuiet) => runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(provide(NodeResolver.layerNode)));
|
|
1194
|
+
})).every(Boolean);
|
|
1195
|
+
};
|
|
1087
1196
|
//#endregion
|
|
1088
|
-
//#region src/cli/utils/
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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;
|
|
1102
1293
|
}
|
|
1103
1294
|
}
|
|
1104
|
-
|
|
1105
|
-
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
1295
|
+
return null;
|
|
1106
1296
|
};
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
for (const includedTag of includedTags) ignoredTags.delete(includedTag);
|
|
1112
|
-
return ignoredTags;
|
|
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);
|
|
1113
1301
|
};
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
uiLayers: inputOptions.uiLayers ?? null,
|
|
1143
|
-
concurrentScan: inputOptions.concurrentScan ?? false,
|
|
1144
|
-
concurrency: inputOptions.concurrency,
|
|
1145
|
-
maxDurationMs: inputOptions.maxDurationMs ?? null,
|
|
1146
|
-
baseline: inputOptions.baseline ?? null,
|
|
1147
|
-
changedLineRanges: inputOptions.changedLineRanges ?? null,
|
|
1148
|
-
supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
|
|
1149
|
-
excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
|
|
1150
|
-
retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
|
|
1151
|
-
precomputedSourceFileCount: inputOptions.precomputedSourceFileCount,
|
|
1152
|
-
precomputedSourceFiles: inputOptions.precomputedSourceFiles
|
|
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);
|
|
1153
1330
|
};
|
|
1331
|
+
visit(record.sourceFile);
|
|
1332
|
+
return component;
|
|
1154
1333
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
//#endregion
|
|
1166
|
-
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
1167
|
-
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
1168
|
-
const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
|
|
1169
|
-
//#endregion
|
|
1170
|
-
//#region src/cli/utils/build-empty-report-message.ts
|
|
1171
|
-
const buildEmptyReportMessage = (input) => {
|
|
1172
|
-
const categoryFilters = [...input.categoryFilters];
|
|
1173
|
-
if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
|
|
1174
|
-
if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
|
|
1175
|
-
return "No issues found!";
|
|
1176
|
-
};
|
|
1177
|
-
//#endregion
|
|
1178
|
-
//#region src/cli/utils/build-inspect-result.ts
|
|
1179
|
-
const buildInspectResult = (input) => {
|
|
1180
|
-
const { payload, cacheStats } = input;
|
|
1181
|
-
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
1182
|
-
didLintFail: payload.didLintFail,
|
|
1183
|
-
lintFailureReason: payload.lintFailureReason,
|
|
1184
|
-
lintPartialFailures: payload.lintPartialFailures,
|
|
1185
|
-
didDeadCodeFail: payload.didDeadCodeFail,
|
|
1186
|
-
deadCodeFailureReason: payload.deadCodeFailureReason,
|
|
1187
|
-
supplyChainOverlapTimedOut: payload.supplyChainOverlapTimedOut,
|
|
1188
|
-
securityScanFailed: payload.securityScanFailed ?? false,
|
|
1189
|
-
securityScanFailureReason: payload.securityScanFailureReason ?? null
|
|
1190
|
-
});
|
|
1191
|
-
return {
|
|
1192
|
-
diagnostics: [...payload.diagnostics],
|
|
1193
|
-
score: payload.score,
|
|
1194
|
-
skippedChecks,
|
|
1195
|
-
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
1196
|
-
project: payload.project,
|
|
1197
|
-
elapsedMilliseconds: input.elapsedMilliseconds,
|
|
1198
|
-
scannedFileCount: payload.scannedFileCount,
|
|
1199
|
-
scannedFilePaths: payload.scannedFilePaths,
|
|
1200
|
-
analyzedFiles: payload.analyzedFiles ?? [],
|
|
1201
|
-
scanElapsedMilliseconds: payload.scanElapsedMilliseconds,
|
|
1202
|
-
...cacheStats.lintCacheTotalFileCount === null ? {} : {
|
|
1203
|
-
lintCacheHitFileCount: cacheStats.lintCacheHitFileCount,
|
|
1204
|
-
lintCacheTotalFileCount: cacheStats.lintCacheTotalFileCount
|
|
1205
|
-
},
|
|
1206
|
-
...cacheStats.lintSidecarTotalFileCount === null ? {} : {
|
|
1207
|
-
lintSidecarReplayedFileCount: cacheStats.lintSidecarReplayedFileCount,
|
|
1208
|
-
lintSidecarTotalFileCount: cacheStats.lintSidecarTotalFileCount
|
|
1209
|
-
},
|
|
1210
|
-
...cacheStats.deadCodeCacheHit === null ? {} : { deadCodeCacheHit: cacheStats.deadCodeCacheHit },
|
|
1211
|
-
...cacheStats.deadCodeSummaryCacheHits === null || cacheStats.deadCodeSummaryCacheMisses === null ? {} : {
|
|
1212
|
-
deadCodeSummaryCacheHits: cacheStats.deadCodeSummaryCacheHits,
|
|
1213
|
-
deadCodeSummaryCacheMisses: cacheStats.deadCodeSummaryCacheMisses
|
|
1214
|
-
},
|
|
1215
|
-
...payload.baselineDelta ? { baselineDelta: payload.baselineDelta } : {}
|
|
1216
|
-
};
|
|
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);
|
|
1217
1344
|
};
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
reason = input.disabledMessage ?? "Score disabled by --no-score.";
|
|
1230
|
-
break;
|
|
1231
|
-
case "analysis-incomplete":
|
|
1232
|
-
reason = "Score not shown because lint or maintainability analysis could not complete.";
|
|
1233
|
-
break;
|
|
1234
|
-
case "api-unavailable":
|
|
1235
|
-
reason = "Score unavailable (could not reach the score API).";
|
|
1236
|
-
break;
|
|
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
|
+
}
|
|
1237
1356
|
}
|
|
1238
|
-
return
|
|
1357
|
+
return tagNames;
|
|
1239
1358
|
};
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
|
|
1246
|
-
//#endregion
|
|
1247
|
-
//#region src/cli/utils/format-diagnostic-site.ts
|
|
1248
|
-
const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
|
|
1249
|
-
//#endregion
|
|
1250
|
-
//#region src/cli/utils/format-rule-summary.ts
|
|
1251
|
-
const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
|
|
1252
|
-
const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
1253
|
-
const firstDiagnostic = ruleDiagnostics[0];
|
|
1254
|
-
if (firstDiagnostic === void 0) return "";
|
|
1255
|
-
const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
|
|
1256
|
-
const sections = [
|
|
1257
|
-
`Rule: ${ruleKey}`,
|
|
1258
|
-
`Severity: ${firstDiagnostic.severity}`,
|
|
1259
|
-
`Category: ${firstDiagnostic.category}`,
|
|
1260
|
-
`Count: ${ruleDiagnostics.length}`,
|
|
1261
|
-
"",
|
|
1262
|
-
distinctMessages.join("\n\n")
|
|
1263
|
-
];
|
|
1264
|
-
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
1265
|
-
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
1266
|
-
const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
|
|
1267
|
-
if (fixRecipeLine) sections.push("", fixRecipeLine);
|
|
1268
|
-
sections.push("", "Files:");
|
|
1269
|
-
for (const diagnostic of ruleDiagnostics) {
|
|
1270
|
-
sections.push(` ${formatDiagnosticSite(diagnostic)}${formatFileContextTag(diagnostic)}`);
|
|
1271
|
-
if (diagnostic.suppressionHint) sections.push(` ${diagnostic.suppressionHint}`);
|
|
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;
|
|
1272
1364
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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;
|
|
1284
1382
|
}
|
|
1383
|
+
return target.text;
|
|
1285
1384
|
};
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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;
|
|
1293
1395
|
};
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
catch: (cause) => cause
|
|
1300
|
-
}).pipe(orElseSucceed(() => null));
|
|
1301
|
-
if (writtenDirectory === null || !verbose && !outputDirectory) return;
|
|
1302
|
-
const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
|
|
1303
|
-
yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
|
|
1304
|
-
});
|
|
1305
|
-
//#endregion
|
|
1306
|
-
//#region src/cli/utils/collect-affected-files.ts
|
|
1307
|
-
const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
|
|
1308
|
-
//#endregion
|
|
1309
|
-
//#region src/cli/utils/print-footer.ts
|
|
1310
|
-
const buildShareUrl = (input) => {
|
|
1311
|
-
const errorCount = input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
1312
|
-
const warningCount = input.diagnostics.length - errorCount;
|
|
1313
|
-
const affectedFileCount = collectAffectedFiles(input.diagnostics).size;
|
|
1314
|
-
const parameters = new URLSearchParams({ p: input.projectName });
|
|
1315
|
-
if (input.scoreResult) parameters.set("s", String(input.scoreResult.score));
|
|
1316
|
-
if (errorCount > 0) parameters.set("e", String(errorCount));
|
|
1317
|
-
if (warningCount > 0) parameters.set("w", String(warningCount));
|
|
1318
|
-
if (affectedFileCount > 0) parameters.set("f", String(affectedFileCount));
|
|
1319
|
-
return `${SHARE_BASE_URL}?${parameters.toString()}`;
|
|
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;
|
|
1320
1401
|
};
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
//#region src/cli/utils/format-elapsed-time.ts
|
|
1330
|
-
const formatElapsedTime = (elapsedMilliseconds) => {
|
|
1331
|
-
if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
|
|
1332
|
-
return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
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();
|
|
1333
1410
|
};
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
yield* log("");
|
|
1346
|
-
yield* log(highlighter.bold(`React Doctor — ${input.projectName}`));
|
|
1347
|
-
if (input.scoreResult) yield* log(`Score: ${input.scoreResult.score} / 100 ${input.scoreResult.label}`);
|
|
1348
|
-
else yield* log(highlighter.gray(input.noScoreMessage));
|
|
1349
|
-
if (input.diagnostics.length === 0) {
|
|
1350
|
-
yield* log("");
|
|
1351
|
-
yield* log(highlighter.success(`✔ ${input.emptyStateMessage}`));
|
|
1352
|
-
} else {
|
|
1353
|
-
yield* log("");
|
|
1354
|
-
yield* log(highlighter.bold(pluralize(input.diagnostics.length, "issue")));
|
|
1355
|
-
const categoryCounts = /* @__PURE__ */ new Map();
|
|
1356
|
-
for (const diagnostic of input.diagnostics) {
|
|
1357
|
-
const counts = categoryCounts.get(diagnostic.category) ?? {
|
|
1358
|
-
error: 0,
|
|
1359
|
-
warning: 0
|
|
1360
|
-
};
|
|
1361
|
-
categoryCounts.set(diagnostic.category, {
|
|
1362
|
-
error: counts.error + Number(diagnostic.severity === "error"),
|
|
1363
|
-
warning: counts.warning + Number(diagnostic.severity === "warning")
|
|
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
|
|
1364
1422
|
});
|
|
1365
1423
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
yield* log(colorize(`${severityIcon} ${representativeDiagnostic.title ?? ruleKey}${occurrenceSuffix}`));
|
|
1380
|
-
yield* log(highlighter.gray(` ${ruleKey}`));
|
|
1381
|
-
for (const diagnostic of ruleDiagnostics) yield* log(` ${formatDiagnosticSite(diagnostic)}${formatContextTag(diagnostic)}`);
|
|
1382
|
-
}
|
|
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;
|
|
1383
1437
|
}
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1386
|
-
|
|
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;
|
|
1387
1458
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
+
};
|
|
1411
1528
|
//#endregion
|
|
1412
|
-
//#region src/cli/utils/
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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;
|
|
1418
1545
|
};
|
|
1419
1546
|
};
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
|
1439
1568
|
});
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
skippedChecks: result.skippedChecks
|
|
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))
|
|
1456
1584
|
});
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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
|
|
1462
1627
|
});
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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;
|
|
1471
1663
|
});
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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)
|
|
1480
1762
|
});
|
|
1481
|
-
return
|
|
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();
|
|
1482
1774
|
}
|
|
1483
|
-
yield* printFullInspectResult({
|
|
1484
|
-
...input,
|
|
1485
|
-
...presentation,
|
|
1486
|
-
result,
|
|
1487
|
-
noScoreMessage
|
|
1488
|
-
});
|
|
1489
|
-
return result;
|
|
1490
|
-
});
|
|
1491
|
-
//#endregion
|
|
1492
|
-
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
1493
|
-
/**
|
|
1494
|
-
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
1495
|
-
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
1496
|
-
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
1497
|
-
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
1498
|
-
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
1499
|
-
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
1500
|
-
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
1501
|
-
* parallelism correctly instead of always reading `false`.
|
|
1502
|
-
*/
|
|
1503
|
-
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
1504
|
-
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
1505
|
-
return {
|
|
1506
|
-
workerCount,
|
|
1507
|
-
parallel: workerCount !== void 0 && workerCount > 1
|
|
1508
|
-
};
|
|
1509
1775
|
};
|
|
1510
1776
|
//#endregion
|
|
1511
|
-
//#region src/cli/utils/
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
deadCode: options.deadCode,
|
|
1527
|
-
supplyChain: options.supplyChain,
|
|
1528
|
-
scoreOnly: options.scoreOnly,
|
|
1529
|
-
noScore: options.noScore,
|
|
1530
|
-
respectInlineDisables: options.respectInlineDisables,
|
|
1531
|
-
showWarnings: options.warnings,
|
|
1532
|
-
usedOutputDir: options.outputDirectory !== null,
|
|
1533
|
-
ignoredTagCount: options.ignoredTags.size,
|
|
1534
|
-
hasCustomConfig: userConfig !== null,
|
|
1535
|
-
userConfig
|
|
1536
|
-
};
|
|
1537
|
-
};
|
|
1538
|
-
const renderAndRecordScan = async (input) => {
|
|
1539
|
-
const cacheStats = {
|
|
1540
|
-
lintCacheHitFileCount: input.cacheStats?.lintCacheHitFileCount ?? null,
|
|
1541
|
-
lintCacheTotalFileCount: input.cacheStats?.lintCacheTotalFileCount ?? null,
|
|
1542
|
-
lintSidecarReplayedFileCount: input.cacheStats?.lintSidecarReplayedFileCount ?? null,
|
|
1543
|
-
lintSidecarTotalFileCount: input.cacheStats?.lintSidecarTotalFileCount ?? null,
|
|
1544
|
-
deadCodeCacheHit: input.cacheStats?.deadCodeCacheHit ?? null,
|
|
1545
|
-
deadCodeSummaryCacheHits: input.cacheStats?.deadCodeSummaryCacheHits ?? null,
|
|
1546
|
-
deadCodeSummaryCacheMisses: input.cacheStats?.deadCodeSummaryCacheMisses ?? null
|
|
1547
|
-
};
|
|
1548
|
-
const finalizeEffect = finalizeInspectResult({
|
|
1549
|
-
options: input.options,
|
|
1550
|
-
elapsedMilliseconds: performance.now() - input.startTime,
|
|
1551
|
-
payload: input.payload,
|
|
1552
|
-
cacheStats
|
|
1553
|
-
});
|
|
1554
|
-
const result = await runPromise(input.options.silent ? finalizeEffect.pipe(provideService(Console, silentConsole$2)) : finalizeEffect);
|
|
1555
|
-
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
1556
|
-
recordScanMetrics({
|
|
1557
|
-
result,
|
|
1558
|
-
mode: input.scanMode,
|
|
1559
|
-
baselineDegraded: input.baselineDegraded,
|
|
1560
|
-
parallel,
|
|
1561
|
-
workerCount: resolvedWorkerCount,
|
|
1562
|
-
lint: input.options.lint,
|
|
1563
|
-
deadCode: input.options.deadCode,
|
|
1564
|
-
scoreOnly: input.options.scoreOnly,
|
|
1565
|
-
noScore: input.options.noScore,
|
|
1566
|
-
didLintFail: input.payload.didLintFail,
|
|
1567
|
-
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
1568
|
-
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
1569
|
-
userConfig: input.payload.userConfig,
|
|
1570
|
-
suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
|
|
1571
|
-
});
|
|
1572
|
-
recordRunEvent(input.rootSpan, {
|
|
1573
|
-
...buildRunEventConfig(input.options, input.payload.userConfig, resolvedWorkerCount),
|
|
1574
|
-
result,
|
|
1575
|
-
mode: input.scanMode,
|
|
1576
|
-
gateExempt: input.baselineDegraded,
|
|
1577
|
-
wholeRepoCacheHit: input.wholeRepoCacheHit,
|
|
1578
|
-
didLintFail: input.payload.didLintFail,
|
|
1579
|
-
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
1580
|
-
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
1581
|
-
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
1582
|
-
lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
|
|
1583
|
-
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
1584
|
-
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
1585
|
-
securityScanFailed: input.payload.securityScanFailed,
|
|
1586
|
-
suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
|
|
1587
|
-
});
|
|
1588
|
-
return result;
|
|
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
|
+
}
|
|
1589
1792
|
};
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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);
|
|
1617
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;
|
|
1618
1901
|
//#endregion
|
|
1619
|
-
//#region src/cli/utils/
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
".
|
|
1626
|
-
".
|
|
1627
|
-
".
|
|
1628
|
-
".cjs"
|
|
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"
|
|
1629
1911
|
];
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1632
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
1633
|
-
if (extension === ".tsx") return _react_doctor_require_typescript_default.ScriptKind.TSX;
|
|
1634
|
-
if (extension === ".jsx") return _react_doctor_require_typescript_default.ScriptKind.JSX;
|
|
1635
|
-
if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return _react_doctor_require_typescript_default.ScriptKind.JS;
|
|
1636
|
-
return _react_doctor_require_typescript_default.ScriptKind.TS;
|
|
1637
|
-
};
|
|
1638
|
-
const resolveSafeSourcePath = (rootDirectory, filePath) => {
|
|
1912
|
+
const bundledRequire = createRequire(import.meta.url);
|
|
1913
|
+
const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
|
|
1639
1914
|
try {
|
|
1640
|
-
|
|
1641
|
-
const absolutePath = path.resolve(resolvedRootDirectory, filePath);
|
|
1642
|
-
const relativePath = path.relative(resolvedRootDirectory, absolutePath);
|
|
1643
|
-
if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
|
|
1644
|
-
const stats = fs.lstatSync(absolutePath);
|
|
1645
|
-
if (!stats.isFile() || stats.isSymbolicLink()) return null;
|
|
1646
|
-
const realPath = fs.realpathSync(absolutePath);
|
|
1647
|
-
const realRelativePath = path.relative(resolvedRootDirectory, realPath);
|
|
1648
|
-
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"}`;
|
|
1649
1916
|
} catch {
|
|
1650
|
-
return
|
|
1917
|
+
return "oxlint-plugin-react-doctor#fingerprint=unresolved";
|
|
1651
1918
|
}
|
|
1652
|
-
};
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
if (
|
|
1656
|
-
const
|
|
1657
|
-
if (
|
|
1658
|
-
|
|
1659
|
-
|
|
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;
|
|
1660
1937
|
}
|
|
1938
|
+
return normalizedRecord;
|
|
1939
|
+
};
|
|
1940
|
+
const stringifyStableJson = (value) => {
|
|
1661
1941
|
try {
|
|
1662
|
-
|
|
1663
|
-
const record = {
|
|
1664
|
-
absolutePath,
|
|
1665
|
-
filePath: normalizedFilePath,
|
|
1666
|
-
sourceFile: _react_doctor_require_typescript_default.createSourceFile(absolutePath, sourceText, _react_doctor_require_typescript_default.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
|
|
1667
|
-
sourceText
|
|
1668
|
-
};
|
|
1669
|
-
state.recordByFilePath.set(normalizedFilePath, record);
|
|
1670
|
-
return record;
|
|
1942
|
+
return JSON.stringify(normalizeForStableJson(value));
|
|
1671
1943
|
} catch {
|
|
1672
|
-
state.recordByFilePath.set(normalizedFilePath, null);
|
|
1673
1944
|
return null;
|
|
1674
1945
|
}
|
|
1675
1946
|
};
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
return currentExpression;
|
|
1688
|
-
};
|
|
1689
|
-
const getCalleeName = (expression) => {
|
|
1690
|
-
const unwrappedExpression = unwrapExpression(expression);
|
|
1691
|
-
if (_react_doctor_require_typescript_default.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
|
|
1692
|
-
if (_react_doctor_require_typescript_default.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
|
|
1693
|
-
return null;
|
|
1694
|
-
};
|
|
1695
|
-
const isComponentWrapperCall = (node) => {
|
|
1696
|
-
const wrapperName = getCalleeName(node.expression);
|
|
1697
|
-
return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
|
|
1698
|
-
};
|
|
1699
|
-
const getFunctionName = (node) => {
|
|
1700
|
-
if (_react_doctor_require_typescript_default.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
|
|
1701
|
-
if (_react_doctor_require_typescript_default.isArrowFunction(node) || _react_doctor_require_typescript_default.isFunctionExpression(node)) {
|
|
1702
|
-
let componentExpression = node;
|
|
1703
|
-
while (true) {
|
|
1704
|
-
const parent = componentExpression.parent;
|
|
1705
|
-
if (_react_doctor_require_typescript_default.isParenthesizedExpression(parent) || _react_doctor_require_typescript_default.isAsExpression(parent) || _react_doctor_require_typescript_default.isTypeAssertionExpression(parent) || _react_doctor_require_typescript_default.isNonNullExpression(parent) || _react_doctor_require_typescript_default.isSatisfiesExpression(parent)) {
|
|
1706
|
-
componentExpression = parent;
|
|
1707
|
-
continue;
|
|
1708
|
-
}
|
|
1709
|
-
if (_react_doctor_require_typescript_default.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
|
|
1710
|
-
componentExpression = parent;
|
|
1711
|
-
continue;
|
|
1712
|
-
}
|
|
1713
|
-
if (_react_doctor_require_typescript_default.isVariableDeclaration(parent) && _react_doctor_require_typescript_default.isIdentifier(parent.name)) return parent.name.text;
|
|
1714
|
-
return node.name?.text ?? null;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
return null;
|
|
1718
|
-
};
|
|
1719
|
-
const isComponentFunction = (node) => _react_doctor_require_typescript_default.isFunctionDeclaration(node) || _react_doctor_require_typescript_default.isFunctionExpression(node) || _react_doctor_require_typescript_default.isArrowFunction(node);
|
|
1720
|
-
const isDefaultExportedComponent = (record, node, componentName) => {
|
|
1721
|
-
if (_react_doctor_require_typescript_default.canHaveModifiers(node) && _react_doctor_require_typescript_default.getModifiers(node)?.some((modifier) => modifier.kind === _react_doctor_require_typescript_default.SyntaxKind.DefaultKeyword)) return true;
|
|
1722
|
-
return record.sourceFile.statements.some((statement) => _react_doctor_require_typescript_default.isExportAssignment(statement) && _react_doctor_require_typescript_default.isIdentifier(statement.expression) && statement.expression.text === componentName);
|
|
1723
|
-
};
|
|
1724
|
-
const findEnclosingComponent = (record, diagnostic) => {
|
|
1725
|
-
if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
|
|
1726
|
-
const lineIndex = diagnostic.line - 1;
|
|
1727
|
-
const lineStarts = record.sourceFile.getLineStarts();
|
|
1728
|
-
const lineStart = lineStarts[lineIndex];
|
|
1729
|
-
if (lineStart === void 0) return null;
|
|
1730
|
-
const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
|
|
1731
|
-
const lineEnd = Math.max(lineStart, nextLineStart - 1);
|
|
1732
|
-
const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
|
|
1733
|
-
const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
|
|
1734
|
-
let component = null;
|
|
1735
|
-
let componentWidth = Number.POSITIVE_INFINITY;
|
|
1736
|
-
const visit = (node) => {
|
|
1737
|
-
if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
|
|
1738
|
-
if (isComponentFunction(node)) {
|
|
1739
|
-
const name = getFunctionName(node);
|
|
1740
|
-
const width = node.end - node.getStart(record.sourceFile);
|
|
1741
|
-
if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
|
|
1742
|
-
component = {
|
|
1743
|
-
isDefaultExport: isDefaultExportedComponent(record, node, name),
|
|
1744
|
-
name,
|
|
1745
|
-
node,
|
|
1746
|
-
record
|
|
1747
|
-
};
|
|
1748
|
-
componentWidth = width;
|
|
1749
|
-
}
|
|
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}`;
|
|
1750
1958
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
|
|
1765
|
-
].includes(componentRecord.absolutePath);
|
|
1766
|
-
};
|
|
1767
|
-
const getComponentTagNames = (record, component) => {
|
|
1768
|
-
if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
|
|
1769
|
-
const tagNames = /* @__PURE__ */ new Set();
|
|
1770
|
-
for (const statement of record.sourceFile.statements) {
|
|
1771
|
-
if (!_react_doctor_require_typescript_default.isImportDeclaration(statement) || !_react_doctor_require_typescript_default.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
|
|
1772
|
-
const importClause = statement.importClause;
|
|
1773
|
-
if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
|
|
1774
|
-
const namedBindings = importClause?.namedBindings;
|
|
1775
|
-
if (namedBindings !== void 0 && _react_doctor_require_typescript_default.isNamedImports(namedBindings)) {
|
|
1776
|
-
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;
|
|
1777
1972
|
}
|
|
1973
|
+
entries.push({
|
|
1974
|
+
statusCode,
|
|
1975
|
+
path: recordPath
|
|
1976
|
+
});
|
|
1778
1977
|
}
|
|
1779
|
-
return
|
|
1780
|
-
};
|
|
1781
|
-
const getTransparentCall = (functionNode) => {
|
|
1782
|
-
if (functionNode.body === void 0) return null;
|
|
1783
|
-
if (!_react_doctor_require_typescript_default.isBlock(functionNode.body)) {
|
|
1784
|
-
const expression = unwrapExpression(functionNode.body);
|
|
1785
|
-
return _react_doctor_require_typescript_default.isCallExpression(expression) ? expression : null;
|
|
1786
|
-
}
|
|
1787
|
-
if (functionNode.body.statements.length !== 1) return null;
|
|
1788
|
-
const statement = functionNode.body.statements[0];
|
|
1789
|
-
if (statement === void 0) return null;
|
|
1790
|
-
const expression = _react_doctor_require_typescript_default.isExpressionStatement(statement) || _react_doctor_require_typescript_default.isReturnStatement(statement) ? statement.expression : void 0;
|
|
1791
|
-
if (expression === void 0) return null;
|
|
1792
|
-
const unwrappedExpression = unwrapExpression(expression);
|
|
1793
|
-
return _react_doctor_require_typescript_default.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
|
|
1978
|
+
return entries;
|
|
1794
1979
|
};
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const argument = call.arguments[parameterIndex];
|
|
1802
|
-
const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
|
|
1803
|
-
if (!_react_doctor_require_typescript_default.isIdentifier(parameter.name) || unwrappedArgument === null || !_react_doctor_require_typescript_default.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
|
|
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
|
-
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (!_react_doctor_require_typescript_default.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
|
|
1810
|
-
const initializer = unwrapExpression(declaration.initializer);
|
|
1811
|
-
if (_react_doctor_require_typescript_default.isArrowFunction(initializer) || _react_doctor_require_typescript_default.isFunctionExpression(initializer)) return initializer;
|
|
1812
|
-
if (_react_doctor_require_typescript_default.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
|
|
1813
|
-
const callback = initializer.arguments[0];
|
|
1814
|
-
if (callback !== void 0 && (_react_doctor_require_typescript_default.isArrowFunction(callback) || _react_doctor_require_typescript_default.isFunctionExpression(callback))) return callback;
|
|
1987
|
+
if (stats.isSymbolicLink()) try {
|
|
1988
|
+
return `link:${fs.readlinkSync(absolutePath)}`;
|
|
1989
|
+
} catch {
|
|
1990
|
+
return null;
|
|
1815
1991
|
}
|
|
1816
|
-
return null;
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
if ((_react_doctor_require_typescript_default.isVariableDeclaration(node) || _react_doctor_require_typescript_default.isParameter(node) || _react_doctor_require_typescript_default.isBindingElement(node) || _react_doctor_require_typescript_default.isFunctionDeclaration(node)) && node.name !== void 0 && _react_doctor_require_typescript_default.isIdentifier(node.name)) return node.name.text;
|
|
1820
|
-
if (_react_doctor_require_typescript_default.isImportClause(node)) return node.name?.text ?? null;
|
|
1821
|
-
if (_react_doctor_require_typescript_default.isImportSpecifier(node) || _react_doctor_require_typescript_default.isNamespaceImport(node)) return node.name.text;
|
|
1822
|
-
return null;
|
|
1992
|
+
if (!stats.isFile()) return null;
|
|
1993
|
+
if (stats.size > 10485760) return null;
|
|
1994
|
+
return hashFileContents(absolutePath);
|
|
1823
1995
|
};
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
+
});
|
|
1830
2010
|
}
|
|
1831
|
-
return
|
|
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
|
+
});
|
|
1832
2016
|
};
|
|
1833
|
-
const
|
|
1834
|
-
const
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
|
|
1840
|
-
const isVisibleDeclaration = _react_doctor_require_typescript_default.isFunctionDeclaration(node) || _react_doctor_require_typescript_default.isParameter(node) || _react_doctor_require_typescript_default.isBindingElement(node) || _react_doctor_require_typescript_default.isImportClause(node) || _react_doctor_require_typescript_default.isImportSpecifier(node) || _react_doctor_require_typescript_default.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
|
|
1841
|
-
if (isVisibleScope && isVisibleDeclaration) candidates.push({
|
|
1842
|
-
declaration: node,
|
|
1843
|
-
scope
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1846
|
-
_react_doctor_require_typescript_default.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
|
|
1847
2023
|
};
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
let nearestScopeWidth = Number.POSITIVE_INFINITY;
|
|
1851
|
-
let isNearestScopeAmbiguous = false;
|
|
1852
|
-
for (const candidate of candidates) {
|
|
1853
|
-
const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
|
|
1854
|
-
if (scopeWidth < nearestScopeWidth) {
|
|
1855
|
-
nearestCandidate = candidate;
|
|
1856
|
-
nearestScopeWidth = scopeWidth;
|
|
1857
|
-
isNearestScopeAmbiguous = false;
|
|
1858
|
-
} else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
|
|
1859
|
-
}
|
|
1860
|
-
if (isNearestScopeAmbiguous) return null;
|
|
1861
|
-
const declaration = nearestCandidate?.declaration;
|
|
1862
|
-
return declaration !== void 0 && (_react_doctor_require_typescript_default.isVariableDeclaration(declaration) || _react_doctor_require_typescript_default.isFunctionDeclaration(declaration)) ? declaration : null;
|
|
2024
|
+
invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
|
|
2025
|
+
return identity;
|
|
1863
2026
|
};
|
|
1864
|
-
const
|
|
1865
|
-
const
|
|
1866
|
-
if (
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
return
|
|
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);
|
|
1870
2033
|
};
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if (initializer.expression === void 0) return null;
|
|
1878
|
-
const expression = unwrapExpression(initializer.expression);
|
|
1879
|
-
return _react_doctor_require_typescript_default.isIdentifier(expression) ? expression : null;
|
|
1880
|
-
}
|
|
1881
|
-
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;
|
|
1882
2040
|
};
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
let callsiteCount = 0;
|
|
1890
|
-
for (const record of listSourceRecords(rootDirectory, state)) {
|
|
1891
|
-
const tagNames = getComponentTagNames(record, component);
|
|
1892
|
-
if (tagNames.size === 0) continue;
|
|
1893
|
-
const visit = (node) => {
|
|
1894
|
-
if (record === component.record && node === component.node) return;
|
|
1895
|
-
if (_react_doctor_require_typescript_default.isJsxOpeningElement(node) || _react_doctor_require_typescript_default.isJsxSelfClosingElement(node)) {
|
|
1896
|
-
const tagName = getJsxTagName(node);
|
|
1897
|
-
if (tagName !== null && tagNames.has(tagName)) {
|
|
1898
|
-
callsiteCount += 1;
|
|
1899
|
-
for (const propName of propNames) {
|
|
1900
|
-
const binding = getHandlerBinding(node, propName);
|
|
1901
|
-
if (binding === null) {
|
|
1902
|
-
unresolvedPropNames.add(propName);
|
|
1903
|
-
continue;
|
|
1904
|
-
}
|
|
1905
|
-
const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
|
|
1906
|
-
bindings.add(resolveHandlerBinding(record, binding));
|
|
1907
|
-
bindingsByPropName.set(propName, bindings);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
_react_doctor_require_typescript_default.forEachChild(node, visit);
|
|
1912
|
-
};
|
|
1913
|
-
visit(record.sourceFile);
|
|
1914
|
-
}
|
|
1915
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
1916
|
-
if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
|
|
1917
|
-
if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
|
|
1918
|
-
const [bindingName] = bindings;
|
|
1919
|
-
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 [];
|
|
1920
2047
|
}
|
|
1921
|
-
state.aliasesByComponent.set(cacheKey, aliases);
|
|
1922
|
-
return aliases;
|
|
1923
|
-
};
|
|
1924
|
-
const normalizeForwardedHandlers = (evidence, aliases) => {
|
|
1925
|
-
let normalizedEvidence = evidence;
|
|
1926
|
-
for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
|
|
1927
|
-
return normalizedEvidence;
|
|
1928
2048
|
};
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
};
|
|
1935
|
-
return (diagnostic) => {
|
|
1936
|
-
const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
|
|
1937
|
-
if (record === null || !Number.isInteger(diagnostic.line)) return null;
|
|
1938
|
-
const sourceLines = record.sourceText.split(/\r?\n/);
|
|
1939
|
-
const startLineIndex = Math.max(0, diagnostic.line - 1);
|
|
1940
|
-
const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
|
|
1941
|
-
const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
|
|
1942
|
-
if (!options.resolveForwardedHandlers) return evidence;
|
|
1943
|
-
const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
|
|
1944
|
-
if (propNames.size === 0) return evidence;
|
|
1945
|
-
const component = findEnclosingComponent(record, diagnostic);
|
|
1946
|
-
if (component === null) return evidence;
|
|
1947
|
-
return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
|
|
1948
|
-
};
|
|
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");
|
|
1949
2054
|
};
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
+
}
|
|
1968
2088
|
};
|
|
1969
|
-
//#endregion
|
|
1970
|
-
//#region src/cli/utils/is-materializable-git-source.ts
|
|
1971
|
-
const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
|
|
1972
|
-
const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
|
|
1973
|
-
//#endregion
|
|
1974
|
-
//#region src/cli/utils/materialize-baseline-files.ts
|
|
1975
|
-
/**
|
|
1976
|
-
* Materializes the base side of the `ref` → worktree diff into
|
|
1977
|
-
* `tempDirectory`, mirroring the project layout plus head's config files so
|
|
1978
|
-
* both sides lint under the same rules. Git rename heuristics are disabled by
|
|
1979
|
-
* the diff planner, so an old path is retained as a base deletion while its
|
|
1980
|
-
* new path is a head addition. Missing head-only additions are expected, and a
|
|
1981
|
-
* base-only deletion that cannot be read has nothing at head to compare
|
|
1982
|
-
* against; only a missing path that the plan says exists on both sides makes
|
|
1983
|
-
* `isComplete` false.
|
|
1984
|
-
*/
|
|
1985
|
-
const materializeBaselineFiles = (input) => runPromise(gen(function* () {
|
|
1986
|
-
const git = yield* Git;
|
|
1987
|
-
const diffPlan = yield* git.baselineDiffPlan({
|
|
1988
|
-
directory: input.directory,
|
|
1989
|
-
ref: input.ref
|
|
1990
|
-
});
|
|
1991
|
-
const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
|
|
1992
|
-
const headFiles = input.headFiles ?? diffPlan?.headFiles;
|
|
1993
|
-
const untrackedFiles = diffPlan?.untrackedFiles ?? [];
|
|
1994
|
-
if (baseFiles === void 0 || headFiles === void 0) return null;
|
|
1995
|
-
const files = [...new Set([...input.files, ...baseFiles])];
|
|
1996
|
-
const tree = yield* materializeSourceTree({
|
|
1997
|
-
directory: input.directory,
|
|
1998
|
-
files,
|
|
1999
|
-
tempDirectory: input.tempDirectory,
|
|
2000
|
-
readContent: (relativePath) => git.showRefContent({
|
|
2001
|
-
directory: input.directory,
|
|
2002
|
-
ref: input.ref,
|
|
2003
|
-
relativePath,
|
|
2004
|
-
options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
|
|
2005
|
-
}).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
|
|
2006
|
-
});
|
|
2007
|
-
const materializedFiles = new Set(tree.materializedFiles);
|
|
2008
|
-
const headFileSet = new Set(headFiles);
|
|
2009
|
-
return {
|
|
2010
|
-
...tree,
|
|
2011
|
-
baseFiles,
|
|
2012
|
-
headFiles,
|
|
2013
|
-
isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
|
|
2014
|
-
untrackedFiles
|
|
2015
|
-
};
|
|
2016
|
-
}).pipe(provide(Git.layerNode)));
|
|
2017
2089
|
/**
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
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.
|
|
2021
2097
|
*/
|
|
2022
|
-
const
|
|
2098
|
+
const resolveProjectIdentity = (projectDirectory) => {
|
|
2099
|
+
try {
|
|
2100
|
+
return fs.realpathSync.native(projectDirectory);
|
|
2101
|
+
} catch {
|
|
2102
|
+
return path.resolve(projectDirectory);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
|
|
2106
|
+
const fileFingerprint = (filePath) => {
|
|
2023
2107
|
try {
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
directory,
|
|
2027
|
-
ref
|
|
2028
|
-
});
|
|
2029
|
-
}).pipe(provide(Git.layerNode)));
|
|
2108
|
+
const stat = fs.statSync(filePath);
|
|
2109
|
+
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
2030
2110
|
} catch {
|
|
2031
2111
|
return null;
|
|
2032
2112
|
}
|
|
2033
2113
|
};
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
|
|
2044
|
-
const runBaselineComparison = async (input) => {
|
|
2045
|
-
const baselineIncludePaths = filterPathsOutsideDirectories({
|
|
2046
|
-
rootDirectory: input.directory,
|
|
2047
|
-
relativePaths: input.options.includePaths,
|
|
2048
|
-
excludedDirectories: input.options.excludedProjectDirectories
|
|
2049
|
-
});
|
|
2050
|
-
const baselineBaseFiles = input.baseFiles ? filterPathsOutsideDirectories({
|
|
2051
|
-
rootDirectory: input.directory,
|
|
2052
|
-
relativePaths: input.baseFiles,
|
|
2053
|
-
excludedDirectories: input.options.excludedProjectDirectories
|
|
2054
|
-
}) : void 0;
|
|
2055
|
-
const baselineHeadFiles = input.headFiles ? filterPathsOutsideDirectories({
|
|
2056
|
-
rootDirectory: input.directory,
|
|
2057
|
-
relativePaths: input.headFiles,
|
|
2058
|
-
excludedDirectories: input.options.excludedProjectDirectories
|
|
2059
|
-
}) : void 0;
|
|
2060
|
-
const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
|
|
2061
|
-
if (remainingBaselineBudgetMs === 0) return null;
|
|
2062
|
-
const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
|
|
2063
|
-
const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
|
|
2064
|
-
let maintainabilitySourceFiles = [];
|
|
2065
|
-
if (input.options.deadCode) try {
|
|
2066
|
-
maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
|
|
2067
|
-
} catch (error) {
|
|
2068
|
-
if (baselineDeadlineSignal?.aborted) return null;
|
|
2069
|
-
throw error;
|
|
2070
|
-
}
|
|
2071
|
-
const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
|
|
2072
|
-
const snapshot = await materializeBaselineFiles({
|
|
2073
|
-
directory: input.directory,
|
|
2074
|
-
ref: input.baselineRef,
|
|
2075
|
-
files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
|
|
2076
|
-
baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
|
|
2077
|
-
headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
|
|
2078
|
-
tempDirectory: temporaryDirectory
|
|
2079
|
-
}).catch((error) => {
|
|
2080
|
-
rmSync(temporaryDirectory, {
|
|
2081
|
-
recursive: true,
|
|
2082
|
-
force: true
|
|
2083
|
-
});
|
|
2084
|
-
throw error;
|
|
2085
|
-
});
|
|
2086
|
-
if (snapshot === null) {
|
|
2087
|
-
rmSync(temporaryDirectory, {
|
|
2088
|
-
recursive: true,
|
|
2089
|
-
force: true
|
|
2090
|
-
});
|
|
2091
|
-
return null;
|
|
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`);
|
|
2092
2123
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
|
|
2124
|
-
const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
|
|
2125
|
-
const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
|
|
2126
|
-
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)));
|
|
2127
|
-
const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
|
|
2128
|
-
const runtimeLayers = buildRuntimeLayers({
|
|
2129
|
-
directory: snapshot.tempDirectory,
|
|
2130
|
-
hasConfigOverride: true,
|
|
2131
|
-
userConfig: input.userConfig,
|
|
2132
|
-
configSourceDirectory: input.configSourceDirectory,
|
|
2133
|
-
projectInfoOverride: input.headProjectInfo,
|
|
2134
|
-
shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
|
|
2135
|
-
shouldRunDeadCode: input.options.deadCode,
|
|
2136
|
-
shouldRunSupplyChain: input.options.supplyChain,
|
|
2137
|
-
shouldComputeScore: false,
|
|
2138
|
-
shouldShowProgressSpinners: false,
|
|
2139
|
-
oxlintConcurrency: input.oxlintRuntime.concurrency,
|
|
2140
|
-
oxlintSpawnSlots: input.oxlintRuntime.spawnSlots
|
|
2141
|
-
});
|
|
2142
|
-
const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
|
|
2143
|
-
directory: snapshot.tempDirectory,
|
|
2144
|
-
includePaths: baseIncludePaths,
|
|
2145
|
-
maintainabilityFocusPaths,
|
|
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,
|
|
2146
2154
|
customRulesOnly: input.options.customRulesOnly,
|
|
2147
2155
|
respectInlineDisables: input.options.respectInlineDisables,
|
|
2148
2156
|
warnings: input.options.warnings,
|
|
2149
2157
|
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
2150
|
-
ignoredTags: input.options.ignoredTags,
|
|
2151
|
-
includedTags: input.options.includedTags,
|
|
2158
|
+
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
2159
|
+
includedTags: [...input.options.includedTags].sort(),
|
|
2152
2160
|
includeTagDefaults: input.options.includeTagDefaults,
|
|
2153
|
-
|
|
2154
|
-
|
|
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,
|
|
2155
2166
|
isCi: input.options.isCi,
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
relativePaths: snapshot.untrackedFiles,
|
|
2175
|
-
excludedDirectories: input.options.excludedProjectDirectories
|
|
2176
|
-
}).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
|
|
2177
|
-
const diagnosticDelta = computeDiagnosticDelta({
|
|
2178
|
-
headDiagnostics: input.headDiagnostics,
|
|
2179
|
-
baseDiagnostics: baseOutput.diagnostics,
|
|
2180
|
-
readHeadLine: createSourceLineReader(input.directory),
|
|
2181
|
-
readBaseLine: createSourceLineReader(snapshot.tempDirectory),
|
|
2182
|
-
readHeadEvidence: createDiagnosticEvidenceReader(input.directory, { resolveForwardedHandlers: true }),
|
|
2183
|
-
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)
|
|
2184
2185
|
});
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
+
};
|
|
2197
2205
|
};
|
|
2198
2206
|
//#endregion
|
|
2199
2207
|
//#region src/cli/utils/scan-result-cache-lifecycle.ts
|
|
2200
|
-
const createScanResultCacheLifecycle = (input) => {
|
|
2201
|
-
const cacheKey = buildScanResultCacheKey({
|
|
2208
|
+
const createScanResultCacheLifecycle = async (input) => {
|
|
2209
|
+
const cacheKey = await buildScanResultCacheKey({
|
|
2202
2210
|
projectDirectory: input.directory,
|
|
2203
2211
|
version: VERSION,
|
|
2204
2212
|
nodeBinaryPath: input.resolvedNodeBinaryPath,
|
|
@@ -2318,7 +2326,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
2318
2326
|
const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
|
|
2319
2327
|
const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
|
|
2320
2328
|
await yieldToEventLoop();
|
|
2321
|
-
const scanResultCacheLifecycle = createScanResultCacheLifecycle({
|
|
2329
|
+
const scanResultCacheLifecycle = await createScanResultCacheLifecycle({
|
|
2322
2330
|
directory,
|
|
2323
2331
|
options,
|
|
2324
2332
|
userConfig,
|
|
@@ -2332,6 +2340,10 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
2332
2340
|
const cachedResult = scanResultCacheLifecycle.replay();
|
|
2333
2341
|
if (cachedResult !== null) return cachedResult;
|
|
2334
2342
|
if (options.lint && resolvedNodeBinaryPath) warmOxlintWorkerPool(resolvedNodeBinaryPath, oxlintRuntime.concurrency);
|
|
2343
|
+
if (options.deadCode && shouldUseMaintainabilityLayer({
|
|
2344
|
+
shouldRunDuplicateJsx: options.deadCode,
|
|
2345
|
+
userConfig
|
|
2346
|
+
})) warmDuplicateJsxWorker();
|
|
2335
2347
|
const shouldShowProgressSpinners = !options.isCiOrCodingAgentEnvironment && !options.silent && !options.scoreOnly && !options.suppressRendering && options.lint && Boolean(resolvedNodeBinaryPath);
|
|
2336
2348
|
const layers = buildRuntimeLayers({
|
|
2337
2349
|
directory,
|
|
@@ -2977,10 +2989,21 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
2977
2989
|
};
|
|
2978
2990
|
//#endregion
|
|
2979
2991
|
//#region src/cli/utils/project-scan-outcome.ts
|
|
2980
|
-
const partitionProjectScanOutcomes = (outcomes) =>
|
|
2981
|
-
completedScans
|
|
2982
|
-
skippedScans
|
|
2983
|
-
|
|
2992
|
+
const partitionProjectScanOutcomes = (outcomes) => {
|
|
2993
|
+
const completedScans = [];
|
|
2994
|
+
const skippedScans = [];
|
|
2995
|
+
for (const outcome of outcomes) {
|
|
2996
|
+
if (outcome.status === "completed") {
|
|
2997
|
+
completedScans.push(outcome.value);
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
3000
|
+
if (outcome.status === "skipped") skippedScans.push(outcome.value);
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
completedScans,
|
|
3004
|
+
skippedScans
|
|
3005
|
+
};
|
|
3006
|
+
};
|
|
2984
3007
|
//#endregion
|
|
2985
3008
|
//#region src/cli/utils/project-manifest-changed.ts
|
|
2986
3009
|
const PACKAGE_JSON = "package.json";
|
|
@@ -3048,5 +3071,5 @@ const shouldFailScanGate = (input) => {
|
|
|
3048
3071
|
//#endregion
|
|
3049
3072
|
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 };
|
|
3050
3073
|
|
|
3051
|
-
//# sourceMappingURL=should-fail-scan-gate-
|
|
3052
|
-
//# debugId=
|
|
3074
|
+
//# sourceMappingURL=should-fail-scan-gate-BC9hJSI-.js.map
|
|
3075
|
+
//# debugId=24fb46c9-b471-5762-a502-488724c08e84
|