react-doctor 0.8.0 → 0.8.1-dev.1839566

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
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]="5cb8a881-e8fc-5561-8b89-ad2c8357acbc")}catch(e){}}();
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]="c24708ed-97a3-59cc-81f7-4cf277ab29f2")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -36242,7 +36242,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36242
36242
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36243
36243
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36244
36244
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36245
- const CORE_PACKAGE_VERSION = "0.8.0";
36245
+ const CORE_PACKAGE_VERSION = "0.8.1";
36246
36246
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36247
36247
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36248
36248
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -38359,6 +38359,9 @@ const SCRIPT_EXTENSION_FRAGMENT = "[cm]?[jt]sx?";
38359
38359
  const STORY_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:stories|story)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
38360
38360
  const TEST_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:test|spec|fixture|fixtures)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
38361
38361
  const TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\//;
38362
+ const AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN = /(?:^|\/)(?:tests|test)\//;
38363
+ const NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN = /\/(?:default|error|forbidden|global-error|layout|loading|not-found|page|route|template|unauthorized)\.[cm]?[jt]sx?$/;
38364
+ const SCRIPT_FILE_SUFFIX_PATTERN = /\.[cm]?[jt]sx?$/;
38362
38365
  const FIXTURE_PROJECT_PATTERN = /\/(?:fixtures|__fixtures__)\//;
38363
38366
  const SOURCE_ROOT_PATTERN = /\/(?:src|app|lib|components|pages|features|modules|packages|apps|frontend|client)\//g;
38364
38367
  const stripAboveSourceRoot = (relativePath) => {
@@ -38369,6 +38372,14 @@ const stripAboveSourceRoot = (relativePath) => {
38369
38372
  if (lastIdx >= 0) return relativePath.slice(lastIdx);
38370
38373
  return relativePath.slice(fixtureMatch.index + fixtureMatch[0].length - 1);
38371
38374
  };
38375
+ const isFrameworkRouteBeforeAmbiguousTestDirectory = (relativePath) => {
38376
+ const testDirectoryMatch = AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN.exec(relativePath);
38377
+ if (testDirectoryMatch === null) return false;
38378
+ let nearestRouteRoot;
38379
+ for (const pathSegment of relativePath.slice(0, testDirectoryMatch.index).split("/")) if (pathSegment === "app" || pathSegment === "pages") nearestRouteRoot = pathSegment;
38380
+ if (nearestRouteRoot === void 0) return false;
38381
+ return nearestRouteRoot === "pages" ? SCRIPT_FILE_SUFFIX_PATTERN.test(relativePath) : NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN.test(relativePath);
38382
+ };
38372
38383
  /**
38373
38384
  * Classifies where a file sits relative to shipped code. A finding in a
38374
38385
  * `.stories.tsx` or `.spec.ts` file never runs in front of users, so
@@ -38385,6 +38396,7 @@ const classifyFileContext = (relativePath) => {
38385
38396
  if (STORY_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "story";
38386
38397
  if (TEST_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "test";
38387
38398
  const scoped = stripAboveSourceRoot(forwardSlashed);
38399
+ if (isFrameworkRouteBeforeAmbiguousTestDirectory(scoped)) return "production";
38388
38400
  return TEST_FILE_DIRECTORY_PATTERN.test(scoped) ? "test" : "production";
38389
38401
  };
38390
38402
  /**
@@ -39244,6 +39256,7 @@ const VALID_RULE_SEVERITIES = [
39244
39256
  "warn",
39245
39257
  "off"
39246
39258
  ];
39259
+ const VALID_INCLUDED_FILE_CONTEXTS = ["test", "story"];
39247
39260
  const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
39248
39261
  const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
39249
39262
  const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
@@ -39278,6 +39291,7 @@ const SURFACE_CONTROL_FIELD_NAMES = [
39278
39291
  "includeRules",
39279
39292
  "excludeRules"
39280
39293
  ];
39294
+ const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
39281
39295
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
39282
39296
  const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39283
39297
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
@@ -39312,6 +39326,15 @@ const validateSurfaceControls = (surface, rawControls) => {
39312
39326
  return;
39313
39327
  }
39314
39328
  const validatedSurfaceControls = {};
39329
+ if (rawControls.includeFileContexts !== void 0) {
39330
+ const qualifiedName = `surfaces.${surface}.includeFileContexts`;
39331
+ const result = validateStringArrayField(qualifiedName, rawControls.includeFileContexts);
39332
+ if (result !== void 0) validatedSurfaceControls.includeFileContexts = result.filter((fileContext) => {
39333
+ if (isIncludedFileContext(fileContext)) return true;
39334
+ warnConfigIssue(`config field "${qualifiedName}" lists "${fileContext}", which is not a non-production file context (expected one of: ${VALID_INCLUDED_FILE_CONTEXTS.join(", ")}); ignoring the entry.`);
39335
+ return false;
39336
+ });
39337
+ }
39315
39338
  for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
39316
39339
  if (rawControls[fieldName] === void 0) continue;
39317
39340
  const qualifiedName = `surfaces.${surface}.${fieldName}`;
@@ -42047,6 +42070,7 @@ const buildResolvedControls = (surface, userControls) => {
42047
42070
  for (const tag of includeTags) excludeTags.delete(tag);
42048
42071
  for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
42049
42072
  return {
42073
+ includeFileContexts: toStringSet(userControls?.includeFileContexts),
42050
42074
  includeTags,
42051
42075
  excludeTags,
42052
42076
  includeCategories: toStringSet(userControls?.includeCategories),
@@ -42062,7 +42086,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
42062
42086
  if (resolved.includeRuleKeys.has(ruleKey)) return true;
42063
42087
  if (resolved.includeCategories.has(category)) return true;
42064
42088
  if (intersects(tags, resolved.includeTags)) return true;
42065
- if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
42089
+ if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure") && !resolved.includeFileContexts.has(diagnostic.fileContext)) return false;
42066
42090
  if (resolved.excludeRuleKeys.has(ruleKey)) return false;
42067
42091
  if (resolved.excludeCategories.has(category)) return false;
42068
42092
  if (intersects(tags, resolved.excludeTags)) return false;
@@ -42310,6 +42334,35 @@ const parseGithubViewerPermission = (stdout) => {
42310
42334
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
42311
42335
  };
42312
42336
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
42337
+ const parseBaselineDiffPlan = (value) => {
42338
+ const entries = splitNullSeparated(value);
42339
+ const baseFiles = /* @__PURE__ */ new Set();
42340
+ const headFiles = /* @__PURE__ */ new Set();
42341
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 2) {
42342
+ const status = entries[entryIndex];
42343
+ const filePath = entries[entryIndex + 1];
42344
+ if (status === void 0 || filePath === void 0 || status.length !== 1) return null;
42345
+ if (status === "A") {
42346
+ headFiles.add(filePath);
42347
+ continue;
42348
+ }
42349
+ if (status === "D") {
42350
+ baseFiles.add(filePath);
42351
+ continue;
42352
+ }
42353
+ if (status === "M" || status === "T") {
42354
+ baseFiles.add(filePath);
42355
+ headFiles.add(filePath);
42356
+ continue;
42357
+ }
42358
+ return null;
42359
+ }
42360
+ return {
42361
+ baseFiles: [...baseFiles],
42362
+ headFiles: [...headFiles],
42363
+ untrackedFiles: []
42364
+ };
42365
+ };
42313
42366
  const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
