react-doctor 0.8.1-dev.7818922 → 0.8.1-dev.7eca0ee

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]="9e32582f-b296-5216-a028-7cc19ed83931")}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]="81bdc95e-12f2-565f-a05c-7b8a60a98f81")}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";
@@ -37317,6 +37317,10 @@ const listWorkspacePackages = (rootDirectory) => {
37317
37317
  return packages;
37318
37318
  };
37319
37319
  const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
37320
+ const MOBX_REACT_PACKAGE_NAME = "mobx-react";
37321
+ const MOBX_REACT_LITE_PACKAGE_NAME = "mobx-react-lite";
37322
+ const MOBX_STATE_TREE_PACKAGE_NAME = "mobx-state-tree";
37323
+ const MOBX_REACT_OBSERVER_PACKAGE_NAME = "mobx-react-observer";
37320
37324
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
37321
37325
  const resolveWorkspaceDependencyVersion = ({ concreteVersion, packageName, rootDirectory, rootPackageJson, sections, workspaceDirectory, workspacePackageJson }) => {
37322
37326
  const dependencyDeclaration = getDependencyDeclaration({
@@ -37335,7 +37339,18 @@ const shouldReplaceReactVersion = (currentVersion, nextVersion) => {
37335
37339
  if (nextMajor === null) return false;
37336
37340
  return nextMajor < currentMajor;
37337
37341
  };
37338
- const evaluateManifestFacts = (facts, packageJson, directory) => {
37342
+ const shouldReplaceSupportedDependencyFact = (currentFact, nextFact, latestSupportedMajor) => {
37343
+ if (currentFact.version === null) return true;
37344
+ if (nextFact.version === null) return false;
37345
+ const currentMajor = getLowestDependencyMajor(currentFact.version);
37346
+ const nextMajor = getLowestDependencyMajor(nextFact.version);
37347
+ if (currentMajor === null) return false;
37348
+ if (nextMajor === null) return true;
37349
+ if (currentMajor > latestSupportedMajor) return false;
37350
+ if (nextMajor > latestSupportedMajor) return true;
37351
+ return nextMajor < currentMajor;
37352
+ };
37353
+ const evaluateManifestFacts = (facts, packageJson, directory, rootDirectory, rootPackageJson) => {
37339
37354
  if (facts.expo.version === null) {
37340
37355
  const spec = getDependencySpec(packageJson, "expo");
37341
37356
  if (spec !== null) facts.expo = {
@@ -37357,13 +37372,65 @@ const evaluateManifestFacts = (facts, packageJson, directory) => {
37357
37372
  sourceDirectory: directory
37358
37373
  };
37359
37374
  }
37375
+ const mobxSpec = getDependencySpec(packageJson, "mobx");
37376
+ if (mobxSpec !== null) {
37377
+ const nextMobxFact = {
37378
+ version: resolveCatalogBackedDependencyVersion({
37379
+ rootDirectory: directory,
37380
+ rootPackageJson: packageJson,
37381
+ packageName: "mobx",
37382
+ version: mobxSpec
37383
+ }),
37384
+ sourceDirectory: directory
37385
+ };
37386
+ if (shouldReplaceSupportedDependencyFact(facts.mobx, nextMobxFact, 6)) facts.mobx = nextMobxFact;
37387
+ }
37388
+ facts.hasMobxReact = facts.hasMobxReact || getDependencySpec(packageJson, MOBX_REACT_PACKAGE_NAME) !== null;
37389
+ facts.hasMobxReactLite = facts.hasMobxReactLite || getDependencySpec(packageJson, MOBX_REACT_LITE_PACKAGE_NAME) !== null;
37390
+ facts.hasMobxStateTree = facts.hasMobxStateTree || getDependencySpec(packageJson, MOBX_STATE_TREE_PACKAGE_NAME) !== null;
37391
+ facts.hasMobxReactObserver = facts.hasMobxReactObserver || getDependencySpec(packageJson, MOBX_REACT_OBSERVER_PACKAGE_NAME) !== null;
37392
+ const zustandSpec = getDependencySpec(packageJson, "zustand");
37393
+ if (zustandSpec !== null) {
37394
+ const nextZustandFact = {
37395
+ version: resolveCatalogBackedDependencyVersion({
37396
+ rootDirectory: directory,
37397
+ rootPackageJson: packageJson,
37398
+ packageName: "zustand",
37399
+ version: zustandSpec
37400
+ }),
37401
+ sourceDirectory: directory
37402
+ };
37403
+ if (shouldReplaceSupportedDependencyFact(facts.zustand, nextZustandFact, 5)) facts.zustand = nextZustandFact;
37404
+ }
37360
37405
  if (facts.reanimatedVersion === null) {
37361
37406
  const spec = getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME);
37362
37407
  if (spec !== null) facts.reanimatedVersion = spec;
37363
37408
  }
37409
+ if (facts.valtioVersion === null) {
37410
+ const spec = getDependencySpec(packageJson, "valtio");
37411
+ if (spec !== null) facts.valtioVersion = spec;
37412
+ }
37364
37413
  facts.hasReactNativeAwarePackage = facts.hasReactNativeAwarePackage || isPackageJsonReactNativeAware(packageJson);
37365
37414
  facts.hasReanimatedAwarePackage = facts.hasReanimatedAwarePackage || isPackageJsonReanimatedAware(packageJson);
37366
37415
  facts.hasSsrDependency = facts.hasSsrDependency || isPackageJsonSsrAware(packageJson);
37416
+ const remotionSpec = getDependencySpec(packageJson, "remotion");
37417
+ if (remotionSpec !== null) {
37418
+ facts.hasRemotionDependency = true;
37419
+ const resolvedRemotionVersion = resolveCatalogBackedDependencyVersion({
37420
+ rootDirectory,
37421
+ rootPackageJson,
37422
+ packageName: "remotion",
37423
+ version: remotionSpec
37424
+ });
37425
+ const remotionMajorVersion = resolvedRemotionVersion === null ? null : getLowestDependencyMajor(resolvedRemotionVersion);
37426
+ if (remotionMajorVersion === null) {
37427
+ facts.hasUnknownRemotionVersion = true;
37428
+ facts.remotionVersion = null;
37429
+ } else if (!facts.hasUnknownRemotionVersion) {
37430
+ const currentRemotionMajorVersion = facts.remotionVersion === null ? null : getLowestDependencyMajor(facts.remotionVersion);
37431
+ if (currentRemotionMajorVersion === null || remotionMajorVersion < currentRemotionMajorVersion) facts.remotionVersion = resolvedRemotionVersion;
37432
+ }
37433
+ }
37367
37434
  };
37368
37435
  const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGroup }) => {
37369
37436
  const facts = {
@@ -37383,12 +37450,28 @@ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGro
37383
37450
  version: null,
37384
37451
  sourceDirectory: null
37385
37452
  },
37453
+ valtioVersion: null,
37454
+ mobx: {
37455
+ version: null,
37456
+ sourceDirectory: null
37457
+ },
37458
+ hasMobxReact: false,
37459
+ hasMobxReactLite: false,
37460
+ hasMobxStateTree: false,
37461
+ hasMobxReactObserver: false,
37462
+ zustand: {
37463
+ version: null,
37464
+ sourceDirectory: null
37465
+ },
37386
37466
  hasReactNativeAwarePackage: false,
37387
37467
  hasReanimatedAwarePackage: false,
37388
37468
  hasSsrDependency: false,
37469
+ hasRemotionDependency: false,
37470
+ hasUnknownRemotionVersion: false,
37471
+ remotionVersion: null,
37389
37472
  reanimatedVersion: null
37390
37473
  };
37391
- evaluateManifestFacts(facts, rootPackageJson, rootDirectory);
37474
+ evaluateManifestFacts(facts, rootPackageJson, rootDirectory, rootDirectory, rootPackageJson);
37392
37475
  let isReactGroupSettled = !collectReactGroup;
37393
37476
  const visitedDirectories = /* @__PURE__ */ new Set();
37394
37477
  for (const pattern of getWorkspacePatterns(rootDirectory, rootPackageJson)) {
@@ -37397,7 +37480,7 @@ const collectWorkspaceFacts = (rootDirectory, rootPackageJson, { collectReactGro
37397
37480
  if (visitedDirectories.has(workspaceDirectory)) continue;
37398
37481
  visitedDirectories.add(workspaceDirectory);
37399
37482
  const workspacePackageJson = readPackageJson$1(Path.join(workspaceDirectory, "package.json"));
37400
- evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory);
37483
+ evaluateManifestFacts(facts, workspacePackageJson, workspaceDirectory, rootDirectory, rootPackageJson);
37401
37484
  const info = extractDependencyInfo(workspacePackageJson);
37402
37485
  if (info.framework !== "unknown" && frameworkMergeRank(info.framework) < frameworkMergeRank(facts.framework)) facts.framework = info.framework;
37403
37486
  if (isReactGroupSettled) continue;
@@ -37618,11 +37701,24 @@ const discoverProjectWithoutPackageJson = (directory) => {
37618
37701
  tailwindVersion: null,
37619
37702
  zodVersion: null,
37620
37703
  zodMajorVersion: null,
37704
+ mobxVersion: null,
37705
+ mobxMajorVersion: null,
37706
+ hasMobxReact: false,
37707
+ hasMobxReactLite: false,
37708
+ hasMobxStateTree: false,
37709
+ hasMobxReactObserver: false,
37710
+ zustandVersion: null,
37711
+ zustandMajorVersion: null,
37621
37712
  framework: "unknown",
37622
37713
  hasTypeScript: hasOwnTsConfig,
37623
37714
  hasReactCompiler: false,
37624
37715
  hasReactCompilerLintPlugin: false,
37625
37716
  hasTanStackQuery: false,
37717
+ valtioVersion: null,
37718
+ valtioMajorVersion: null,
37719
+ hasRemotion: false,
37720
+ remotionVersion: null,
37721
+ remotionMajorVersion: null,
37626
37722
  hasSsrDependency: false,
37627
37723
  preactVersion: null,
37628
37724
  preactMajorVersion: null,
@@ -37721,6 +37817,12 @@ const discoverProject = (directory) => {
37721
37817
  packageName: SHOPIFY_FLASH_LIST_PACKAGE_NAME,
37722
37818
  version: workspaceFacts.shopifyFlashList.version
37723
37819
  }) : null;
37820
+ const zustandVersion = resolveCatalogBackedDependencyVersion({
37821
+ rootDirectory: directory,
37822
+ rootPackageJson: packageJson,
37823
+ packageName: "zustand",
37824
+ version: workspaceFacts.zustand.version
37825
+ });
37724
37826
  const hasReanimated = hasReactNativeWorkspace && workspaceFacts.hasReanimatedAwarePackage;
37725
37827
  const reanimatedVersion = hasReanimated ? workspaceFacts.reanimatedVersion : null;
37726
37828
  const nextjsVersion = framework === "nextjs" ? resolveCatalogBackedDependencyVersion({
@@ -37729,7 +37831,20 @@ const discoverProject = (directory) => {
37729
37831
  packageName: "next",
37730
37832
  version: workspaceFacts.next.version
37731
37833
  }) : null;
37834
+ const valtioVersion = resolveCatalogBackedDependencyVersion({
37835
+ rootDirectory: directory,
37836
+ rootPackageJson: packageJson,
37837
+ packageName: "valtio",
37838
+ version: workspaceFacts.valtioVersion
37839
+ });
37840
+ const mobxVersion = resolveCatalogBackedDependencyVersion({
37841
+ rootDirectory: directory,
37842
+ rootPackageJson: packageJson,
37843
+ packageName: "mobx",
37844
+ version: workspaceFacts.mobx.version
37845
+ });
37732
37846
  const preactVersion = getPreactVersion(packageJson);
37847
+ const remotionVersion = workspaceFacts.remotionVersion;
37733
37848
  const isPreES2023Target = hasTypeScript && detectPreES2023Target(directory);
37734
37849
  const projectInfo = {
37735
37850
  rootDirectory: directory,
@@ -37739,11 +37854,24 @@ const discoverProject = (directory) => {
37739
37854
  tailwindVersion,
37740
37855
  zodVersion,
37741
37856
  zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
37857
+ mobxVersion,
37858
+ mobxMajorVersion: mobxVersion === null ? null : getLowestDependencyMajor(mobxVersion),
37859
+ hasMobxReact: workspaceFacts.hasMobxReact,
37860
+ hasMobxReactLite: workspaceFacts.hasMobxReactLite,
37861
+ hasMobxStateTree: workspaceFacts.hasMobxStateTree,
37862
+ hasMobxReactObserver: workspaceFacts.hasMobxReactObserver,
37863
+ zustandVersion,
37864
+ zustandMajorVersion: zustandVersion === null ? null : getLowestDependencyMajor(zustandVersion),
37742
37865
  framework,
37743
37866
  hasTypeScript,
37744
37867
  hasReactCompiler: detectReactCompiler(directory, packageJson),
37745
37868
  hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(directory, packageJson),
37746
37869
  hasTanStackQuery: hasTanStackQuery(packageJson),
37870
+ valtioVersion,
37871
+ valtioMajorVersion: valtioVersion === null ? null : getLowestDependencyMajor(valtioVersion),
37872
+ hasRemotion: workspaceFacts.hasRemotionDependency,
37873
+ remotionVersion,
37874
+ remotionMajorVersion: remotionVersion === null ? null : getLowestDependencyMajor(remotionVersion),
37747
37875
  hasSsrDependency: workspaceFacts.hasSsrDependency,
37748
37876
  preactVersion,
37749
37877
  preactMajorVersion: parseReactMajor(preactVersion),
@@ -37977,7 +38105,6 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
37977
38105
  "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
37978
38106
  "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
37979
38107
  "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
37980
- "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
37981
38108
  "react/jsx-no-undef": "react-doctor/jsx-no-undef",
37982
38109
  "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
37983
38110
  "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
@@ -40299,9 +40426,25 @@ const BABEL_CONFIG_FILE_NAMES = [
40299
40426
  ".babelrc.json"
40300
40427
  ];
40301
40428
  const LEGACY_PRESET_SPEC = "module:metro-react-native-babel-preset";
40429
+ const LEGACY_PRESET_PACKAGE = "metro-react-native-babel-preset";
40430
+ const REACT_NATIVE_PACKAGE = "react-native";
40431
+ const PRESET_RENAME_MINIMUM_REACT_NATIVE_VERSION = "0.73.0";
40432
+ const LEGACY_PRESET_REFERENCE = new RegExp(`['"]${LEGACY_PRESET_SPEC}['"]`);
40302
40433
  const MODERN_PRESET_REFERENCE = new RegExp(`['"]module:@react-native/babel-preset['"]`);
40303
40434
  const ENABLE_BABEL_RUNTIME_VERSION = /enableBabelRuntime["']?\s*:\s*['"]/;
40435
+ const isReactNativeVersionAtLeastPresetRename = (rootDirectory, packageJson) => {
40436
+ const installedReactNativeManifestPath = Path.join(rootDirectory, "node_modules", REACT_NATIVE_PACKAGE, "package.json");
40437
+ const reactNativeVersionSpec = (isFile(installedReactNativeManifestPath) ? readPackageJson$1(installedReactNativeManifestPath).version : void 0) ?? getDependencySpec(packageJson, REACT_NATIVE_PACKAGE);
40438
+ if (reactNativeVersionSpec === null || reactNativeVersionSpec === void 0) return false;
40439
+ const normalizedVersionRange = import_semver.validRange(reactNativeVersionSpec);
40440
+ if (normalizedVersionRange === null) return false;
40441
+ const minimumVersion = import_semver.minVersion(normalizedVersionRange);
40442
+ return minimumVersion !== null && import_semver.gte(minimumVersion, PRESET_RENAME_MINIMUM_REACT_NATIVE_VERSION);
40443
+ };
40444
+ const canResolveLegacyPreset = (rootDirectory, packageJson) => getDependencySpec(packageJson, LEGACY_PRESET_PACKAGE) !== null || isFile(Path.join(rootDirectory, "node_modules", LEGACY_PRESET_PACKAGE, "package.json"));
40304
40445
  const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40446
+ const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
40447
+ const shouldReportLegacyPreset = isReactNativeVersionAtLeastPresetRename(rootDirectory, packageJson) && !canResolveLegacyPreset(rootDirectory, packageJson);
40305
40448
  for (const fileName of BABEL_CONFIG_FILE_NAMES) {
40306
40449
  const filePath = Path.join(rootDirectory, fileName);
40307
40450
  if (!isFile(filePath)) continue;
@@ -40311,7 +40454,7 @@ const checkReactNativeMetroBabelPreset = (rootDirectory) => {
40311
40454
  } catch {
40312
40455
  continue;
40313
40456
  }
40314
- if (contents.includes(LEGACY_PRESET_SPEC)) return [buildReactNativeDiagnostic({
40457
+ if (shouldReportLegacyPreset && LEGACY_PRESET_REFERENCE.test(contents)) return [buildReactNativeDiagnostic({
40315
40458
  filePath: fileName,
40316
40459
  rule: "rn-no-metro-babel-preset",
40317
40460
  severity: "error",
@@ -40334,12 +40477,6 @@ const checkReactNativeProject = (rootDirectory, project) => {
40334
40477
  return [...checkReactNativeMetroBabelPreset(rootDirectory), ...checkReactNativeLibraryDependencies(rootDirectory)];
40335
40478
  };
40336
40479
  const RULE_KEY = "no-vulnerable-react-server-components";
40337
- const DEPENDENCY_SECTIONS = [
40338
- "dependencies",
40339
- "devDependencies",
40340
- "peerDependencies",
40341
- "optionalDependencies"
40342
- ];
40343
40480
  const REACT_RSC_ADVISORIES_BY_MINOR = {
40344
40481
  "19.0": {
40345
40482
  rceFixedVersion: "19.0.1",
@@ -40386,20 +40523,13 @@ const enumerateWorkspaceDirectories = (workspaceRoot) => {
40386
40523
  for (const pattern of patterns) for (const directory of resolveWorkspaceDirectories(workspaceRoot, pattern)) directories.add(directory);
40387
40524
  return [...directories];
40388
40525
  };
40389
- const readDeclaredSpec = (packageJson, packageName) => {
40390
- for (const section of DEPENDENCY_SECTIONS) {
40391
- const spec = packageJson[section]?.[packageName];
40392
- if (typeof spec === "string") return spec;
40393
- }
40394
- return null;
40395
- };
40396
40526
  const resolveVersionInDirectory = (directory, packageName, declaredSpecOverride) => {
40397
40527
  const manifestPath = Path.join(directory, "node_modules", packageName, "package.json");
40398
40528
  if (isFile(manifestPath)) {
40399
40529
  const installedVersion = import_semver.valid(readPackageJson$1(manifestPath).version ?? null);
40400
40530
  if (installedVersion !== null) return installedVersion;
40401
40531
  }
40402
- const candidateSpecs = [readDeclaredSpec(readPackageJson$1(Path.join(directory, "package.json")), packageName), declaredSpecOverride];
40532
+ const candidateSpecs = [getDependencySpec(readPackageJson$1(Path.join(directory, "package.json")), packageName), declaredSpecOverride];
40403
40533
  for (const spec of candidateSpecs) {
40404
40534
  const pinnedVersion = spec === null ? null : import_semver.valid(spec);
40405
40535
  if (pinnedVersion !== null) return pinnedVersion;
@@ -41079,9 +41209,24 @@ const buildCapabilities = (project) => {
41079
41209
  })) capabilities.add("tailwind:3.4");
41080
41210
  if (project.zodVersion !== null) capabilities.add("zod");
41081
41211
  if (project.zodMajorVersion !== null && project.zodMajorVersion >= 4) capabilities.add("zod:4");
41212
+ if (project.mobxVersion !== void 0 && project.mobxVersion !== null || project.hasMobxReact === true || project.hasMobxReactLite === true || project.hasMobxStateTree === true || project.hasMobxReactObserver === true) capabilities.add("mobx");
41213
+ if (project.hasMobxReact === true) capabilities.add("mobx-react");
41214
+ if (project.hasMobxReactLite === true) capabilities.add("mobx-react-lite");
41215
+ if (project.hasMobxReact === true || project.hasMobxReactLite === true) capabilities.add("mobx-react-binding");
41216
+ if (project.hasMobxStateTree === true) capabilities.add("mobx-state-tree");
41217
+ if (project.hasMobxReactObserver === true) capabilities.add("mobx-react-observer");
41218
+ if (project.mobxMajorVersion !== void 0 && project.mobxMajorVersion !== null && project.mobxMajorVersion >= 4 && project.mobxMajorVersion <= 6) addMajorLadder(capabilities, "mobx", project.mobxMajorVersion, 4, 6);
41219
+ if (project.zustandVersion !== void 0 && project.zustandVersion !== null) capabilities.add("zustand");
41220
+ if (project.zustandMajorVersion !== void 0 && project.zustandMajorVersion !== null && project.zustandMajorVersion >= 1 && project.zustandMajorVersion <= 5) addMajorLadder(capabilities, "zustand", project.zustandMajorVersion, 1, 5);
41082
41221
  if (project.isPreES2023Target) capabilities.add("pre-es2023");
41083
41222
  if (project.hasReactCompiler) capabilities.add("react-compiler");
41084
41223
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
41224
+ if (project.valtioVersion !== null) capabilities.add("valtio");
41225
+ addMajorLadder(capabilities, "valtio", project.valtioMajorVersion, 1, 10);
41226
+ if (project.hasRemotion) {
41227
+ capabilities.add("remotion");
41228
+ addMajorLadder(capabilities, "remotion", project.remotionMajorVersion ?? null, 4, 30);
41229
+ }
41085
41230
  if (project.hasTypeScript) capabilities.add("typescript");
41086
41231
  if (project.preactVersion !== null) capabilities.add("preact");
41087
41232
  addMajorLadder(capabilities, "preact", project.preactMajorVersion, 10, 20);
@@ -42334,6 +42479,35 @@ const parseGithubViewerPermission = (stdout) => {
42334
42479
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
42335
42480
  };
42336
42481
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
42482
+ const parseBaselineDiffPlan = (value) => {
42483
+ const entries = splitNullSeparated(value);
42484
+ const baseFiles = /* @__PURE__ */ new Set();
42485
+ const headFiles = /* @__PURE__ */ new Set();
42486
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 2) {
42487
+ const status = entries[entryIndex];
42488
+ const filePath = entries[entryIndex + 1];
42489
+ if (status === void 0 || filePath === void 0 || status.length !== 1) return null;
42490
+ if (status === "A") {
42491
+ headFiles.add(filePath);
42492
+ continue;
42493
+ }
42494
+ if (status === "D") {
42495
+ baseFiles.add(filePath);
42496
+ continue;
42497
+ }
42498
+ if (status === "M" || status === "T") {
42499
+ baseFiles.add(filePath);
42500
+ headFiles.add(filePath);
42501
+ continue;
42502
+ }
42503
+ return null;
42504
+ }
42505
+ return {
42506
+ baseFiles: [...baseFiles],
42507
+ headFiles: [...headFiles],
42508
+ untrackedFiles: []
42509
+ };
42510
+ };
42337
42511
  const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
42338
42512
  /**
42339
42513
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -42536,6 +42710,45 @@ var Git = class Git extends Service()("react-doctor/Git") {
42536
42710
  githubViewerPermission,
42537
42711
  branchExists,
42538
42712
  mergeBase,
42713
+ baselineDiffPlan: (input) => {
42714
+ if (!isSafeGitRevision(input.ref)) return succeed$2(null);
42715
+ return gen(function* () {
42716
+ const unmerged = yield* runGit(input.directory, [
42717
+ "diff",
42718
+ "--no-ext-diff",
42719
+ "-z",
42720
+ "--name-only",
42721
+ "--diff-filter=U",
42722
+ "--relative"
42723
+ ]);
42724
+ if (unmerged.status !== 0 || unmerged.stdout.length > 0) return null;
42725
+ const result = yield* runGit(input.directory, [
42726
+ "diff",
42727
+ "--no-ext-diff",
42728
+ "--no-textconv",
42729
+ "--no-renames",
42730
+ "-z",
42731
+ "--name-status",
42732
+ "--relative",
42733
+ input.ref
42734
+ ]);
42735
+ if (result.status !== 0) return null;
42736
+ const plan = parseBaselineDiffPlan(result.stdout);
42737
+ if (plan === null) return null;
42738
+ const untracked = yield* runGit(input.directory, [
42739
+ "ls-files",
42740
+ "--others",
42741
+ "--exclude-standard",
42742
+ "-z"
42743
+ ]);
42744
+ if (untracked.status !== 0) return null;
42745
+ return {
42746
+ baseFiles: plan.baseFiles,
42747
+ headFiles: plan.headFiles,
42748
+ untrackedFiles: splitNullSeparated(untracked.stdout)
42749
+ };
42750
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.baselineDiffPlan"));
42751
+ },
42539
42752
  diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42540
42753
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42541
42754
  if (explicitBaseBranch !== void 0) {
@@ -42685,6 +42898,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42685
42898
  githubViewerPermission: () => succeed$2(snapshot.githubViewerPermission ?? null),
42686
42899
  branchExists: (_directory, branch) => succeed$2(snapshot.branchExists?.get(branch) ?? false),
42687
42900
  mergeBase: ({ ref }) => succeed$2(snapshot.mergeBase?.get(ref) ?? null),
42901
+ baselineDiffPlan: () => succeed$2(snapshot.baselineDiffPlan ?? null),
42688
42902
  diffSelection: () => succeed$2(snapshot.diffSelection ?? null),
42689
42903
  stagedFilePaths: () => succeed$2(snapshot.stagedFiles ?? []),
42690
42904
  showStagedContent: (_directory, relativePath) => succeed$2(snapshot.stagedContent?.get(relativePath) ?? null),
@@ -44022,12 +44236,17 @@ const REACT_ERROR_BOUNDARY_MESSAGE = "This try/catch cannot catch errors thrown
44022
44236
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
44023
44237
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
44024
44238
  const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
44239
+ const SET_STATE_IN_EFFECT_ACTION = "Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with `// react-doctor-disable-next-line react-hooks-js/set-state-in-effect`.";
44025
44240
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
44026
44241
  const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
44027
44242
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
44028
44243
  if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
44029
44244
  return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
44030
44245
  };
44246
+ const buildSetStateInEffectMessage = (reasonSummary) => {
44247
+ const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
44248
+ return `This synchronous effect update causes an extra render${normalizedSummary ? `: ${normalizedSummary}` : ""}. ${SET_STATE_IN_EFFECT_ACTION}`;
44249
+ };
44031
44250
  const PLUGIN_CATEGORY_MAP = {
44032
44251
  react: "Bugs",
44033
44252
  "react-hooks": "Bugs",
@@ -44085,6 +44304,10 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
44085
44304
  message: REACT_ERROR_BOUNDARY_MESSAGE,
44086
44305
  help: reasonDetail || help
44087
44306
  };
44307
+ if (rule === "set-state-in-effect") return {
44308
+ message: buildSetStateInEffectMessage(reasonSummary.trim()),
44309
+ help: reasonDetail || help
44310
+ };
44088
44311
  return {
44089
44312
  message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
44090
44313
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
@@ -46421,12 +46644,19 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
46421
46644
  */
46422
46645
  const materializeSourceTree = (input) => gen(function* () {
46423
46646
  const materializedFiles = [];
46647
+ const unmaterializedFiles = [];
46424
46648
  const resolvedTempDirectory = path.resolve(input.tempDirectory);
46425
46649
  for (const relativePath of input.files) {
46426
46650
  const content = yield* input.readContent(relativePath).pipe(orElseSucceed(() => null));
46427
- if (content === null) continue;
46651
+ if (content === null) {
46652
+ unmaterializedFiles.push(relativePath);
46653
+ continue;
46654
+ }
46428
46655
  const candidateTargetPath = path.resolve(resolvedTempDirectory, relativePath);
46429
- if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) continue;
46656
+ if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) {
46657
+ unmaterializedFiles.push(relativePath);
46658
+ continue;
46659
+ }
46430
46660
  yield* sync(() => {
46431
46661
  fs.mkdirSync(path.dirname(candidateTargetPath), { recursive: true });
46432
46662
  fs.writeFileSync(candidateTargetPath, content);
@@ -46443,6 +46673,7 @@ const materializeSourceTree = (input) => gen(function* () {
46443
46673
  return {
46444
46674
  tempDirectory: input.tempDirectory,
46445
46675
  materializedFiles,
46676
+ unmaterializedFiles,
46446
46677
  cleanup: () => {
46447
46678
  try {
46448
46679
  fs.rmSync(input.tempDirectory, {
@@ -46712,54 +46943,80 @@ const buildSkippedChecks = (input) => {
46712
46943
  skippedCheckReasons
46713
46944
  };
46714
46945
  };
46715
- const fingerprintDiagnostic = (diagnostic, lineText) => {
46946
+ const fingerprintText = (text) => createHash("sha256").update(text).digest("hex");
46947
+ const normalizeEvidence = (evidence) => evidence.replace(/\s+/g, " ").trim();
46948
+ const getDiagnosticMatchKeys = (diagnostic, evidence) => {
46716
46949
  const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
46717
- const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
46718
- return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
46719
- };
46720
- /**
46721
- * Diffs a head scan against a base scan to isolate the diagnostics a change
46722
- * introduced (and count the ones it resolved). Matching is a multiset over a
46723
- * position-independent fingerprint — `(filePath, plugin/rule, hash(flagged
46724
- * line text))` — so inserting lines above an existing issue doesn't make it
46725
- * look new, while a genuinely new occurrence (new line text, or one more of
46726
- * the same) surfaces. Identical repeated findings are matched by count.
46727
- *
46728
- * Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
46729
- * every Accessibility-category finding, plus rules opting in via their
46730
- * `matchByOccurrence` metadata flag) drop the line-text snippet and match by
46731
- * `(filePath, plugin/rule)` occurrence count alone. Their identity is the
46732
- * flagged element, not the line's text, so editing the line (reindentation,
46733
- * prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
46734
- * pre-existing finding as new — while one MORE occurrence of the same rule in
46735
- * the file still surfaces. Expression-level rules keep the line-text snippet:
46736
- * there the flagged expression IS the finding, so changed text means new +
46737
- * fixed.
46738
- *
46739
- * v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
46740
- * content is read at that same path. A file renamed by the change therefore has
46741
- * no base match, so its pre-existing findings are reported as new. This
46742
- * over-reports (never hides a real issue) and is rare; rename-aware base
46743
- * resolution is a follow-up.
46950
+ const messageFingerprint = fingerprintText(`${diagnostic.title ?? ""}\0${diagnostic.message}`);
46951
+ const normalizedEvidence = evidence === null ? "" : normalizeEvidence(evidence);
46952
+ const stableEvidenceKey = normalizedEvidence.length > 0 ? `evidence\0${ruleKey}\0${messageFingerprint}\0${fingerprintText(normalizedEvidence)}` : null;
46953
+ return {
46954
+ stableEvidenceKey,
46955
+ sameFileStableEvidenceKey: stableEvidenceKey === null ? null : `${diagnostic.filePath}\0${stableEvidenceKey}`,
46956
+ sameFileFallbackKey: diagnostic.matchByOccurrence || normalizedEvidence.length === 0 ? `fallback\0${diagnostic.filePath}\0${ruleKey}\0${messageFingerprint}` : null
46957
+ };
46958
+ };
46959
+ const addDiagnosticIndex = (buckets, key, diagnosticIndex) => {
46960
+ if (key === null) return;
46961
+ const diagnosticIndexes = buckets.get(key) ?? [];
46962
+ diagnosticIndexes.push(diagnosticIndex);
46963
+ buckets.set(key, diagnosticIndexes);
46964
+ };
46965
+ const takeMatchingDiagnosticIndex = (buckets, key, matchedDiagnosticIndexes) => {
46966
+ if (key === null) return null;
46967
+ for (const diagnosticIndex of buckets.get(key) ?? []) if (!matchedDiagnosticIndexes.has(diagnosticIndex)) return diagnosticIndex;
46968
+ return null;
46969
+ };
46970
+ const readDiagnosticEvidence = (diagnostic, readEvidence, readLine) => readEvidence?.(diagnostic) ?? readLine(diagnostic.filePath, diagnostic.line);
46971
+ const buildMatchCandidates = (diagnostics, readEvidence, readLine) => diagnostics.map((diagnostic, diagnosticIndex) => ({
46972
+ diagnosticIndex,
46973
+ ...getDiagnosticMatchKeys(diagnostic, readDiagnosticEvidence(diagnostic, readEvidence, readLine))
46974
+ }));
46975
+ /**
46976
+ * Diffs a head scan against a base scan using a multiset of construct-level
46977
+ * evidence. Stable identities combine plugin/rule, the diagnostic message,
46978
+ * and normalized diagnosed source, so unchanged findings can move across
46979
+ * files while changed constructs or messages remain new. Cardinality is
46980
+ * retained for identical findings. Diagnostics explicitly marked
46981
+ * `matchByOccurrence` may fall back to same-file plugin/rule/message matching
46982
+ * after same-file strict evidence matching. Cross-file evidence matching runs
46983
+ * last so a copy cannot consume a reformatted local occurrence. Unreadable
46984
+ * evidence uses the same conservative fallback rather than matching across
46985
+ * files without proof.
46744
46986
  */
46745
46987
  const computeDiagnosticDelta = (input) => {
46746
- const unmatchedBaseByFingerprint = /* @__PURE__ */ new Map();
46747
- for (const diagnostic of input.baseDiagnostics) {
46748
- const key = fingerprintDiagnostic(diagnostic, input.readBaseLine(diagnostic.filePath, diagnostic.line));
46749
- unmatchedBaseByFingerprint.set(key, (unmatchedBaseByFingerprint.get(key) ?? 0) + 1);
46750
- }
46751
- const newDiagnostics = [];
46752
- for (const diagnostic of input.headDiagnostics) {
46753
- const key = fingerprintDiagnostic(diagnostic, input.readHeadLine(diagnostic.filePath, diagnostic.line));
46754
- const availableMatches = unmatchedBaseByFingerprint.get(key) ?? 0;
46755
- if (availableMatches > 0) unmatchedBaseByFingerprint.set(key, availableMatches - 1);
46756
- else newDiagnostics.push(diagnostic);
46757
- }
46758
- let fixedCount = 0;
46759
- for (const remaining of unmatchedBaseByFingerprint.values()) fixedCount += remaining;
46988
+ const baseCandidates = buildMatchCandidates(input.baseDiagnostics, input.readBaseEvidence, input.readBaseLine);
46989
+ const headCandidates = buildMatchCandidates(input.headDiagnostics, input.readHeadEvidence, input.readHeadLine);
46990
+ const baseByStableEvidence = /* @__PURE__ */ new Map();
46991
+ const baseBySameFileStableEvidence = /* @__PURE__ */ new Map();
46992
+ const baseBySameFileFallback = /* @__PURE__ */ new Map();
46993
+ for (const candidate of baseCandidates) {
46994
+ addDiagnosticIndex(baseByStableEvidence, candidate.stableEvidenceKey, candidate.diagnosticIndex);
46995
+ addDiagnosticIndex(baseBySameFileStableEvidence, candidate.sameFileStableEvidenceKey, candidate.diagnosticIndex);
46996
+ addDiagnosticIndex(baseBySameFileFallback, candidate.sameFileFallbackKey, candidate.diagnosticIndex);
46997
+ }
46998
+ const matchedHeadDiagnosticIndexes = /* @__PURE__ */ new Set();
46999
+ const matchedBaseDiagnosticIndexes = /* @__PURE__ */ new Set();
47000
+ const matchCandidates = (baseBuckets, getKey, onMatch) => {
47001
+ for (const candidate of headCandidates) {
47002
+ if (matchedHeadDiagnosticIndexes.has(candidate.diagnosticIndex)) continue;
47003
+ const baseDiagnosticIndex = takeMatchingDiagnosticIndex(baseBuckets, getKey(candidate), matchedBaseDiagnosticIndexes);
47004
+ if (baseDiagnosticIndex === null) continue;
47005
+ matchedHeadDiagnosticIndexes.add(candidate.diagnosticIndex);
47006
+ matchedBaseDiagnosticIndexes.add(baseDiagnosticIndex);
47007
+ onMatch?.(candidate.diagnosticIndex, baseDiagnosticIndex);
47008
+ }
47009
+ };
47010
+ matchCandidates(baseBySameFileStableEvidence, (candidate) => candidate.sameFileStableEvidenceKey);
47011
+ matchCandidates(baseBySameFileFallback, (candidate) => candidate.sameFileFallbackKey);
47012
+ let crossFileMatchCount = 0;
47013
+ matchCandidates(baseByStableEvidence, (candidate) => candidate.stableEvidenceKey, (head, base) => {
47014
+ if (input.headDiagnostics[head]?.filePath !== input.baseDiagnostics[base]?.filePath) crossFileMatchCount += 1;
47015
+ });
46760
47016
  return {
46761
- newDiagnostics,
46762
- fixedCount
47017
+ newDiagnostics: input.headDiagnostics.filter((_diagnostic, diagnosticIndex) => !matchedHeadDiagnosticIndexes.has(diagnosticIndex)),
47018
+ fixedCount: input.baseDiagnostics.length - matchedBaseDiagnosticIndexes.size,
47019
+ crossFileMatchCount
46763
47020
  };
46764
47021
  };
46765
47022
  /**
@@ -46791,6 +47048,12 @@ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runProm
46791
47048
  };
46792
47049
  }).pipe(provide(Git.layerNode)));
46793
47050
  const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
47051
+ const getBaselineDiffPlan = (directory, ref) => runPromise(gen(function* () {
47052
+ return yield* (yield* Git).baselineDiffPlan({
47053
+ directory,
47054
+ ref
47055
+ });
47056
+ }).pipe(provide(Git.layerNode)));
46794
47057
  /**
46795
47058
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
46796
47059
  * `files` with `--unified=0` against `baseRef` (or the index when `cached`),
@@ -47247,7 +47510,7 @@ const makeNoopConsole = () => ({
47247
47510
  });
47248
47511
  //#endregion
47249
47512
  //#region src/cli/utils/version.ts
47250
- const VERSION = "0.8.1-dev.7818922";
47513
+ const VERSION = "0.8.1-dev.7eca0ee";
47251
47514
  //#endregion
47252
47515
  //#region src/cli/utils/json-mode.ts
47253
47516
  let context = null;
@@ -47599,13 +47862,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47599
47862
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47600
47863
  * standard `SENTRY_RELEASE` override.
47601
47864
  */
47602
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.1-dev.7818922`;
47865
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.1-dev.7eca0ee`;
47603
47866
  /**
47604
47867
  * Deployment environment shown in Sentry's environment filter. Defaults to
47605
47868
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47606
47869
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47607
47870
  */
47608
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.1-dev.7818922") ? "development" : "production");
47871
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.1-dev.7eca0ee") ? "development" : "production");
47609
47872
  /**
47610
47873
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47611
47874
  * (set to `0` to disable tracing) and falls back to
@@ -60228,6 +60491,7 @@ const buildOutcomeAttributes = (input) => {
60228
60491
  new: summary.totalDiagnosticCount,
60229
60492
  fixed: result.baselineDelta.fixedCount,
60230
60493
  baseTotal: result.baselineDelta.baseTotalCount,
60494
+ crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
60231
60495
  degraded: false
60232
60496
  }));
60233
60497
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -60341,28 +60605,49 @@ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
60341
60605
  return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
60342
60606
  };
60343
60607
  //#endregion
60608
+ //#region src/cli/utils/is-materializable-git-source.ts
60609
+ const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
60610
+ const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
60611
+ //#endregion
60344
60612
  //#region src/cli/utils/materialize-baseline-files.ts
60345
60613
  /**
60346
- * Materializes the `ref` version of `files` into `tempDirectory` (via
60347
- * `git show <ref>:<path>`), mirroring the project layout + head's config files
60348
- * so the base lints under the same rules as head. Reuses the shared,
60349
- * zip-slip-guarded `materializeSourceTree`. Per-file read misses (e.g. a file
60350
- * the PR added, absent at base) are skipped, which is exactly what we want:
60351
- * a brand-new file has no base counterpart, so all its findings are "new".
60614
+ * Materializes the base side of the `ref` → worktree diff into
60615
+ * `tempDirectory`, mirroring the project layout plus head's config files so
60616
+ * both sides lint under the same rules. Git rename heuristics are disabled by
60617
+ * the diff planner, so an old path is retained as a base deletion while its
60618
+ * new path is a head addition. Missing head-only additions are expected;
60619
+ * missing paths that the plan says must exist at base make `isComplete` false.
60352
60620
  */
60353
60621
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
60354
60622
  const git = yield* Git;
60355
- return yield* materializeSourceTree({
60623
+ const diffPlan = yield* git.baselineDiffPlan({
60624
+ directory: input.directory,
60625
+ ref: input.ref
60626
+ });
60627
+ const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
60628
+ const headFiles = input.headFiles ?? diffPlan?.headFiles;
60629
+ const untrackedFiles = diffPlan?.untrackedFiles ?? [];
60630
+ if (baseFiles === void 0 || headFiles === void 0) return null;
60631
+ const files = [...new Set([...input.files, ...baseFiles])];
60632
+ const tree = yield* materializeSourceTree({
60356
60633
  directory: input.directory,
60357
- files: input.files,
60634
+ files,
60358
60635
  tempDirectory: input.tempDirectory,
60359
60636
  readContent: (relativePath) => git.showRefContent({
60360
60637
  directory: input.directory,
60361
60638
  ref: input.ref,
60362
60639
  relativePath,
60363
60640
  options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
60364
- })
60641
+ }).pipe(map$3((content) => content !== null && isMaterializableGitSource(content) ? content : null))
60365
60642
  });
60643
+ const materializedFiles = new Set(tree.materializedFiles);
60644
+ return {
60645
+ ...tree,
60646
+ baseFiles,
60647
+ headFiles,
60648
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
60649
+ untrackedFiles
60650
+ };
60366
60651
  }).pipe(provide(Git.layerNode)));
60367
60652
  /**
60368
60653
  * Resolves the commit a baseline scan should read base content from — the
@@ -60385,8 +60670,8 @@ const resolveMergeBaseRef = async (directory, ref) => {
60385
60670
  //#region src/cli/utils/read-source-line.ts
60386
60671
  /**
60387
60672
  * Builds a cached `(filePath, line) -> source text` reader rooted at
60388
- * `rootDirectory`, used to fingerprint a diagnostic by the content of its
60389
- * flagged line (see `computeDiagnosticDelta`). Wraps core's
60673
+ * `rootDirectory`, used as the fallback when construct-level diagnostic
60674
+ * evidence cannot be read. Wraps core's
60390
60675
  * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
60391
60676
  * lookup; unreadable files / out-of-range lines return `null`.
60392
60677
  */
@@ -60401,6 +60686,338 @@ const createSourceLineReader = (rootDirectory) => {
60401
60686
  };
60402
60687
  };
60403
60688
  //#endregion
60689
+ //#region src/cli/utils/read-diagnostic-evidence.ts
60690
+ const SOURCE_EXTENSIONS = [
60691
+ ".ts",
60692
+ ".tsx",
60693
+ ".js",
60694
+ ".jsx",
60695
+ ".mts",
60696
+ ".cts",
60697
+ ".mjs",
60698
+ ".cjs"
60699
+ ];
60700
+ const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
60701
+ const getScriptKind = (filePath) => {
60702
+ const extension = path.extname(filePath).toLowerCase();
60703
+ if (extension === ".tsx") return ts.ScriptKind.TSX;
60704
+ if (extension === ".jsx") return ts.ScriptKind.JSX;
60705
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return ts.ScriptKind.JS;
60706
+ return ts.ScriptKind.TS;
60707
+ };
60708
+ const resolveSafeSourcePath = (rootDirectory, filePath) => {
60709
+ try {
60710
+ const resolvedRootDirectory = fs.realpathSync(rootDirectory);
60711
+ const absolutePath = path.resolve(resolvedRootDirectory, filePath);
60712
+ const relativePath = path.relative(resolvedRootDirectory, absolutePath);
60713
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
60714
+ const stats = fs.lstatSync(absolutePath);
60715
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
60716
+ const realPath = fs.realpathSync(absolutePath);
60717
+ const realRelativePath = path.relative(resolvedRootDirectory, realPath);
60718
+ return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
60719
+ } catch {
60720
+ return null;
60721
+ }
60722
+ };
60723
+ const readSourceRecord = (rootDirectory, filePath, state) => {
60724
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
60725
+ if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
60726
+ const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
60727
+ if (absolutePath === null) {
60728
+ state.recordByFilePath.set(normalizedFilePath, null);
60729
+ return null;
60730
+ }
60731
+ try {
60732
+ const sourceText = fs.readFileSync(absolutePath, "utf-8");
60733
+ const record = {
60734
+ absolutePath,
60735
+ filePath: normalizedFilePath,
60736
+ sourceFile: ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
60737
+ sourceText
60738
+ };
60739
+ state.recordByFilePath.set(normalizedFilePath, record);
60740
+ return record;
60741
+ } catch {
60742
+ state.recordByFilePath.set(normalizedFilePath, null);
60743
+ return null;
60744
+ }
60745
+ };
60746
+ const listSourceRecords = (rootDirectory, state) => {
60747
+ if (state.sourceRecords !== null) return state.sourceRecords;
60748
+ state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
60749
+ const record = readSourceRecord(rootDirectory, filePath, state);
60750
+ return record === null ? [] : [record];
60751
+ });
60752
+ return state.sourceRecords;
60753
+ };
60754
+ const unwrapExpression = (expression) => {
60755
+ let currentExpression = expression;
60756
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
60757
+ return currentExpression;
60758
+ };
60759
+ const getCalleeName = (expression) => {
60760
+ const unwrappedExpression = unwrapExpression(expression);
60761
+ if (ts.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
60762
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
60763
+ return null;
60764
+ };
60765
+ const isComponentWrapperCall = (node) => {
60766
+ const wrapperName = getCalleeName(node.expression);
60767
+ return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
60768
+ };
60769
+ const getFunctionName = (node) => {
60770
+ if (ts.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
60771
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
60772
+ let componentExpression = node;
60773
+ while (true) {
60774
+ const parent = componentExpression.parent;
60775
+ if (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) {
60776
+ componentExpression = parent;
60777
+ continue;
60778
+ }
60779
+ if (ts.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
60780
+ componentExpression = parent;
60781
+ continue;
60782
+ }
60783
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
60784
+ return node.name?.text ?? null;
60785
+ }
60786
+ }
60787
+ return null;
60788
+ };
60789
+ const isComponentFunction = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
60790
+ const isDefaultExportedComponent = (record, node, componentName) => {
60791
+ if (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true;
60792
+ return record.sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression) && statement.expression.text === componentName);
60793
+ };
60794
+ const findEnclosingComponent = (record, diagnostic) => {
60795
+ if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
60796
+ const lineIndex = diagnostic.line - 1;
60797
+ const lineStarts = record.sourceFile.getLineStarts();
60798
+ const lineStart = lineStarts[lineIndex];
60799
+ if (lineStart === void 0) return null;
60800
+ const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
60801
+ const lineEnd = Math.max(lineStart, nextLineStart - 1);
60802
+ const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
60803
+ const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
60804
+ let component = null;
60805
+ let componentWidth = Number.POSITIVE_INFINITY;
60806
+ const visit = (node) => {
60807
+ if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
60808
+ if (isComponentFunction(node)) {
60809
+ const name = getFunctionName(node);
60810
+ const width = node.end - node.getStart(record.sourceFile);
60811
+ if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
60812
+ component = {
60813
+ isDefaultExport: isDefaultExportedComponent(record, node, name),
60814
+ name,
60815
+ node,
60816
+ record
60817
+ };
60818
+ componentWidth = width;
60819
+ }
60820
+ }
60821
+ ts.forEachChild(node, visit);
60822
+ };
60823
+ visit(record.sourceFile);
60824
+ return component;
60825
+ };
60826
+ const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
60827
+ if (!moduleSpecifier.startsWith(".")) return false;
60828
+ const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
60829
+ const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
60830
+ const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
60831
+ return [
60832
+ unresolvedPath,
60833
+ ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
60834
+ ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
60835
+ ].includes(componentRecord.absolutePath);
60836
+ };
60837
+ const getComponentTagNames = (record, component) => {
60838
+ if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
60839
+ const tagNames = /* @__PURE__ */ new Set();
60840
+ for (const statement of record.sourceFile.statements) {
60841
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
60842
+ const importClause = statement.importClause;
60843
+ if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
60844
+ const namedBindings = importClause?.namedBindings;
60845
+ if (namedBindings !== void 0 && ts.isNamedImports(namedBindings)) {
60846
+ for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
60847
+ }
60848
+ }
60849
+ return tagNames;
60850
+ };
60851
+ const getTransparentCall = (functionNode) => {
60852
+ if (functionNode.body === void 0) return null;
60853
+ if (!ts.isBlock(functionNode.body)) {
60854
+ const expression = unwrapExpression(functionNode.body);
60855
+ return ts.isCallExpression(expression) ? expression : null;
60856
+ }
60857
+ if (functionNode.body.statements.length !== 1) return null;
60858
+ const statement = functionNode.body.statements[0];
60859
+ if (statement === void 0) return null;
60860
+ const expression = ts.isExpressionStatement(statement) || ts.isReturnStatement(statement) ? statement.expression : void 0;
60861
+ if (expression === void 0) return null;
60862
+ const unwrappedExpression = unwrapExpression(expression);
60863
+ return ts.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
60864
+ };
60865
+ const resolveTransparentTarget = (functionNode) => {
60866
+ const call = getTransparentCall(functionNode);
60867
+ if (call === null) return null;
60868
+ const target = unwrapExpression(call.expression);
60869
+ if (!ts.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
60870
+ for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
60871
+ const argument = call.arguments[parameterIndex];
60872
+ const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
60873
+ if (!ts.isIdentifier(parameter.name) || unwrappedArgument === null || !ts.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
60874
+ }
60875
+ return target.text;
60876
+ };
60877
+ const getForwardingFunction = (declaration) => {
60878
+ if (ts.isFunctionDeclaration(declaration)) return declaration;
60879
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
60880
+ const initializer = unwrapExpression(declaration.initializer);
60881
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer;
60882
+ if (ts.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
60883
+ const callback = initializer.arguments[0];
60884
+ if (callback !== void 0 && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return callback;
60885
+ }
60886
+ return null;
60887
+ };
60888
+ const getDeclaredBindingName = (node) => {
60889
+ 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;
60890
+ if (ts.isImportClause(node)) return node.name?.text ?? null;
60891
+ if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) return node.name.text;
60892
+ return null;
60893
+ };
60894
+ const findBindingScope = (node) => {
60895
+ let currentNode = node;
60896
+ while (currentNode !== void 0) {
60897
+ if (ts.isParameter(currentNode) && ts.isFunctionLike(currentNode.parent)) return currentNode.parent;
60898
+ if (ts.isBlock(currentNode) || ts.isModuleBlock(currentNode) || ts.isSourceFile(currentNode)) return currentNode;
60899
+ currentNode = currentNode.parent;
60900
+ }
60901
+ return node.getSourceFile();
60902
+ };
60903
+ const findBindingDeclaration = (record, binding) => {
60904
+ const usagePosition = binding.getStart(record.sourceFile);
60905
+ const candidates = [];
60906
+ const visit = (node) => {
60907
+ if (getDeclaredBindingName(node) === binding.text) {
60908
+ const scope = findBindingScope(node);
60909
+ const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
60910
+ 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;
60911
+ if (isVisibleScope && isVisibleDeclaration) candidates.push({
60912
+ declaration: node,
60913
+ scope
60914
+ });
60915
+ }
60916
+ ts.forEachChild(node, visit);
60917
+ };
60918
+ visit(record.sourceFile);
60919
+ let nearestCandidate = null;
60920
+ let nearestScopeWidth = Number.POSITIVE_INFINITY;
60921
+ let isNearestScopeAmbiguous = false;
60922
+ for (const candidate of candidates) {
60923
+ const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
60924
+ if (scopeWidth < nearestScopeWidth) {
60925
+ nearestCandidate = candidate;
60926
+ nearestScopeWidth = scopeWidth;
60927
+ isNearestScopeAmbiguous = false;
60928
+ } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
60929
+ }
60930
+ if (isNearestScopeAmbiguous) return null;
60931
+ const declaration = nearestCandidate?.declaration;
60932
+ return declaration !== void 0 && (ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration)) ? declaration : null;
60933
+ };
60934
+ const resolveHandlerBinding = (record, binding) => {
60935
+ const declaration = findBindingDeclaration(record, binding);
60936
+ if (declaration === null) return binding.text;
60937
+ const forwardingFunction = getForwardingFunction(declaration);
60938
+ const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
60939
+ return target === null || target === binding.text ? binding.text : target;
60940
+ };
60941
+ const getJsxTagName = (node) => ts.isIdentifier(node.tagName) ? node.tagName.text : null;
60942
+ const getHandlerBinding = (node, propName) => {
60943
+ for (const property of node.attributes.properties) {
60944
+ if (!ts.isJsxAttribute(property) || property.name.getText() !== propName) continue;
60945
+ const initializer = property.initializer;
60946
+ if (initializer === void 0 || !ts.isJsxExpression(initializer)) return null;
60947
+ if (initializer.expression === void 0) return null;
60948
+ const expression = unwrapExpression(initializer.expression);
60949
+ return ts.isIdentifier(expression) ? expression : null;
60950
+ }
60951
+ return null;
60952
+ };
60953
+ const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
60954
+ const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
60955
+ const cachedAliases = state.aliasesByComponent.get(cacheKey);
60956
+ if (cachedAliases !== void 0) return cachedAliases;
60957
+ const bindingsByPropName = /* @__PURE__ */ new Map();
60958
+ const unresolvedPropNames = /* @__PURE__ */ new Set();
60959
+ let callsiteCount = 0;
60960
+ for (const record of listSourceRecords(rootDirectory, state)) {
60961
+ const tagNames = getComponentTagNames(record, component);
60962
+ if (tagNames.size === 0) continue;
60963
+ const visit = (node) => {
60964
+ if (record === component.record && node === component.node) return;
60965
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
60966
+ const tagName = getJsxTagName(node);
60967
+ if (tagName !== null && tagNames.has(tagName)) {
60968
+ callsiteCount += 1;
60969
+ for (const propName of propNames) {
60970
+ const binding = getHandlerBinding(node, propName);
60971
+ if (binding === null) {
60972
+ unresolvedPropNames.add(propName);
60973
+ continue;
60974
+ }
60975
+ const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
60976
+ bindings.add(resolveHandlerBinding(record, binding));
60977
+ bindingsByPropName.set(propName, bindings);
60978
+ }
60979
+ }
60980
+ }
60981
+ ts.forEachChild(node, visit);
60982
+ };
60983
+ visit(record.sourceFile);
60984
+ }
60985
+ const aliases = /* @__PURE__ */ new Map();
60986
+ if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
60987
+ if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
60988
+ const [bindingName] = bindings;
60989
+ if (bindingName !== void 0) aliases.set(propName, bindingName);
60990
+ }
60991
+ state.aliasesByComponent.set(cacheKey, aliases);
60992
+ return aliases;
60993
+ };
60994
+ const normalizeForwardedHandlers = (evidence, aliases) => {
60995
+ let normalizedEvidence = evidence;
60996
+ for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
60997
+ return normalizedEvidence;
60998
+ };
60999
+ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
61000
+ const state = {
61001
+ aliasesByComponent: /* @__PURE__ */ new Map(),
61002
+ recordByFilePath: /* @__PURE__ */ new Map(),
61003
+ sourceRecords: null
61004
+ };
61005
+ return (diagnostic) => {
61006
+ const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
61007
+ if (record === null || !Number.isInteger(diagnostic.line)) return null;
61008
+ const sourceLines = record.sourceText.split(/\r?\n/);
61009
+ const startLineIndex = Math.max(0, diagnostic.line - 1);
61010
+ const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
61011
+ const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
61012
+ if (!options.resolveForwardedHandlers) return evidence;
61013
+ const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
61014
+ if (propNames.size === 0) return evidence;
61015
+ const component = findEnclosingComponent(record, diagnostic);
61016
+ if (component === null) return evidence;
61017
+ return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
61018
+ };
61019
+ };
61020
+ //#endregion
60404
61021
  //#region src/cli/utils/build-no-score-message.ts
60405
61022
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
60406
61023
  const buildNoScoreMessage = (isScoreDisabled) => {
@@ -61644,6 +62261,8 @@ const runBaselineComparison = async (params) => {
61644
62261
  directory: params.directory,
61645
62262
  ref: params.baselineRef,
61646
62263
  files: params.options.includePaths,
62264
+ baseFiles: params.baseFiles,
62265
+ headFiles: params.headFiles,
61647
62266
  tempDirectory
61648
62267
  }).catch((error) => {
61649
62268
  rmSync(tempDirectory, {
@@ -61652,7 +62271,24 @@ const runBaselineComparison = async (params) => {
61652
62271
  });
61653
62272
  throw error;
61654
62273
  });
62274
+ if (snapshot === null) {
62275
+ rmSync(tempDirectory, {
62276
+ recursive: true,
62277
+ force: true
62278
+ });
62279
+ return null;
62280
+ }
61655
62281
  try {
62282
+ if (!snapshot.isComplete) return null;
62283
+ const analyzedHeadFiles = new Set(params.headAnalyzedFiles.map(toForwardSlashes));
62284
+ const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
62285
+ const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
62286
+ const expectedHeadFiles = new Set(trackedHeadFiles);
62287
+ for (const filePath of params.options.includePaths) {
62288
+ const normalizedFilePath = toForwardSlashes(filePath);
62289
+ if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
62290
+ }
62291
+ if (filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
61656
62292
  const baseLayers = buildRuntimeLayers({
61657
62293
  directory: snapshot.tempDirectory,
61658
62294
  hasConfigOverride: true,
@@ -61668,7 +62304,7 @@ const runBaselineComparison = async (params) => {
61668
62304
  });
61669
62305
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
61670
62306
  directory: snapshot.tempDirectory,
61671
- includePaths: params.options.includePaths,
62307
+ includePaths: snapshot.materializedFiles,
61672
62308
  customRulesOnly: params.options.customRulesOnly,
61673
62309
  respectInlineDisables: params.options.respectInlineDisables,
61674
62310
  warnings: params.options.warnings,
@@ -61685,18 +62321,22 @@ const runBaselineComparison = async (params) => {
61685
62321
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
61686
62322
  }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
61687
62323
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
62324
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
61688
62325
  const delta = computeDiagnosticDelta({
61689
62326
  headDiagnostics: params.headDiagnostics,
61690
62327
  baseDiagnostics: baseOutput.diagnostics,
61691
62328
  readHeadLine: createSourceLineReader(params.directory),
61692
- readBaseLine: createSourceLineReader(snapshot.tempDirectory)
62329
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
62330
+ readHeadEvidence: createDiagnosticEvidenceReader(params.directory, { resolveForwardedHandlers: true }),
62331
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
61693
62332
  });
61694
62333
  return {
61695
62334
  displayDiagnostics: delta.newDiagnostics,
61696
62335
  baselineDelta: {
61697
62336
  baseRef: params.baselineRef,
61698
- fixedCount: delta.fixedCount,
61699
- baseTotalCount: baseOutput.diagnostics.length
62337
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : delta.fixedCount,
62338
+ baseTotalCount: baseOutput.diagnostics.length,
62339
+ crossFileMatchCount: delta.crossFileMatchCount
61700
62340
  }
61701
62341
  };
61702
62342
  } finally {
@@ -61803,6 +62443,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
61803
62443
  headDiagnostics: output.diagnostics,
61804
62444
  resolvedNodeBinaryPath,
61805
62445
  baselineRef: options.baseline.ref,
62446
+ baseFiles: options.baseline.baseFiles,
62447
+ headFiles: options.baseline.headFiles,
62448
+ headAnalyzedFiles: output.analyzedFiles,
61806
62449
  deadlineEpochMs
61807
62450
  });
61808
62451
  if (comparison) {
@@ -64087,20 +64730,25 @@ const resolveProjectRelativeDirectory = (rootDirectory, projectDirectory) => {
64087
64730
  return relativeProjectDirectory;
64088
64731
  };
64089
64732
  //#endregion
64090
- //#region src/cli/utils/resolve-project-diff-include-paths.ts
64091
- const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64733
+ //#region src/cli/utils/resolve-project-source-file-paths.ts
64734
+ const resolveProjectSourceFilePaths = (rootDirectory, projectDirectory, filePaths) => {
64092
64735
  const relativeProjectDirectory = resolveProjectRelativeDirectory(rootDirectory, projectDirectory);
64093
64736
  if (relativeProjectDirectory === null) return [];
64094
- const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
64095
- if (relativeProjectDirectory.length === 0) return changedSourceFiles;
64737
+ const sourceFilePaths = filterSourceFiles([...filePaths]);
64738
+ if (relativeProjectDirectory.length === 0) return sourceFilePaths;
64096
64739
  const projectPrefix = `${relativeProjectDirectory}/`;
64097
- return changedSourceFiles.flatMap((filePath) => {
64740
+ return sourceFilePaths.flatMap((filePath) => {
64098
64741
  const normalizedFilePath = toForwardSlashes(filePath);
64099
64742
  if (!normalizedFilePath.startsWith(projectPrefix)) return [];
64100
64743
  const projectRelativePath = normalizedFilePath.slice(projectPrefix.length);
64101
64744
  return projectRelativePath.length > 0 ? [projectRelativePath] : [];
64102
64745
  });
64103
64746
  };
64747
+ //#endregion
64748
+ //#region src/cli/utils/resolve-project-diff-include-paths.ts
64749
+ const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
64750
+ return resolveProjectSourceFilePaths(rootDirectory, projectDirectory, diffInfo.changedFiles);
64751
+ };
64104
64752
  /**
64105
64753
  * Re-keys repo-relative changed line ranges (from `getChangedLineRanges`) to
64106
64754
  * the scanned project, mirroring `resolveProjectDiffIncludePaths`'s boundary
@@ -64639,6 +65287,7 @@ const inspectAction = async (directory, flags) => {
64639
65287
  const isDiffMode = scope !== "full";
64640
65288
  const comparisonBaseRef = isDiffMode && diffInfo && !diffInfo.isCurrentChanges ? diffInfo.baseSha ? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseSha) : diffInfo.diffBaseRef ?? await resolveMergeBaseRef(resolvedDirectory, diffInfo.baseBranch) : null;
64641
65289
  const baselineRef = scope === "changed" ? comparisonBaseRef : null;
65290
+ const baselineDiffPlan = baselineRef === null ? null : await getBaselineDiffPlan(resolvedDirectory, baselineRef);
64642
65291
  const linesBaseRef = diffInfo?.isCurrentChanges ? "HEAD" : comparisonBaseRef;
64643
65292
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64644
65293
  directory: resolvedDirectory,
@@ -64669,10 +65318,13 @@ const inspectAction = async (directory, flags) => {
64669
65318
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
64670
65319
  let includePaths;
64671
65320
  let supplyChainManifestChanged = false;
65321
+ const projectBaselineBaseFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.baseFiles);
65322
+ const projectBaselineHeadFiles = baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(resolvedDirectory, scanDirectory, baselineDiffPlan.headFiles);
64672
65323
  if (isDiffMode) {
64673
65324
  const changedSourceFiles = diffInfo === null ? [] : resolveProjectDiffIncludePaths(resolvedDirectory, scanDirectory, diffInfo);
64674
65325
  supplyChainManifestChanged = supplyChainEnabled && diffInfo !== null && projectManifestChanged(resolvedDirectory, scanDirectory, diffInfo);
64675
- if (changedSourceFiles.length === 0 && !supplyChainManifestChanged) {
65326
+ const hasBaselineOnlyFiles = (projectBaselineBaseFiles?.length ?? 0) > 0;
65327
+ if (changedSourceFiles.length === 0 && !supplyChainManifestChanged && !hasBaselineOnlyFiles) {
64676
65328
  if (!isQuiet) {
64677
65329
  cliLogger.dim(`No changed source files in ${scanDirectory}, skipping.`);
64678
65330
  cliLogger.break();
@@ -64680,6 +65332,7 @@ const inspectAction = async (directory, flags) => {
64680
65332
  return null;
64681
65333
  }
64682
65334
  includePaths = [...changedSourceFiles];
65335
+ if (includePaths.length === 0 && hasBaselineOnlyFiles) includePaths.push(...projectBaselineBaseFiles ?? []);
64683
65336
  if (supplyChainManifestChanged) includePaths.push("package.json");
64684
65337
  }
64685
65338
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
@@ -64691,7 +65344,11 @@ const inspectAction = async (directory, flags) => {
64691
65344
  configSourceDirectory: projectConfigSourceDirectory ?? void 0,
64692
65345
  suppressRendering: isMultiProject,
64693
65346
  concurrentScan: isMultiProject,
64694
- baseline: baselineRef ? { ref: baselineRef } : void 0,
65347
+ baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
65348
+ ref: baselineRef,
65349
+ baseFiles: projectBaselineBaseFiles,
65350
+ headFiles: projectBaselineHeadFiles
65351
+ } : void 0,
64695
65352
  changedLineRanges: scope === "lines" && changedLineRanges !== null ? resolveProjectChangedLineRanges(resolvedDirectory, scanDirectory, changedLineRanges) : void 0,
64696
65353
  supplyChainManifestChanged
64697
65354
  });
@@ -65891,4 +66548,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65891
66548
  export {};
65892
66549
 
65893
66550
  //# sourceMappingURL=cli.js.map
65894
- //# debugId=9e32582f-b296-5216-a028-7cc19ed83931
66551
+ //# debugId=81bdc95e-12f2-565f-a05c-7b8a60a98f81