42314
42367
  /**
42315
42368
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -42512,6 +42565,45 @@ var Git = class Git extends Service()("react-doctor/Git") {
42512
42565
  githubViewerPermission,
42513
42566
  branchExists,
42514
42567
  mergeBase,
42568
+ baselineDiffPlan: (input) => {
42569
+ if (!isSafeGitRevision(input.ref)) return succeed$2(null);
42570
+ return gen(function* () {
42571
+ const unmerged = yield* runGit(input.directory, [
42572
+ "diff",
42573
+ "--no-ext-diff",
42574
+ "-z",
42575
+ "--name-only",
42576
+ "--diff-filter=U",
42577
+ "--relative"
42578
+ ]);
42579
+ if (unmerged.status !== 0 || unmerged.stdout.length > 0) return null;
42580
+ const result = yield* runGit(input.directory, [
42581
+ "diff",
42582
+ "--no-ext-diff",
42583
+ "--no-textconv",
42584
+ "--no-renames",
42585
+ "-z",
42586
+ "--name-status",
42587
+ "--relative",
42588
+ input.ref
42589
+ ]);
42590
+ if (result.status !== 0) return null;
42591
+ const plan = parseBaselineDiffPlan(result.stdout);
42592
+ if (plan === null) return null;
42593
+ const untracked = yield* runGit(input.directory, [
42594
+ "ls-files",
42595
+ "--others",
42596
+ "--exclude-standard",
42597
+ "-z"
42598
+ ]);
42599
+ if (untracked.status !== 0) return null;
42600
+ return {
42601
+ baseFiles: plan.baseFiles,
42602
+ headFiles: plan.headFiles,
42603
+ untrackedFiles: splitNullSeparated(untracked.stdout)
42604
+ };
42605
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.baselineDiffPlan"));
42606
+ },
42515
42607
  diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42516
42608
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42517
42609
  if (explicitBaseBranch !== void 0) {
@@ -42661,6 +42753,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42661
42753
  githubViewerPermission: () => succeed$2(snapshot.githubViewerPermission ?? null),
42662
42754
  branchExists: (_directory, branch) => succeed$2(snapshot.branchExists?.get(branch) ?? false),
42663
42755
  mergeBase: ({ ref }) => succeed$2(snapshot.mergeBase?.get(ref) ?? null),
42756
+ baselineDiffPlan: () => succeed$2(snapshot.baselineDiffPlan ?? null),
42664
42757
  diffSelection: () => succeed$2(snapshot.diffSelection ?? null),
42665
42758
  stagedFilePaths: () => succeed$2(snapshot.stagedFiles ?? []),
42666
42759
  showStagedContent: (_directory, relativePath) => succeed$2(snapshot.stagedContent?.get(relativePath) ?? null),
@@ -46397,12 +46490,19 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
46397
46490
  */
46398
46491
  const materializeSourceTree = (input) => gen(function* () {
46399
46492
  const materializedFiles = [];
46493
+ const unmaterializedFiles = [];
46400
46494
  const resolvedTempDirectory = path.resolve(input.tempDirectory);
46401
46495
  for (const relativePath of input.files) {
46402
46496
  const content = yield* input.readContent(relativePath).pipe(orElseSucceed(() => null));
46403
- if (content === null) continue;
46497
+ if (content === null) {
46498
+ unmaterializedFiles.push(relativePath);
46499
+ continue;
46500
+ }
46404
46501
  const candidateTargetPath = path.resolve(resolvedTempDirectory, relativePath);
46405
- if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) continue;
46502
+ if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) {
46503
+ unmaterializedFiles.push(relativePath);
46504
+ continue;
46505
+ }
46406
46506
  yield* sync(() => {
46407
46507
  fs.mkdirSync(path.dirname(candidateTargetPath), { recursive: true });
46408
46508
  fs.writeFileSync(candidateTargetPath, content);
@@ -46419,6 +46519,7 @@ const materializeSourceTree = (input) => gen(function* () {
46419
46519
  return {
46420
46520
  tempDirectory: input.tempDirectory,
46421
46521
  materializedFiles,
46522
+ unmaterializedFiles,
46422
46523
  cleanup: () => {
46423
46524
  try {
46424
46525
  fs.rmSync(input.tempDirectory, {
@@ -46688,54 +46789,80 @@ const buildSkippedChecks = (input) => {
46688
46789
  skippedCheckReasons
46689
46790
  };
46690
46791
  };
46691
- const fingerprintDiagnostic = (diagnostic, lineText) => {
46792
+ const fingerprintText = (text) => createHash("sha256").update(text).digest("hex");
46793
+ const normalizeEvidence = (evidence) => evidence.replace(/\s+/g, " ").trim();
46794
+ const getDiagnosticMatchKeys = (diagnostic, evidence) => {
46692
46795
  const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
46693
- const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
46694
- return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
46695
- };
46696
- /**
46697
- * Diffs a head scan against a base scan to isolate the diagnostics a change
46698
- * introduced (and count the ones it resolved). Matching is a multiset over a
46699
- * position-independent fingerprint — `(filePath, plugin/rule, hash(flagged
46700
- * line text))` — so inserting lines above an existing issue doesn't make it
46701
- * look new, while a genuinely new occurrence (new line text, or one more of
46702
- * the same) surfaces. Identical repeated findings are matched by count.
46703
- *
46704
- * Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
46705
- * every Accessibility-category finding, plus rules opting in via their
46706
- * `matchByOccurrence` metadata flag) drop the line-text snippet and match by
46707
- * `(filePath, plugin/rule)` occurrence count alone. Their identity is the
46708
- * flagged element, not the line's text, so editing the line (reindentation,
46709
- * prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
46710
- * pre-existing finding as new — while one MORE occurrence of the same rule in
46711
- * the file still surfaces. Expression-level rules keep the line-text snippet:
46712
- * there the flagged expression IS the finding, so changed text means new +
46713
- * fixed.
46714
- *
46715
- * v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
46716
- * content is read at that same path. A file renamed by the change therefore has
46717
- * no base match, so its pre-existing findings are reported as new. This
46718
- * over-reports (never hides a real issue) and is rare; rename-aware base
46719
- * resolution is a follow-up.
46796
+ const messageFingerprint = fingerprintText(`${diagnostic.title ?? ""}\0${diagnostic.message}`);
46797
+ const normalizedEvidence = evidence === null ? "" : normalizeEvidence(evidence);
46798
+ const stableEvidenceKey = normalizedEvidence.length > 0 ? `evidence\0${ruleKey}\0${messageFingerprint}\0${fingerprintText(normalizedEvidence)}` : null;
46799
+ return {
46800
+ stableEvidenceKey,
46801
+ sameFileStableEvidenceKey: stableEvidenceKey === null ? null : `${diagnostic.filePath}\0${stableEvidenceKey}`,
46802
+ sameFileFallbackKey: diagnostic.matchByOccurrence || normalizedEvidence.length === 0 ? `fallback\0${diagnostic.filePath}\0${ruleKey}\0${messageFingerprint}` : null
46803
+ };
46804
+ };
46805
+ const addDiagnosticIndex = (buckets, key, diagnosticIndex) => {
46806
+ if (key === null) return;
46807
+ const diagnosticIndexes = buckets.get(key) ?? [];
46808
+ diagnosticIndexes.push(diagnosticIndex);
46809
+ buckets.set(key, diagnosticIndexes);
46810
+ };
46811
+ const takeMatchingDiagnosticIndex = (buckets, key, matchedDiagnosticIndexes) => {
46812
+ if (key === null) return null;
46813
+ for (const diagnosticIndex of buckets.get(key) ?? []) if (!matchedDiagnosticIndexes.has(diagnosticIndex)) return diagnosticIndex;
46814
+ return null;
46815
+ };
46816
+ const readDiagnosticEvidence = (diagnostic, readEvidence, readLine) => readEvidence?.(diagnostic) ?? readLine(diagnostic.filePath, diagnostic.line);
46817
+ const buildMatchCandidates = (diagnostics, readEvidence, readLine) => diagnostics.map((diagnostic, diagnosticIndex) => ({
46818
+ diagnosticIndex,
46819
+ ...getDiagnosticMatchKeys(diagnostic, readDiagnosticEvidence(diagnostic, readEvidence, readLine))
46820
+ }));
46821
+ /**
46822
+ * Diffs a head scan against a base scan using a multiset of construct-level
46823
+ * evidence. Stable identities combine plugin/rule, the diagnostic message,
46824
+ * and normalized diagnosed source, so unchanged findings can move across
46825
+ * files while changed constructs or messages remain new. Cardinality is
46826
+ * retained for identical findings. Diagnostics explicitly marked
46827
+ * `matchByOccurrence` may fall back to same-file plugin/rule/message matching
46828
+ * after same-file strict evidence matching. Cross-file evidence matching runs
46829
+ * last so a copy cannot consume a reformatted local occurrence. Unreadable
46830
+ * evidence uses the same conservative fallback rather than matching across
46831
+ * files without proof.
46720
46832
  */
46721
46833
  const computeDiagnosticDelta = (input) => {
46722
- const unmatchedBaseByFingerprint = /* @__PURE__ */ new Map();
46723
- for (const diagnostic of input.baseDiagnostics) {
46724
- const key = fingerprintDiagnostic(diagnostic, input.readBaseLine(diagnostic.filePath, diagnostic.line));
46725
- unmatchedBaseByFingerprint.set(key, (unmatchedBaseByFingerprint.get(key) ?? 0) + 1);
46726
- }
46727
- const newDiagnostics = [];
46728
- for (const diagnostic of input.headDiagnostics) {
46729
- const key = fingerprintDiagnostic(diagnostic, input.readHeadLine(diagnostic.filePath, diagnostic.line));
46730
- const availableMatches = unmatchedBaseByFingerprint.get(key) ?? 0;
46731
- if (availableMatches > 0) unmatchedBaseByFingerprint.set(key, availableMatches - 1);
46732
- else newDiagnostics.push(diagnostic);
46733
- }
46734
- let fixedCount = 0;
46735
- for (const remaining of unmatchedBaseByFingerprint.values()) fixedCount += remaining;
46834
+ const baseCandidates = buildMatchCandidates(input.baseDiagnostics, input.readBaseEvidence, input.readBaseLine);
46835
+ const headCandidates = buildMatchCandidates(input.headDiagnostics, input.readHeadEvidence, input.readHeadLine);
46836
+ const baseByStableEvidence = /* @__PURE__ */ new Map();
46837
+ const baseBySameFileStableEvidence = /* @__PURE__ */ new Map();
46838
+ const baseBySameFileFallback = /* @__PURE__ */ new Map();
46839
+ for (const candidate of baseCandidates) {
46840
+ addDiagnosticIndex(baseByStableEvidence, candidate.stableEvidenceKey, candidate.diagnosticIndex);
46841
+ addDiagnosticIndex(baseBySameFileStableEvidence, candidate.sameFileStableEvidenceKey, candidate.diagnosticIndex);
46842
+ addDiagnosticIndex(baseBySameFileFallback, candidate.sameFileFallbackKey, candidate.diagnosticIndex);
46843
+ }
46844
+ const matchedHeadDiagnosticIndexes = /* @__PURE__ */ new Set();
46845
+ const matchedBaseDiagnosticIndexes = /* @__PURE__ */ new Set();
46846
+ const matchCandidates = (baseBuckets, getKey, onMatch) => {
46847
+ for (const candidate of headCandidates) {
46848
+ if (matchedHeadDiagnosticIndexes.has(candidate.diagnosticIndex)) continue;
46849
+ const baseDiagnosticIndex = takeMatchingDiagnosticIndex(baseBuckets, getKey(candidate), matchedBaseDiagnosticIndexes);
46850
+ if (baseDiagnosticIndex === null) continue;
46851
+ matchedHeadDiagnosticIndexes.add(candidate.diagnosticIndex);
46852
+ matchedBaseDiagnosticIndexes.add(baseDiagnosticIndex);
46853
+ onMatch?.(candidate.diagnosticIndex, baseDiagnosticIndex);
46854
+ }
46855
+ };
46856
+ matchCandidates(baseBySameFileStableEvidence, (candidate) => candidate.sameFileStableEvidenceKey);
46857
+ matchCandidates(baseBySameFileFallback, (candidate) => candidate.sameFileFallbackKey);
46858
+ let crossFileMatchCount = 0;
46859
+ matchCandidates(baseByStableEvidence, (candidate) => candidate.stableEvidenceKey, (head, base) => {
46860
+ if (input.headDiagnostics[head]?.filePath !== input.baseDiagnostics[base]?.filePath) crossFileMatchCount += 1;
46861
+ });
46736
46862
  return {
46737
- newDiagnostics,
46738
- fixedCount
46863
+ newDiagnostics: input.headDiagnostics.filter((_diagnostic, diagnosticIndex) => !matchedHeadDiagnosticIndexes.has(diagnosticIndex)),
46864
+ fixedCount: input.baseDiagnostics.length - matchedBaseDiagnosticIndexes.size,
46865
+ crossFileMatchCount
46739
46866
  };
46740
46867
  };
46741
46868
  /**
@@ -46767,6 +46894,12 @@ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runProm
46767
46894
  };
46768
46895
  }).pipe(provide(Git.layerNode)));
46769
46896
  const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
46897
+ const getBaselineDiffPlan = (directory, ref) => runPromise(gen(function* () {
46898
+ return yield* (yield* Git).baselineDiffPlan({
46899
+ directory,
46900
+ ref
46901
+ });
46902
+ }).pipe(provide(Git.layerNode)));
46770
46903
  /**
46771
46904
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
46772
46905
  * `files` with `--unified=0` against `baseRef` (or the index when `cached`),
@@ -47223,7 +47356,7 @@ const makeNoopConsole = () => ({
47223
47356
  });
47224
47357
  //#endregion
47225
47358
  //#region src/cli/utils/version.ts
47226
- const VERSION = "0.8.0";
47359
+ const VERSION = "0.8.1-dev.1839566";
47227
47360
  //#endregion
47228
47361
  //#region src/cli/utils/json-mode.ts
47229
47362
  let context = null;
@@ -47575,13 +47708,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47575
47708
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47576
47709
  * standard `SENTRY_RELEASE` override.
47577
47710
  */
47578
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.0`;
47711
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.1-dev.1839566`;
47579
47712
  /**
47580
47713
  * Deployment environment shown in Sentry's environment filter. Defaults to
47581
47714
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47582
47715
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47583
47716
  */
47584
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.0") ? "development" : "production");
47717
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.1-dev.1839566") ? "development" : "production");
47585
47718
  /**
47586
47719
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47587
47720
  * (set to `0` to disable tracing) and falls back to
@@ -60204,6 +60337,7 @@ const buildOutcomeAttributes = (input) => {
60204
60337
  new: summary.totalDiagnosticCount,
60205
60338
  fixed: result.baselineDelta.fixedCount,
60206
60339
  baseTotal: result.baselineDelta.baseTotalCount,
60340
+ crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
60207
60341
  degraded: false
60208
60342
  }));
60209
60343
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -60317,28 +60451,49 @@ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
60317
60451
  return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
60318
60452
  };
60319
60453
  //#endregion
60454
+ //#region src/cli/utils/is-materializable-git-source.ts
60455
+ const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
60456
+ const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
60457
+ //#endregion
60320
60458
  //#region src/cli/utils/materialize-baseline-files.ts
60321
60459
  /**
60322
- * Materializes the `ref` version of `files` into `tempDirectory` (via
60323
- * `git show <ref>:<path>`), mirroring the project layout + head's config files
60324
- * so the base lints under the same rules as head. Reuses the shared,
60325
- * zip-slip-guarded `materializeSourceTree`. Per-file read misses (e.g. a file
60326
- * the PR added, absent at base) are skipped, which is exactly what we want:
60327
- * a brand-new file has no base counterpart, so all its findings are "new".
60460
+ * Materializes the base side of the `ref` → worktree diff into
60461
+ * `tempDirectory`, mirroring the project layout plus head's config files so
60462
+ * both sides lint under the same rules. Git rename heuristics are disabled by
60463
+ * the diff planner, so an old path is retained as a base deletion while its
60464
+ * new path is a head addition. Missing head-only additions are expected;
60465
+ * missing paths that the plan says must exist at base make `isComplete` false.
60328
60466
  */
60329
60467
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
60330
60468
  const git = yield* Git;
60331
- return yield* materializeSourceTree({
60469
+ const diffPlan = yield* git.baselineDiffPlan({
60332
60470
  directory: input.directory,
60333
- files: input.files,
60471
+ ref: input.ref
60472
+ });
60473
+ const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
60474
+ const headFiles = input.headFiles ?? diffPlan?.headFiles;
60475
+ const untrackedFiles = diffPlan?.untrackedFiles ?? [];
60476
+ if (baseFiles === void 0 || headFiles === void 0) return null;
60477
+ const files = [...new Set([...input.files, ...baseFiles])];
60478
+ const tree = yield* materializeSourceTree({
60479
+ directory: input.directory,
60480
+ files,
60334
60481
  tempDirectory: input.tempDirectory,
60335
60482
  readContent: (relativePath) => git.showRefContent({
60336
60483
  directory: input.directory,
60337
60484
  ref: input.ref,
60338
60485
  relativePath,
60339
60486
  options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
60340
- })
60487
+ }).pipe(map$3((content) => content !== null && isMaterializableGitSource(content) ? content : null))
60341
60488
  });
60489
+ const materializedFiles = new Set(tree.materializedFiles);
60490
+ return {
60491
+ ...tree,
60492
+ baseFiles,
60493
+ headFiles,
60494
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
60495
+ untrackedFiles
60496
+ };
60342
60497
  }).pipe(provide(Git.layerNode)));
60343
60498
  /**
60344
60499
  * Resolves the commit a baseline scan should read base content from — the
@@ -60361,8 +60516,8 @@ const resolveMergeBaseRef = async (directory, ref) => {
60361
60516
  //#region src/cli/utils/read-source-line.ts
60362
60517
  /**
60363
60518
  * Builds a cached `(filePath, line) -> source text` reader rooted at
60364
- * `rootDirectory`, used to fingerprint a diagnostic by the content of its
60365
- * flagged line (see `computeDiagnosticDelta`). Wraps core's
60519
+ * `rootDirectory`, used as the fallback when construct-level diagnostic
60520
+ * evidence cannot be read. Wraps core's
60366
60521
  * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
60367
60522
  * lookup; unreadable files / out-of-range lines return `null`.
60368
60523
  */
@@ -60377,6 +60532,338 @@ const createSourceLineReader = (rootDirectory) => {
60377
60532
  };
60378
60533
  };
60379
60534
  //#endregion
60535
+ //#region src/cli/utils/read-diagnostic-evidence.ts
60536
+ const SOURCE_EXTENSIONS = [
60537
+ ".ts",
60538
+ ".tsx",
60539
+ ".js",
60540
+ ".jsx",
60541
+ ".mts",
60542
+ ".cts",
60543
+ ".mjs",
60544
+ ".cjs"
60545
+ ];
60546
+ const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
60547
+ const getScriptKind = (filePath) => {
60548
+ const extension = path.extname(filePath).toLowerCase();
60549
+ if (extension === ".tsx") return ts.ScriptKind.TSX;
60550
+ if (extension === ".jsx") return ts.ScriptKind.JSX;
60551
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return ts.ScriptKind.JS;
60552
+ return ts.ScriptKind.TS;
60553
+ };
60554
+ const resolveSafeSourcePath = (rootDirectory, filePath) => {
60555
+ try {
60556
+ const resolvedRootDirectory = fs.realpathSync(rootDirectory);
60557
+ const absolutePath = path.resolve(resolvedRootDirectory, filePath);
60558
+ const relativePath = path.relative(resolvedRootDirectory, absolutePath);
60559
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
60560
+ const stats = fs.lstatSync(absolutePath);
60561
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
60562
+ const realPath = fs.realpathSync(absolutePath);
60563
+ const realRelativePath = path.relative(resolvedRootDirectory, realPath);
60564
+ return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
60565
+ } catch {
60566
+ return null;
60567
+ }
60568
+ };
60569
+ const readSourceRecord = (rootDirectory, filePath, state) => {
60570
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
60571
+ if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
60572
+ const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
60573
+ if (absolutePath === null) {
60574
+ state.recordByFilePath.set(normalizedFilePath, null);
60575
+ return null;
60576
+ }
60577
+ try {
60578
+ const sourceText = fs.readFileSync(absolutePath, "utf-8");
60579
+ const record = {
60580
+ absolutePath,
60581
+ filePath: normalizedFilePath,
60582
+ sourceFile: ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
60583
+ sourceText
60584
+ };
60585
+ state.recordByFilePath.set(normalizedFilePath, record);
60586
+ return record;
60587
+ } catch {
60588
+ state.recordByFilePath.set(normalizedFilePath, null);
60589
+ return null;
60590
+ }
60591
+ };
60592
+ const listSourceRecords = (rootDirectory, state) => {
60593
+ if (state.sourceRecords !== null) return state.sourceRecords;
60594
+ state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
60595
+ const record = readSourceRecord(rootDirectory, filePath, state);
60596
+ return record === null ? [] : [record];
60597
+ });
60598
+ return state.sourceRecords;
60599
+ };
60600
+ const unwrapExpression = (expression) => {
60601
+ let currentExpression = expression;
60602
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
60603
+ return currentExpression;
60604
+ };
60605
+ const getCalleeName = (expression) => {
60606
+ const unwrappedExpression = unwrapExpression(expression);
60607
+ if (ts.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
60608
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
60609
+ return null;
60610
+ };
60611
+ const isComponentWrapperCall = (node) => {
60612
+ const wrapperName = getCalleeName(node.expression);
60613
+ return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
60614
+ };
60615
+ const getFunctionName = (node) => {
60616
+ if (ts.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
60617
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
60618
+ let componentExpression = node;
60619
+ while (true) {
60620
+ const parent = componentExpression.parent;
60621
+ if (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) {
60622
+ componentExpression = parent;
60623
+ continue;
60624
+ }
60625
+ if (ts.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
60626
+ componentExpression = parent;
60627
+ continue;
60628
+ }
60629
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
60630
+ return node.name?.text ?? null;
60631
+ }
60632
+ }
60633
+ return null;
60634
+ };
60635
+ const isComponentFunction = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
60636
+ const isDefaultExportedComponent = (record, node, componentName) => {
60637
+ if (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true;
60638
+ return record.sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression) && statement.expression.text === componentName);
60639
+ };
60640
+ const findEnclosingComponent = (record, diagnostic) => {
60641
+ if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
60642
+ const lineIndex = diagnostic.line - 1;
60643
+ const lineStarts = record.sourceFile.getLineStarts();
60644
+ const lineStart = lineStarts[lineIndex];
60645
+ if (lineStart === void 0) return null;
60646
+ const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
60647
+ const lineEnd = Math.max(lineStart, nextLineStart - 1);
60648
+ const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
60649
+ const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
60650
+ let component = null;
60651
+ let componentWidth = Number.POSITIVE_INFINITY;
60652
+ const visit = (node) => {
60653
+ if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
60654
+ if (isComponentFunction(node)) {
60655
+ const name = getFunctionName(node);
60656
+ const width = node.end - node.getStart(record.sourceFile);
60657
+ if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
60658
+ component = {
60659
+ isDefaultExport: isDefaultExportedComponent(record, node, name),
60660
+ name,
60661
+ node,
60662
+ record
60663
+ };
60664
+ componentWidth = width;
60665
+ }
60666
+ }
60667
+ ts.forEachChild(node, visit);
60668
+ };
60669
+ visit(record.sourceFile);
60670
+ return component;
60671
+ };
60672
+ const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
60673
+ if (!moduleSpecifier.startsWith(".")) return false;
60674
+ const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
60675
+ const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
60676
+ const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
60677
+ return [
60678
+ unresolvedPath,
60679
+ ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
60680
+ ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
60681
+ ].includes(componentRecord.absolutePath);
60682
+ };
60683
+ const getComponentTagNames = (record, component) => {
60684
+ if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
60685
+ const tagNames = /* @__PURE__ */ new Set();
60686
+ for (const statement of record.sourceFile.statements) {
60687
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
60688
+ const importClause = statement.importClause;
60689
+ if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
60690
+ const namedBindings = importClause?.namedBindings;
60691
+ if (namedBindings !== void 0 && ts.isNamedImports(namedBindings)) {
60692
+ for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
60693
+ }
60694
+ }
60695
+ return tagNames;
60696
+ };
60697
+ const getTransparentCall = (functionNode) => {
60698
+ if (functionNode.body === void 0) return null;
60699
+ if (!ts.isBlock(functionNode.body)) {
60700
+ const expression = unwrapExpression(functionNode.body);
60701
+ return ts.isCallExpression(expression) ? expression : null;
60702
+ }
60703
+ if (functionNode.body.statements.length !== 1) return null;
60704
+ const statement = functionNode.body.statements[0];
60705
+ if (statement === void 0) return null;
60706
+ const expression = ts.isExpressionStatement(statement) || ts.isReturnStatement(statement) ? statement.expression : void 0;
60707
+ if (expression === void 0) return null;
60708
+ const unwrappedExpression = unwrapExpression(expression);
60709
+ return ts.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
60710
+ };
60711
+ const resolveTransparentTarget = (functionNode) => {
60712
+ const call = getTransparentCall(functionNode);
60713
+ if (call === null) return null;
60714
+ const target = unwrapExpression(call.expression);
60715
+ if (!ts.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
60716
+ for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
60717
+ const argument = call.arguments[parameterIndex];
60718
+ const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
60719
+ if (!ts.isIdentifier(parameter.name) || unwrappedArgument === null || !ts.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
60720
+ }
60721
+ return target.text;
60722
+ };
60723
+ const getForwardingFunction = (declaration) => {
60724
+ if (ts.isFunctionDeclaration(declaration)) return declaration;
60725
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
60726
+ const initializer = unwrapExpression(declaration.initializer);
60727
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer;
60728
+ if (ts.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
60729
+ const callback = initializer.arguments[0];
60730
+ if (callback !== void 0 && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return callback;
60731
+ }
60732
+ return null;
60733
+ };
60734
+ const getDeclaredBindingName = (node) => {
60735
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isFunctionDeclaration(node)) && node.name !== void 0 && ts.isIdentifier(node.name)) return node.name.text;
60736
+ if (ts.isImportClause(node)) return node.name?.text ?? null;
60737
+ if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) return node.name.text;
60738
+ return null;
60739
+ };
60740
+ const findBindingScope = (node) => {
60741
+ let currentNode = node;
60742
+ while (currentNode !== void 0) {
60743
+ if (ts.isParameter(currentNode) && ts.isFunctionLike(currentNode.parent)) return currentNode.parent;
60744
+ if (ts.isBlock(currentNode) || ts.isModuleBlock(currentNode) || ts.isSourceFile(currentNode)) return currentNode;
60745
+ currentNode = currentNode.parent;
60746
+ }
60747
+ return node.getSourceFile();
60748
+ };
60749
+ const findBindingDeclaration = (record, binding) => {
60750
+ const usagePosition = binding.getStart(record.sourceFile);
60751
+ const candidates = [];
60752
+ const visit = (node) => {
60753
+ if (getDeclaredBindingName(node) === binding.text) {
60754
+ const scope = findBindingScope(node);
60755
+ const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
60756
+ const isVisibleDeclaration = ts.isFunctionDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isImportClause(node) || ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
60757
+ if (isVisibleScope && isVisibleDeclaration) candidates.push({
60758
+ declaration: node,
60759
+ scope
60760
+ });
60761
+ }
60762
+ ts.forEachChild(node, visit);
60763
+ };
60764
+ visit(record.sourceFile);
60765
+ let nearestCandidate = null;
60766
+ let nearestScopeWidth = Number.POSITIVE_INFINITY;
60767
+ let isNearestScopeAmbiguous = false;
60768
+ for (const candidate of candidates) {
60769
+ const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
60770
+ if (scopeWidth < nearestScopeWidth) {
60771
+ nearestCandidate = candidate;
60772
+ nearestScopeWidth = scopeWidth;
60773
+ isNearestScopeAmbiguous = false;
60774
+ } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
60775
+ }
60776
+ if (isNearestScopeAmbiguous) return null;
60777
+ const declaration = nearestCandidate?.declaration;
60778
+ return declaration !== void 0 && (ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration)) ? declaration : null;
60779
+ };
60780
+ const resolveHandlerBinding = (record, binding) => {
60781
+ const declaration = findBindingDeclaration(record, binding);
60782
+ if (declaration === null) return binding.text;
60783
+ const forwardingFunction = getForwardingFunction(declaration);
60784
+ const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
60785
+ return target === null || target === binding.text ? binding.text : target;
60786
+ };
60787
+ const getJsxTagName = (node) => ts.isIdentifier(node.tagName) ? node.tagName.text : null;
60788
+ const getHandlerBinding = (node, propName) => {
60789
+ for (const property of node.attributes.properties) {
60790
+ if (!ts.isJsxAttribute(property) || property.name.getText() !== propName) continue;
60791
+ const initializer = property.initializer;
60792
+ if (initializer === void 0 || !ts.isJsxExpression(initializer)) return null;
60793
+ if (initializer.expression === void 0) return null;
60794
+ const expression = unwrapExpression(initializer.expression);
60795
+ return ts.isIdentifier(expression) ? expression : null;
60796
+ }
60797
+ return null;
60798
+ };
60799
+ const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
60800
+ const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
60801
+ const cachedAliases = state.aliasesByComponent.get(cacheKey);
60802
+ if (cachedAliases !== void 0) return cachedAliases;
60803
+ const bindingsByPropName = /* @__PURE__ */ new Map();
60804
+ const unresolvedPropNames = /* @__PURE__ */ new Set();
60805
+ let callsiteCount = 0;
60806
+ for (const record of listSourceRecords(rootDirectory, state)) {
60807
+ const tagNames = getComponentTagNames(record, component);
60808
+ if (tagNames.size === 0) continue;
60809
+ const visit = (node) => {
60810
+ if (record === component.record && node === component.node) return;
60811
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
60812
+ const tagName = getJsxTagName(node);
60813
+ if (tagName !== null && tagNames.has(tagName)) {
60814
+ callsiteCount += 1;
60815
+ for (const propName of propNames) {
60816
+ const binding = getHandlerBinding(node, propName);
60817
+ if (binding === null) {
60818
+ unresolvedPropNames.add(propName);
60819
+ continue;
60820
+ }
60821
+ const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
60822
+ bindings.add(resolveHandlerBinding(record, binding));
60823
+ bindingsByPropName.set(propName, bindings);
60824
+ }
60825
+ }
60826
+ }
60827
+ ts.forEachChild(node, visit);
60828
+ };
60829
+ visit(record.sourceFile);
60830
+ }
60831
+ const aliases = /* @__PURE__ */ new Map();
60832
+ if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
60833
+ if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
60834
+ const [bindingName] = bindings;
60835
+ if (bindingName !== void 0) aliases.set(propName, bindingName);
60836
+ }
60837
+ state.aliasesByComponent.set(cacheKey, aliases);
60838
+ return aliases;
60839
+ };
60840
+ const normalizeForwardedHandlers = (evidence, aliases) => {
60841
+ let normalizedEvidence = evidence;
60842
+ for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
60843
+ return normalizedEvidence;
60844
+ };
60845
+ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
60846
+ const state = {
60847
+ aliasesByComponent: /* @__PURE__ */ new Map(),
60848
+ recordByFilePath: /* @__PURE__ */ new Map(),
60849
+ sourceRecords: null
60850
+ };
60851
+ return (diagnostic) => {
60852
+ const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
60853
+ if (record === null || !Number.isInteger(diagnostic.line)) return null;
60854
+ const sourceLines = record.sourceText.split(/\r?\n/);
60855
+ const startLineIndex = Math.max(0, diagnostic.line - 1);
60856
+ const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
60857
+ const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
60858
+ if (!options.resolveForwardedHandlers) return evidence;
60859
+ const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
60860
+ if (propNames.size === 0) return evidence;
60861
+ const component = findEnclosingComponent(record, diagnostic);
60862
+ if (component === null) return evidence;
60863
+ return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
60864
+ };
60865
+ };
60866
+ //#endregion
60380
60867
  //#region src/cli/utils/build-no-score-message.ts
60381
60868
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
60382
60869
  const buildNoScoreMessage = (isScoreDisabled) => {
@@ -61620,6 +62107,8 @@ const runBaselineComparison = async (params) => {
61620
62107
  directory: params.directory,
61621
62108
  ref: params.baselineRef,
61622
62109
  files: params.options.includePaths,
62110
+ baseFiles: params.baseFiles,
62111
+ headFiles: params.headFiles,
61623
62112
  tempDirectory
61624
62113
  }).catch((error) => {
61625
62114
  rmSync(tempDirectory, {
@@ -61628,7 +62117,24 @@ const runBaselineComparison = async (params) => {
61628
62117
  });
61629
62118
  throw error;
61630
62119
  });
62120
+ if (snapshot === null) {
62121
+ rmSync(tempDirectory, {
62122
+ recursive: true,
62123
+ force: true
62124
+ });
62125
+ return null;
62126
+ }
61631
62127
  try {
62128
+ if (!snapshot.isComplete) return null;
62129
+ const analyzedHeadFiles = new Set(params.headAnalyzedFiles.map(toForwardSlashes));
62130
+ const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
62131
+ const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
62132
+ const expectedHeadFiles = new Set(trackedHeadFiles);
62133
+ for (const filePath of params.options.includePaths) {
62134
+ const normalizedFilePath = toForwardSlashes(filePath);
62135
+ if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
62136
+ }
62137
+ if (filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
61632
62138
  const baseLayers = buildRuntimeLayers({
61633
62139
  directory: snapshot.tempDirectory,
61634
62140
  hasConfigOverride: true,
@@ -61644,7 +62150,7 @@ const runBaselineComparison = async (params) => {
61644
62150
  });
61645
62151
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
61646
62152
  directory: snapshot.tempDirectory,
61647
- includePaths: params.options.includePaths,
62153
+ includePaths: snapshot.materializedFiles,
61648
62154
  customRulesOnly: params.options.customRulesOnly,
61649
62155
  respectInlineDisables: params.options.respectInlineDisables,
61650
62156
  warnings: params.options.warnings,
@@ -61661,18 +62167,22 @@ const runBaselineComparison = async (params) => {
61661
62167
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
61662
62168
  }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
61663
62169
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
62170
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
61664
62171
  const delta = computeDiagnosticDelta({
61665
62172
  headDiagnostics: params.headDiagnostics,
61666
62173
  baseDiagnostics: baseOutput.diagnostics,
61667
62174
  readHeadLine: createSourceLineReader(params.directory),
61668
- readBaseLine: createSourceLineReader(snapshot.tempDirectory)
62175
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
62176
+ readHeadEvidence: createDiagnosticEvidenceReader(params.directory, { resolveForwardedHandlers: true }),
62177
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
61669
62178
  });
61670
62179
  return {
61671
62180
  displayDiagnostics: delta.newDiagnostics,
61672
62181
  baselineDelta: {
61673
62182
  baseRef: params.baselineRef,
61674
- fixedCount: delta.fixedCount,
61675
- baseTotalCount: baseOutput.diagnostics.length
62183
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : delta.fixedCount,
62184
+ baseTotalCount: baseOutput.diagnostics.length,
62185
+ crossFileMatchCount: delta.crossFileMatchCount
61676
62186
  }
61677
62187
  };
61678
62188
  } finally {
@@ -61779,6 +62289,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61779
62289
  headDiagnostics: output.diagnostics,
61780
62290
  resolvedNodeBinaryPath,
61781
62291
  baselineRef: options.baseline.ref,
62292
+ baseFiles: options.baseline.baseFiles,
62293
+ headFiles: options.baseline.headFiles,
62294
+ headAnalyzedFiles: output.analyzedFiles,
61782
62295
  deadlineEpochMs
61783
62296
  });
61784
62297
  if (comparison) {
@@ -64063,20 +64576,25 @@ const resolveProjectRelativeDirectory = (rootDirectory, projectDirectory) => {
64063
64576
  return relativeProjectDirectory;
64064
64577
  };
64065
64578
  //#endregion
64066
- //#region src/cli/utils/resolve-project-diff-include-paths.ts
64067
- const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64579
+ //#region src/cli/utils/resolve-project-source-file-paths.ts
64580
+ const resolveProjectSourceFilePaths = (rootDirectory, projectDirectory, filePaths) => {
64068
64581
  const relativeProjectDirectory = resolveProjectRelativeDirectory(rootDirectory, projectDirectory);
64069
64582
  if (relativeProjectDirectory === null) return [];
64070
- const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
64071
- if (relativeProjectDirectory.length === 0) return changedSourceFiles;
64583
+ const sourceFilePaths = filterSourceFiles([...filePaths]);
64584
+ if (relativeProjectDirectory.length === 0) return sourceFilePaths;
64072
64585
  const projectPrefix = `${relativeProjectDirectory}/`;
64073
- return changedSourceFiles.flatMap((filePath) => {
64586
+ return sourceFilePaths.flatMap((filePath) => {
64074
64587
  const normalizedFilePath = toForwardSlashes(filePath);
64075
64588
  if (!normalizedFilePath.startsWith(projectPrefix)) return [];
64076
64589
  const projectRelativePath = normalizedFilePath.slice(projectPrefix.length);
64077
64590
  return projectRelativePath.length > 0 ? [projectRelativePath] : [];
64078
64591
  });
64079
64592
  };
64593
+ //#endregion
64594
+ //#region src/cli/utils/resolve-project-diff-include-paths.ts
64595
+ const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64596
+ return resolveProjectSourceFilePaths(rootDirectory, projectDirectory, diffInfo.changedFiles);
64597
+ };
64080
64598
  /**
64081
64599
  * Re-keys repo-relative changed line ranges (from `getChangedLineRanges`) to
64082
64600
  * the scanned project, mirroring `resolveProjectDiffIncludePaths`'s boundary
@@ -64615,6 +65133,7 @@ const inspectAction = async (directory, flags) => {
64615
65133
  const isDiffMode = scope !== "full";
64616
65134
  const comparisonBaseRef = isDiffMode && diffInfo && !diffInfo.isCurrentChanges ? diffInfo.baseSha ? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseSha) : diffInfo.diffBaseRef ?? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseBranch) : null;
64617
65135
  const baselineRef = scope === "changed" ? comparisonBaseRef : null;
65136
+ const baselineDiffPlan = baselineRef === null ? null : await getBaselineDiffPlan(resolvedDirectory, baselineRef);
64618
65137
  const linesBaseRef = diffInfo?.isCurrentChanges ? "HEAD" : comparisonBaseRef;
64619
65138
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64620
65139
  directory: resolvedDirectory,
@@ -64645,10 +65164,13 @@ const inspectAction = async (directory, flags) => {
64645
65164
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
64646
65165
  let includePaths;
64647
65166
  let supplyChainManifestChanged = false;
65167
+ const projectBaselineBaseFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.baseFiles);
65168
+ const projectBaselineHeadFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.headFiles);
64648
65169
  if (isDiffMode) {
64649
65170
  const changedSourceFiles = diffInfo === null ? [] : resolveProjectDiffIncludePaths(resolvedDirectory, scanDirectory, diffInfo);
64650
65171
  supplyChainManifestChanged = supplyChainEnabled && diffInfo !== null && projectManifestChanged(resolvedDirectory, scanDirectory, diffInfo);
64651
- if (changedSourceFiles.length === 0 && !supplyChainManifestChanged) {
65172
+ const hasBaselineOnlyFiles = (projectBaselineBaseFiles?.length ?? 0) > 0;
65173
+ if (changedSourceFiles.length === 0 && !supplyChainManifestChanged && !hasBaselineOnlyFiles) {
64652
65174
  if (!isQuiet) {
64653
65175
  cliLogger.dim(`No changed source files in ${scanDirectory}, skipping.`);
64654
65176
  cliLogger.break();
@@ -64656,6 +65178,7 @@ const inspectAction = async (directory, flags) => {
64656
65178
  return null;
64657
65179
  }
64658
65180
  includePaths = [...changedSourceFiles];
65181
+ if (includePaths.length === 0 && hasBaselineOnlyFiles) includePaths.push(...projectBaselineBaseFiles ?? []);
64659
65182
  if (supplyChainManifestChanged) includePaths.push("package.json");
64660
65183
  }
64661
65184
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
@@ -64667,7 +65190,11 @@ const inspectAction = async (directory, flags) => {
64667
65190
  configSourceDirectory: projectConfigSourceDirectory ?? void 0,
64668
65191
  suppressRendering: isMultiProject,
64669
65192
  concurrentScan: isMultiProject,
64670
- baseline: baselineRef ? { ref: baselineRef } : void 0,
65193
+ baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
65194
+ ref: baselineRef,
65195
+ baseFiles: projectBaselineBaseFiles,
65196
+ headFiles: projectBaselineHeadFiles
65197
+ } : void 0,
64671
65198
  changedLineRanges: scope === "lines" && changedLineRanges !== null ? resolveProjectChangedLineRanges(resolvedDirectory, scanDirectory, changedLineRanges) : void 0,
64672
65199
  supplyChainManifestChanged
64673
65200
  });
@@ -65867,4 +66394,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65867
66394
  export {};
65868
66395
 
65869
66396
  //# sourceMappingURL=cli.js.map
65870
- //# debugId=5cb8a881-e8fc-5561-8b89-ad2c8357acbc
66397
+ //# debugId=c24708ed-97a3-59cc-81f7-4cf277ab29f2