react-doctor 0.7.5-dev.dbd4067 → 0.7.6-dev.0150fc9

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]="72ed96f2-03b8-5285-a0e9-be502d986219")}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]="9a5c1a3c-7706-5a23-bca4-ddddd2f085af")}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";
@@ -36241,7 +36241,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36241
36241
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36242
36242
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36243
36243
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36244
- const CORE_PACKAGE_VERSION = "0.7.5";
36244
+ const CORE_PACKAGE_VERSION = "0.7.6";
36245
36245
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36246
36246
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36247
36247
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36341,10 +36341,12 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36341
36341
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36342
36342
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36343
36343
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36344
+ const UTF8_BOM_CHARACTER = "";
36345
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
36344
36346
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36345
36347
  const readPackageJsonUncached = (packageJsonPath) => {
36346
36348
  try {
36347
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
36349
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
36348
36350
  } catch (error) {
36349
36351
  if (error instanceof SyntaxError) return {};
36350
36352
  if (isErrnoException(error)) {
@@ -40017,7 +40019,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
40017
40019
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
40018
40020
  const PACKAGE_JSON_FILE = "package.json";
40019
40021
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
40020
- const UTF8_BOM_CHAR = "";
40021
40022
  const HARDENING_SETTING_KEYS = new Set([
40022
40023
  "minimumReleaseAge",
40023
40024
  "blockExoticSubdeps",
@@ -40042,12 +40043,11 @@ const stripInlineComment = (rawValue) => {
40042
40043
  return rawValue;
40043
40044
  };
40044
40045
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
40045
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
40046
40046
  const parseHardeningSettings = (content) => {
40047
40047
  let minimumReleaseAge = null;
40048
40048
  let blockExoticSubdeps = null;
40049
40049
  let trustPolicy = null;
40050
- const lines = stripBom(content).split(/\r?\n/);
40050
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
40051
40051
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
40052
40052
  const lineText = lines[lineIndex];
40053
40053
  if (lineText === void 0) continue;
@@ -40081,7 +40081,7 @@ const isPnpmManagedProject = (rootDirectory) => {
40081
40081
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
40082
40082
  if (!isFile(packageJsonPath)) return false;
40083
40083
  try {
40084
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
40084
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
40085
40085
  const packageJson = JSON.parse(packageJsonRaw);
40086
40086
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
40087
40087
  } catch {
@@ -40762,9 +40762,7 @@ const setColorEnabled = (enabled) => {
40762
40762
  highlighter.gray = colors.gray;
40763
40763
  highlighter.bold = colors.bold;
40764
40764
  };
40765
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
40766
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
40767
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
40765
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
40768
40766
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
40769
40767
  const FALSY_VALUES = new Set([
40770
40768
  "false",
@@ -41603,11 +41601,11 @@ const resolveDeadCodeTimeout = (input) => {
41603
41601
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
41604
41602
  };
41605
41603
  };
41606
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
41604
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
41607
41605
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
41608
41606
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
41609
41607
  return listSourceFiles(rootDirectory).filter((filePath) => {
41610
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
41608
+ if (!isLintableSourceFile(filePath)) return false;
41611
41609
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
41612
41610
  });
41613
41611
  };
@@ -42368,7 +42366,7 @@ const normalizeConfigForHash = (config) => {
42368
42366
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
42369
42367
  return clone;
42370
42368
  };
42371
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
42369
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").update("\0").update(input.respectInlineDisables ? "respect-inline-disables" : "audit-mode").digest("hex");
42372
42370
  /**
42373
42371
  * Loads a plugin module via the local require resolver and extracts
42374
42372
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -42812,6 +42810,23 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
42812
42810
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
42813
42811
  return null;
42814
42812
  };
42813
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
42814
+ ["no-initialize-state", 0],
42815
+ ["no-adjust-state-on-prop-change", 1],
42816
+ ["no-derived-state", 2],
42817
+ ["no-derived-state-effect", 3]
42818
+ ]);
42819
+ const doDiagnosticSpansOverlap = (first, second) => {
42820
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
42821
+ if (first.offset !== void 0 && second.offset !== void 0) {
42822
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
42823
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
42824
+ return first.offset < secondEnd && second.offset < firstEnd;
42825
+ }
42826
+ const firstEndLine = first.endLine ?? first.line;
42827
+ const secondEndLine = second.endLine ?? second.line;
42828
+ return first.line <= secondEndLine && second.line <= firstEndLine;
42829
+ };
42815
42830
  const dedupeDiagnostics = (diagnostics) => {
42816
42831
  const seenKeys = /* @__PURE__ */ new Set();
42817
42832
  const uniqueDiagnostics = [];
@@ -42819,6 +42834,16 @@ const dedupeDiagnostics = (diagnostics) => {
42819
42834
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
42820
42835
  if (seenKeys.has(key)) continue;
42821
42836
  seenKeys.add(key);
42837
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
42838
+ if (priority !== void 0) {
42839
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
42840
+ if (overlappingDiagnosticIndex >= 0) {
42841
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
42842
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
42843
+ if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
42844
+ continue;
42845
+ }
42846
+ }
42822
42847
  uniqueDiagnostics.push(diagnostic);
42823
42848
  }
42824
42849
  return uniqueDiagnostics;
@@ -42861,6 +42886,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42861
42886
  if (errors.length > 0) throw errors[0];
42862
42887
  return results;
42863
42888
  };
42889
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
42890
+ let lineNumber = 1;
42891
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
42892
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
42893
+ return lineNumber;
42894
+ };
42864
42895
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
42865
42896
  const ACCESSORS_MIN_VERSION = "3.15.0";
42866
42897
  const supportsAccessors = (reanimatedVersion) => {
@@ -43432,13 +43463,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43432
43463
  visit(sourceFile);
43433
43464
  return foundRange;
43434
43465
  };
43435
- const NEWLINE_BYTE = 10;
43436
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43437
- let lineNumber = 1;
43438
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43439
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43440
- return lineNumber;
43441
- };
43442
43466
  /**
43443
43467
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43444
43468
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -43643,8 +43667,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43643
43667
  }
43644
43668
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43645
43669
  const minifiedFileCache = /* @__PURE__ */ new Map();
43670
+ const sourceBufferCache = /* @__PURE__ */ new Map();
43671
+ const sourceLinesCache = /* @__PURE__ */ new Map();
43672
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43673
+ const readSourceBuffer = (filename) => {
43674
+ const absolutePath = resolveAbsolutePath(filename);
43675
+ const cached = sourceBufferCache.get(absolutePath);
43676
+ if (cached !== void 0) return cached;
43677
+ let sourceBuffer;
43678
+ try {
43679
+ sourceBuffer = NFS.readFileSync(absolutePath);
43680
+ } catch {
43681
+ sourceBuffer = null;
43682
+ }
43683
+ sourceBufferCache.set(absolutePath, sourceBuffer);
43684
+ return sourceBuffer;
43685
+ };
43686
+ const readSourceLines = (filename) => {
43687
+ const absolutePath = resolveAbsolutePath(filename);
43688
+ const cached = sourceLinesCache.get(absolutePath);
43689
+ if (cached !== void 0) return cached;
43690
+ const sourceBuffer = readSourceBuffer(filename);
43691
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
43692
+ sourceLinesCache.set(absolutePath, sourceLines);
43693
+ return sourceLines;
43694
+ };
43646
43695
  const isMinifiedDiagnosticFile = (filename) => {
43647
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
43696
+ const absolutePath = resolveAbsolutePath(filename);
43648
43697
  const cached = minifiedFileCache.get(absolutePath);
43649
43698
  if (cached !== void 0) return cached;
43650
43699
  const minified = isMinifiedSource(absolutePath);
@@ -43657,8 +43706,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43657
43706
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
43658
43707
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
43659
43708
  const primarySpan = primaryLabel?.span;
43709
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
43660
43710
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
43661
43711
  const category = resolveDiagnosticCategory(plugin, rule);
43712
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
43713
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
43714
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
43715
+ const primaryLine = sourceLines?.[primaryLineIndex];
43716
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
43717
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
43718
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
43719
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
43662
43720
  return {
43663
43721
  filePath: normalizedFilePath,
43664
43722
  plugin,
@@ -43672,10 +43730,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43672
43730
  column: primarySpan?.column ?? 0,
43673
43731
  ...primarySpan ? {
43674
43732
  offset: primarySpan.offset,
43675
- length: primarySpan.length
43733
+ length: primarySpan.length,
43734
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
43676
43735
  } : {},
43677
43736
  category,
43678
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
43737
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
43679
43738
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43680
43739
  };
43681
43740
  }), rootDirectory);
@@ -44369,7 +44428,7 @@ const runOxlint = async (options) => {
44369
44428
  };
44370
44429
  }
44371
44430
  };
44372
- if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
44431
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
44373
44432
  const rulesetHash = computeRulesetHash({
44374
44433
  config: buildConfig({
44375
44434
  extendsPaths: [],
@@ -44377,7 +44436,8 @@ const runOxlint = async (options) => {
44377
44436
  }),
44378
44437
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
44379
44438
  ignorePatterns: combinedPatterns,
44380
- tsconfigContent
44439
+ tsconfigContent,
44440
+ respectInlineDisables
44381
44441
  });
44382
44442
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
44383
44443
  const cacheKeyByFile = /* @__PURE__ */ new Map();
@@ -44420,7 +44480,8 @@ const runOxlint = async (options) => {
44420
44480
  }),
44421
44481
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
44422
44482
  ignorePatterns: combinedPatterns,
44423
- tsconfigContent
44483
+ tsconfigContent,
44484
+ respectInlineDisables
44424
44485
  })) : null;
44425
44486
  const probeAnswers = createSidecarProbeAnswerResolver({
44426
44487
  rootDirectory,
@@ -45460,7 +45521,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45460
45521
  directory: scanDirectory,
45461
45522
  repo
45462
45523
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
45463
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
45524
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
45464
45525
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
45465
45526
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
45466
45527
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -46638,7 +46699,7 @@ const makeNoopConsole = () => ({
46638
46699
  });
46639
46700
  //#endregion
46640
46701
  //#region src/cli/utils/version.ts
46641
- const VERSION = "0.7.5-dev.dbd4067";
46702
+ const VERSION = "0.7.6-dev.0150fc9";
46642
46703
  //#endregion
46643
46704
  //#region src/cli/utils/json-mode.ts
46644
46705
  let context = null;
@@ -47007,13 +47068,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47007
47068
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47008
47069
  * standard `SENTRY_RELEASE` override.
47009
47070
  */
47010
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.5-dev.dbd4067`;
47071
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.0150fc9`;
47011
47072
  /**
47012
47073
  * Deployment environment shown in Sentry's environment filter. Defaults to
47013
47074
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47014
47075
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47015
47076
  */
47016
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.5-dev.dbd4067") ? "development" : "production");
47077
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.0150fc9") ? "development" : "production");
47017
47078
  /**
47018
47079
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47019
47080
  * (set to `0` to disable tracing) and falls back to
@@ -50778,7 +50839,7 @@ const BLOCKING_SUMMARY = {
50778
50839
  const SCOPE_SUMMARY = {
50779
50840
  full: "Scan the whole project on every run",
50780
50841
  files: "Report every issue in changed files",
50781
- lines: "Report issues on changed lines",
50842
+ lines: "Report issues whose source spans touch changed lines",
50782
50843
  changed: "Report only the issues a change introduces"
50783
50844
  };
50784
50845
  const gatesEqual = (left, right) => left.blocking === right.blocking && left.scope === right.scope && left.comment === right.comment && left.reviewComments === right.reviewComments && left.commitStatus === right.commitStatus;
@@ -59173,7 +59234,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59173
59234
  return null;
59174
59235
  }
59175
59236
  if (!stats.isFile()) return null;
59176
- if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59237
+ if (stats.size > 10485760) return null;
59177
59238
  return hashFileContents(absolutePath);
59178
59239
  };
59179
59240
  const buildWorktreeFingerprint = (projectDirectory) => {
@@ -59647,7 +59708,9 @@ const buildScanAttributes = (input) => {
59647
59708
  hasCustomConfig: input.hasCustomConfig,
59648
59709
  rulesConfigured: ruleKeys.length,
59649
59710
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
59650
- fileCount: input.result?.scannedFileCount ?? null
59711
+ fileCount: input.result?.scannedFileCount ?? null,
59712
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
59713
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
59651
59714
  });
59652
59715
  };
59653
59716
  /**
@@ -59716,6 +59779,12 @@ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching
59716
59779
  const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
59717
59780
  const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
59718
59781
  //#endregion
59782
+ //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
59783
+ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
59784
+ const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
59785
+ return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
59786
+ };
59787
+ //#endregion
59719
59788
  //#region src/cli/utils/materialize-baseline-files.ts
59720
59789
  /**
59721
59790
  * Materializes the `ref` version of `files` into `tempDirectory` (via
@@ -60901,7 +60970,7 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
60901
60970
  const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(directory, diagnostic.filePath) : diagnostic.filePath);
60902
60971
  const ranges = rangesByFile.get(relativePath);
60903
60972
  if (ranges === void 0) return false;
60904
- return ranges.some(([start, end]) => diagnostic.line >= start && diagnostic.line <= end);
60973
+ return diagnosticIntersectsLineRanges(diagnostic, ranges);
60905
60974
  };
60906
60975
  };
60907
60976
  const buildIgnoredTags = (userConfig) => {
@@ -65152,7 +65221,7 @@ ${highlighter.dim("Learn more:")}
65152
65221
  ${highlighter.info(CI_URL)}
65153
65222
  `;
65154
65223
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65155
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65224
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65156
65225
  program.action(inspectAction);
65157
65226
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
65158
65227
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -65202,4 +65271,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65202
65271
  export {};
65203
65272
 
65204
65273
  //# sourceMappingURL=cli.js.map
65205
- //# debugId=72ed96f2-03b8-5285-a0e9-be502d986219
65274
+ //# debugId=9a5c1a3c-7706-5a23-bca4-ddddd2f085af
package/dist/index.d.ts CHANGED
@@ -9654,6 +9654,7 @@ interface DiagnoseOptions {
9654
9654
  /** See `ReactDoctorConfig.deadCode`. Ignored in diff mode. */
9655
9655
  deadCode?: boolean;
9656
9656
  verbose?: boolean;
9657
+ /** Restrict linting to these supported JS/TS source files. */
9657
9658
  includePaths?: string[];
9658
9659
  /**
9659
9660
  * Per-call override for `ReactDoctorConfig.respectInlineDisables`.
@@ -9800,9 +9801,9 @@ interface InspectResult {
9800
9801
  scanElapsedMilliseconds?: number;
9801
9802
  /**
9802
9803
  * Per-file lint cache outcome: files served from cache, and total files
9803
- * considered. Both absent when the cache was off or bypassed (audit mode,
9804
- * adopted `extends`, user plugins). The CLI projects these onto the Sentry
9805
- * wide event as `lint.cacheHitRatio`.
9804
+ * considered. Both absent when the cache was off or bypassed (adopted
9805
+ * `extends`, user plugins, React Compiler). The CLI projects these onto the
9806
+ * Sentry wide event as `lint.cacheHitRatio`.
9806
9807
  */
9807
9808
  lintCacheHitFileCount?: number | null;
9808
9809
  lintCacheTotalFileCount?: number | null;
@@ -9915,9 +9916,9 @@ interface JsonReportProjectEntry {
9915
9916
  skippedCheckReasons?: Record<string, string>;
9916
9917
  /**
9917
9918
  * Number of source files this scan's linter examined. In diff / changed
9918
- * mode it's the count of changed React-eligible files (`.tsx`/`.jsx` plus
9919
- * framework entry files); in a full scan it's the whole source tree. `0`
9920
- * in a diff scan means the changed files held nothing React Doctor lints —
9919
+ * mode it's the count of changed supported JS/TS source files; in a full
9920
+ * scan it's the whole source tree. `0` in a diff scan means the changed
9921
+ * files held nothing React Doctor lints —
9921
9922
  * the GitHub Action reads that as "nothing to report" (skips the PR comment;
9922
9923
  * the commit status says "skipped"). Optional: absent on reports from
9923
9924
  * constructors that don't track it (e.g. `toJsonReport`).
package/dist/index.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]="6430a76b-a36a-5cf5-9ca0-cc1ca8952f41")}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]="11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -32930,7 +32930,6 @@ const IGNORED_DIRECTORIES = new Set([
32930
32930
  "out",
32931
32931
  "storybook-static"
32932
32932
  ]);
32933
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32934
32933
  const MILLISECONDS_PER_SECOND = 1e3;
32935
32934
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32936
32935
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33040,7 +33039,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33040
33039
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33041
33040
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33042
33041
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33043
- const CORE_PACKAGE_VERSION = "0.7.5";
33042
+ const CORE_PACKAGE_VERSION = "0.7.6";
33044
33043
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33045
33044
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33046
33045
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33143,13 +33142,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33143
33142
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33144
33143
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33145
33144
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33145
+ const UTF8_BOM_CHARACTER = "";
33146
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33146
33147
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33147
33148
  const clearPackageJsonCache = () => {
33148
33149
  cachedPackageJsons.clear();
33149
33150
  };
33150
33151
  const readPackageJsonUncached = (packageJsonPath) => {
33151
33152
  try {
33152
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33153
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33153
33154
  } catch (error) {
33154
33155
  if (error instanceof SyntaxError) return {};
33155
33156
  if (isErrnoException(error)) {
@@ -36790,7 +36791,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36790
36791
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36791
36792
  const PACKAGE_JSON_FILE = "package.json";
36792
36793
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36793
- const UTF8_BOM_CHAR = "";
36794
36794
  const HARDENING_SETTING_KEYS = new Set([
36795
36795
  "minimumReleaseAge",
36796
36796
  "blockExoticSubdeps",
@@ -36815,12 +36815,11 @@ const stripInlineComment = (rawValue) => {
36815
36815
  return rawValue;
36816
36816
  };
36817
36817
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36818
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36819
36818
  const parseHardeningSettings = (content) => {
36820
36819
  let minimumReleaseAge = null;
36821
36820
  let blockExoticSubdeps = null;
36822
36821
  let trustPolicy = null;
36823
- const lines = stripBom(content).split(/\r?\n/);
36822
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36824
36823
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36825
36824
  const lineText = lines[lineIndex];
36826
36825
  if (lineText === void 0) continue;
@@ -36854,7 +36853,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36854
36853
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36855
36854
  if (!isFile(packageJsonPath)) return false;
36856
36855
  try {
36857
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36856
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36858
36857
  const packageJson = JSON.parse(packageJsonRaw);
36859
36858
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36860
36859
  } catch {
@@ -37516,9 +37515,7 @@ const highlighter = {
37516
37515
  gray: import_picocolors.default.gray,
37517
37516
  bold: import_picocolors.default.bold
37518
37517
  };
37519
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37520
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37521
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37518
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37522
37519
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37523
37520
  const FALSY_VALUES = new Set([
37524
37521
  "false",
@@ -38360,11 +38357,11 @@ const resolveDeadCodeTimeout = (input) => {
38360
38357
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38361
38358
  };
38362
38359
  };
38363
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38360
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38364
38361
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38365
38362
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38366
38363
  return listSourceFiles(rootDirectory).filter((filePath) => {
38367
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38364
+ if (!isLintableSourceFile(filePath)) return false;
38368
38365
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38369
38366
  });
38370
38367
  };
@@ -39125,7 +39122,7 @@ const normalizeConfigForHash = (config) => {
39125
39122
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
39126
39123
  return clone;
39127
39124
  };
39128
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
39125
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").update("\0").update(input.respectInlineDisables ? "respect-inline-disables" : "audit-mode").digest("hex");
39129
39126
  /**
39130
39127
  * Loads a plugin module via the local require resolver and extracts
39131
39128
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -39569,6 +39566,23 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39569
39566
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39570
39567
  return null;
39571
39568
  };
39569
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
39570
+ ["no-initialize-state", 0],
39571
+ ["no-adjust-state-on-prop-change", 1],
39572
+ ["no-derived-state", 2],
39573
+ ["no-derived-state-effect", 3]
39574
+ ]);
39575
+ const doDiagnosticSpansOverlap = (first, second) => {
39576
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39577
+ if (first.offset !== void 0 && second.offset !== void 0) {
39578
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39579
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39580
+ return first.offset < secondEnd && second.offset < firstEnd;
39581
+ }
39582
+ const firstEndLine = first.endLine ?? first.line;
39583
+ const secondEndLine = second.endLine ?? second.line;
39584
+ return first.line <= secondEndLine && second.line <= firstEndLine;
39585
+ };
39572
39586
  const dedupeDiagnostics = (diagnostics) => {
39573
39587
  const seenKeys = /* @__PURE__ */ new Set();
39574
39588
  const uniqueDiagnostics = [];
@@ -39576,6 +39590,16 @@ const dedupeDiagnostics = (diagnostics) => {
39576
39590
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39577
39591
  if (seenKeys.has(key)) continue;
39578
39592
  seenKeys.add(key);
39593
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39594
+ if (priority !== void 0) {
39595
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39596
+ if (overlappingDiagnosticIndex >= 0) {
39597
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39598
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39599
+ if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39600
+ continue;
39601
+ }
39602
+ }
39579
39603
  uniqueDiagnostics.push(diagnostic);
39580
39604
  }
39581
39605
  return uniqueDiagnostics;
@@ -39618,6 +39642,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39618
39642
  if (errors.length > 0) throw errors[0];
39619
39643
  return results;
39620
39644
  };
39645
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39646
+ let lineNumber = 1;
39647
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39648
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39649
+ return lineNumber;
39650
+ };
39621
39651
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
39622
39652
  const ACCESSORS_MIN_VERSION = "3.15.0";
39623
39653
  const supportsAccessors = (reanimatedVersion) => {
@@ -40189,13 +40219,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40189
40219
  visit(sourceFile);
40190
40220
  return foundRange;
40191
40221
  };
40192
- const NEWLINE_BYTE = 10;
40193
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40194
- let lineNumber = 1;
40195
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40196
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40197
- return lineNumber;
40198
- };
40199
40222
  /**
40200
40223
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40201
40224
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40400,8 +40423,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40400
40423
  }
40401
40424
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40402
40425
  const minifiedFileCache = /* @__PURE__ */ new Map();
40426
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40427
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40428
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40429
+ const readSourceBuffer = (filename) => {
40430
+ const absolutePath = resolveAbsolutePath(filename);
40431
+ const cached = sourceBufferCache.get(absolutePath);
40432
+ if (cached !== void 0) return cached;
40433
+ let sourceBuffer;
40434
+ try {
40435
+ sourceBuffer = NFS.readFileSync(absolutePath);
40436
+ } catch {
40437
+ sourceBuffer = null;
40438
+ }
40439
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40440
+ return sourceBuffer;
40441
+ };
40442
+ const readSourceLines = (filename) => {
40443
+ const absolutePath = resolveAbsolutePath(filename);
40444
+ const cached = sourceLinesCache.get(absolutePath);
40445
+ if (cached !== void 0) return cached;
40446
+ const sourceBuffer = readSourceBuffer(filename);
40447
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40448
+ sourceLinesCache.set(absolutePath, sourceLines);
40449
+ return sourceLines;
40450
+ };
40403
40451
  const isMinifiedDiagnosticFile = (filename) => {
40404
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40452
+ const absolutePath = resolveAbsolutePath(filename);
40405
40453
  const cached = minifiedFileCache.get(absolutePath);
40406
40454
  if (cached !== void 0) return cached;
40407
40455
  const minified = isMinifiedSource(absolutePath);
@@ -40414,8 +40462,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40414
40462
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40415
40463
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40416
40464
  const primarySpan = primaryLabel?.span;
40465
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40417
40466
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40418
40467
  const category = resolveDiagnosticCategory(plugin, rule);
40468
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40469
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40470
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40471
+ const primaryLine = sourceLines?.[primaryLineIndex];
40472
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40473
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40474
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40475
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40419
40476
  return {
40420
40477
  filePath: normalizedFilePath,
40421
40478
  plugin,
@@ -40429,10 +40486,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40429
40486
  column: primarySpan?.column ?? 0,
40430
40487
  ...primarySpan ? {
40431
40488
  offset: primarySpan.offset,
40432
- length: primarySpan.length
40489
+ length: primarySpan.length,
40490
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40433
40491
  } : {},
40434
40492
  category,
40435
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40493
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40436
40494
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40437
40495
  };
40438
40496
  }), rootDirectory);
@@ -41126,7 +41184,7 @@ const runOxlint = async (options) => {
41126
41184
  };
41127
41185
  }
41128
41186
  };
41129
- if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41187
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41130
41188
  const rulesetHash = computeRulesetHash({
41131
41189
  config: buildConfig({
41132
41190
  extendsPaths: [],
@@ -41134,7 +41192,8 @@ const runOxlint = async (options) => {
41134
41192
  }),
41135
41193
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41136
41194
  ignorePatterns: combinedPatterns,
41137
- tsconfigContent
41195
+ tsconfigContent,
41196
+ respectInlineDisables
41138
41197
  });
41139
41198
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
41140
41199
  const cacheKeyByFile = /* @__PURE__ */ new Map();
@@ -41177,7 +41236,8 @@ const runOxlint = async (options) => {
41177
41236
  }),
41178
41237
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41179
41238
  ignorePatterns: combinedPatterns,
41180
- tsconfigContent
41239
+ tsconfigContent,
41240
+ respectInlineDisables
41181
41241
  })) : null;
41182
41242
  const probeAnswers = createSidecarProbeAnswerResolver({
41183
41243
  rootDirectory,
@@ -42217,7 +42277,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42217
42277
  directory: scanDirectory,
42218
42278
  repo
42219
42279
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42220
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42280
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42221
42281
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42222
42282
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42223
42283
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -43181,4 +43241,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43181
43241
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43182
43242
 
43183
43243
  //# sourceMappingURL=index.js.map
43184
- //# debugId=6430a76b-a36a-5cf5-9ca0-cc1ca8952f41
43244
+ //# debugId=11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c
package/dist/lsp.js CHANGED
@@ -32959,7 +32959,6 @@ const IGNORED_DIRECTORIES = new Set([
32959
32959
  "out",
32960
32960
  "storybook-static"
32961
32961
  ]);
32962
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32963
32962
  const MILLISECONDS_PER_SECOND = 1e3;
32964
32963
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32965
32964
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33092,7 +33091,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33092
33091
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33093
33092
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33094
33093
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33095
- const CORE_PACKAGE_VERSION = "0.7.5";
33094
+ const CORE_PACKAGE_VERSION = "0.7.6";
33096
33095
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33097
33096
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33098
33097
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33195,13 +33194,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33195
33194
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33196
33195
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33197
33196
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33197
+ const UTF8_BOM_CHARACTER = "";
33198
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33198
33199
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33199
33200
  const clearPackageJsonCache = () => {
33200
33201
  cachedPackageJsons.clear();
33201
33202
  };
33202
33203
  const readPackageJsonUncached = (packageJsonPath) => {
33203
33204
  try {
33204
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33205
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33205
33206
  } catch (error) {
33206
33207
  if (error instanceof SyntaxError) return {};
33207
33208
  if (isErrnoException(error)) {
@@ -36768,7 +36769,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36768
36769
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36769
36770
  const PACKAGE_JSON_FILE = "package.json";
36770
36771
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36771
- const UTF8_BOM_CHAR = "";
36772
36772
  const HARDENING_SETTING_KEYS = new Set([
36773
36773
  "minimumReleaseAge",
36774
36774
  "blockExoticSubdeps",
@@ -36793,12 +36793,11 @@ const stripInlineComment = (rawValue) => {
36793
36793
  return rawValue;
36794
36794
  };
36795
36795
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36796
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36797
36796
  const parseHardeningSettings = (content) => {
36798
36797
  let minimumReleaseAge = null;
36799
36798
  let blockExoticSubdeps = null;
36800
36799
  let trustPolicy = null;
36801
- const lines = stripBom(content).split(/\r?\n/);
36800
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36802
36801
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36803
36802
  const lineText = lines[lineIndex];
36804
36803
  if (lineText === void 0) continue;
@@ -36832,7 +36831,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36832
36831
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36833
36832
  if (!isFile(packageJsonPath)) return false;
36834
36833
  try {
36835
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36834
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36836
36835
  const packageJson = JSON.parse(packageJsonRaw);
36837
36836
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36838
36837
  } catch {
@@ -37494,9 +37493,7 @@ const highlighter = {
37494
37493
  gray: import_picocolors.default.gray,
37495
37494
  bold: import_picocolors.default.bold
37496
37495
  };
37497
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37498
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37499
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37496
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37500
37497
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37501
37498
  const FALSY_VALUES = new Set([
37502
37499
  "false",
@@ -38338,11 +38335,11 @@ const resolveDeadCodeTimeout = (input) => {
38338
38335
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38339
38336
  };
38340
38337
  };
38341
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38338
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38342
38339
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38343
38340
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38344
38341
  return listSourceFiles(rootDirectory).filter((filePath) => {
38345
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38342
+ if (!isLintableSourceFile(filePath)) return false;
38346
38343
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38347
38344
  });
38348
38345
  };
@@ -39103,7 +39100,7 @@ const normalizeConfigForHash = (config) => {
39103
39100
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
39104
39101
  return clone;
39105
39102
  };
39106
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
39103
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").update("\0").update(input.respectInlineDisables ? "respect-inline-disables" : "audit-mode").digest("hex");
39107
39104
  /**
39108
39105
  * Loads a plugin module via the local require resolver and extracts
39109
39106
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -39547,6 +39544,23 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39547
39544
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39548
39545
  return null;
39549
39546
  };
39547
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
39548
+ ["no-initialize-state", 0],
39549
+ ["no-adjust-state-on-prop-change", 1],
39550
+ ["no-derived-state", 2],
39551
+ ["no-derived-state-effect", 3]
39552
+ ]);
39553
+ const doDiagnosticSpansOverlap = (first, second) => {
39554
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39555
+ if (first.offset !== void 0 && second.offset !== void 0) {
39556
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39557
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39558
+ return first.offset < secondEnd && second.offset < firstEnd;
39559
+ }
39560
+ const firstEndLine = first.endLine ?? first.line;
39561
+ const secondEndLine = second.endLine ?? second.line;
39562
+ return first.line <= secondEndLine && second.line <= firstEndLine;
39563
+ };
39550
39564
  const dedupeDiagnostics = (diagnostics) => {
39551
39565
  const seenKeys = /* @__PURE__ */ new Set();
39552
39566
  const uniqueDiagnostics = [];
@@ -39554,6 +39568,16 @@ const dedupeDiagnostics = (diagnostics) => {
39554
39568
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39555
39569
  if (seenKeys.has(key)) continue;
39556
39570
  seenKeys.add(key);
39571
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39572
+ if (priority !== void 0) {
39573
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39574
+ if (overlappingDiagnosticIndex >= 0) {
39575
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39576
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39577
+ if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39578
+ continue;
39579
+ }
39580
+ }
39557
39581
  uniqueDiagnostics.push(diagnostic);
39558
39582
  }
39559
39583
  return uniqueDiagnostics;
@@ -39596,6 +39620,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39596
39620
  if (errors.length > 0) throw errors[0];
39597
39621
  return results;
39598
39622
  };
39623
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
39624
+ let lineNumber = 1;
39625
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
39626
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
39627
+ return lineNumber;
39628
+ };
39599
39629
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
39600
39630
  const ACCESSORS_MIN_VERSION = "3.15.0";
39601
39631
  const supportsAccessors = (reanimatedVersion) => {
@@ -40167,13 +40197,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40167
40197
  visit(sourceFile);
40168
40198
  return foundRange;
40169
40199
  };
40170
- const NEWLINE_BYTE = 10;
40171
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40172
- let lineNumber = 1;
40173
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40174
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40175
- return lineNumber;
40176
- };
40177
40200
  /**
40178
40201
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40179
40202
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40378,8 +40401,33 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40378
40401
  }
40379
40402
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40380
40403
  const minifiedFileCache = /* @__PURE__ */ new Map();
40404
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40405
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40406
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40407
+ const readSourceBuffer = (filename) => {
40408
+ const absolutePath = resolveAbsolutePath(filename);
40409
+ const cached = sourceBufferCache.get(absolutePath);
40410
+ if (cached !== void 0) return cached;
40411
+ let sourceBuffer;
40412
+ try {
40413
+ sourceBuffer = NFS.readFileSync(absolutePath);
40414
+ } catch {
40415
+ sourceBuffer = null;
40416
+ }
40417
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40418
+ return sourceBuffer;
40419
+ };
40420
+ const readSourceLines = (filename) => {
40421
+ const absolutePath = resolveAbsolutePath(filename);
40422
+ const cached = sourceLinesCache.get(absolutePath);
40423
+ if (cached !== void 0) return cached;
40424
+ const sourceBuffer = readSourceBuffer(filename);
40425
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40426
+ sourceLinesCache.set(absolutePath, sourceLines);
40427
+ return sourceLines;
40428
+ };
40381
40429
  const isMinifiedDiagnosticFile = (filename) => {
40382
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40430
+ const absolutePath = resolveAbsolutePath(filename);
40383
40431
  const cached = minifiedFileCache.get(absolutePath);
40384
40432
  if (cached !== void 0) return cached;
40385
40433
  const minified = isMinifiedSource(absolutePath);
@@ -40392,8 +40440,17 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40392
40440
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40393
40441
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40394
40442
  const primarySpan = primaryLabel?.span;
40443
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40395
40444
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40396
40445
  const category = resolveDiagnosticCategory(plugin, rule);
40446
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40447
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40448
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40449
+ const primaryLine = sourceLines?.[primaryLineIndex];
40450
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40451
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40452
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40453
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40397
40454
  return {
40398
40455
  filePath: normalizedFilePath,
40399
40456
  plugin,
@@ -40407,10 +40464,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40407
40464
  column: primarySpan?.column ?? 0,
40408
40465
  ...primarySpan ? {
40409
40466
  offset: primarySpan.offset,
40410
- length: primarySpan.length
40467
+ length: primarySpan.length,
40468
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40411
40469
  } : {},
40412
40470
  category,
40413
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40471
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40414
40472
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40415
40473
  };
40416
40474
  }), rootDirectory);
@@ -41104,7 +41162,7 @@ const runOxlint = async (options) => {
41104
41162
  };
41105
41163
  }
41106
41164
  };
41107
- if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41165
+ if (perFileLintCacheEnabled && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
41108
41166
  const rulesetHash = computeRulesetHash({
41109
41167
  config: buildConfig({
41110
41168
  extendsPaths: [],
@@ -41112,7 +41170,8 @@ const runOxlint = async (options) => {
41112
41170
  }),
41113
41171
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41114
41172
  ignorePatterns: combinedPatterns,
41115
- tsconfigContent
41173
+ tsconfigContent,
41174
+ respectInlineDisables
41116
41175
  });
41117
41176
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
41118
41177
  const cacheKeyByFile = /* @__PURE__ */ new Map();
@@ -41155,7 +41214,8 @@ const runOxlint = async (options) => {
41155
41214
  }),
41156
41215
  toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
41157
41216
  ignorePatterns: combinedPatterns,
41158
- tsconfigContent
41217
+ tsconfigContent,
41218
+ respectInlineDisables
41159
41219
  })) : null;
41160
41220
  const probeAnswers = createSidecarProbeAnswerResolver({
41161
41221
  rootDirectory,
@@ -42195,7 +42255,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42195
42255
  directory: scanDirectory,
42196
42256
  repo
42197
42257
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42198
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42258
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42199
42259
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42200
42260
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42201
42261
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -42690,7 +42750,7 @@ const runEditorScan = async (input) => {
42690
42750
  warnings,
42691
42751
  isCi: false,
42692
42752
  resolveLocalGithubViewerPermission: false,
42693
- skipJsxIncludeFilter: true
42753
+ skipExplicitIncludePathFilter: true
42694
42754
  }).pipe(withSpan("runEditorScan", { attributes: {
42695
42755
  "editor.lint": lint,
42696
42756
  "editor.runDeadCode": runDeadCode
@@ -42774,7 +42834,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42774
42834
  /** Display name used in client-facing messages and progress titles. */
42775
42835
  const SERVER_DISPLAY_NAME = "React Doctor";
42776
42836
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42777
- const SERVER_VERSION = "0.7.5";
42837
+ const SERVER_VERSION = "0.7.6";
42778
42838
  /** `Diagnostic.source` shown next to every published diagnostic. */
42779
42839
  const DIAGNOSTIC_SOURCE = "react-doctor";
42780
42840
  /**
@@ -43453,7 +43513,7 @@ const createLintCache = (input) => {
43453
43513
  let persistTimer = null;
43454
43514
  try {
43455
43515
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
43456
- if (parsed.version === 1 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43516
+ if (parsed.version === 2 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43457
43517
  } catch {}
43458
43518
  const persist = () => {
43459
43519
  if (persistTimer) {
@@ -43464,7 +43524,7 @@ const createLintCache = (input) => {
43464
43524
  dirty = false;
43465
43525
  try {
43466
43526
  const payload = {
43467
- version: 1,
43527
+ version: 2,
43468
43528
  fingerprint: input.fingerprint,
43469
43529
  entries: Object.fromEntries(entries)
43470
43530
  };
@@ -43477,14 +43537,14 @@ const createLintCache = (input) => {
43477
43537
  }
43478
43538
  };
43479
43539
  return {
43480
- lookup: (fsPath, stat) => {
43540
+ lookup: (fsPath, identity) => {
43481
43541
  const entry = entries.get(fsPath);
43482
- if (entry !== void 0 && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.diagnostics;
43542
+ if (entry !== void 0 && entry.contentHash === identity.contentHash) return entry.diagnostics;
43483
43543
  return null;
43484
43544
  },
43485
- store: (fsPath, stat, diagnostics) => {
43545
+ store: (fsPath, identity, diagnostics) => {
43486
43546
  entries.set(fsPath, {
43487
- ...stat,
43547
+ ...identity,
43488
43548
  diagnostics
43489
43549
  });
43490
43550
  dirty = true;
@@ -43573,16 +43633,9 @@ const resolveDiagnosticFsPath = (rawFilePath, scanDirectory, projectDirectory, o
43573
43633
  }
43574
43634
  return normalizeFsPath(absolute);
43575
43635
  };
43576
- const statSafe = (fsPath) => {
43577
- try {
43578
- const stat = fs.statSync(fsPath);
43579
- return {
43580
- mtimeMs: stat.mtimeMs,
43581
- size: stat.size
43582
- };
43583
- } catch {
43584
- return null;
43585
- }
43636
+ const readFileIdentity = (fsPath) => {
43637
+ const contentHash = hashFileContents(fsPath);
43638
+ return contentHash === null ? null : { contentHash };
43586
43639
  };
43587
43640
  /**
43588
43641
  * Outcome for a per-file request served without running oxlint (every
@@ -43608,7 +43661,7 @@ const outcomeWithoutScan = (request, byFile, requestedPaths) => ({
43608
43661
  * overlay tree (unsaved buffers) or disk, groups diagnostics by canonical
43609
43662
  * absolute path, and reports stale-detection metadata.
43610
43663
  *
43611
- * A persistent per-file lint cache (keyed by mtime + size, namespaced by a
43664
+ * A persistent per-file lint cache (keyed by content, namespaced by a
43612
43665
  * config fingerprint) short-circuits unchanged files so a re-opened editor
43613
43666
  * or repeated workspace scan skips the oxlint subprocess for everything it
43614
43667
  * hasn't edited. The cache applies only to disk-based, lint-only, per-file
@@ -43638,15 +43691,15 @@ const createScanRunner = (options) => {
43638
43691
  const requestedPaths = request.priority === "background" && !request.useOverlay && options.isOpen ? allRequested.filter((fsPath) => !options.isOpen?.(fsPath)) : allRequested;
43639
43692
  const cache = cacheEnabled && !isWholeProject && !request.useOverlay && !request.runDeadCode ? getCache(projectDirectory) : null;
43640
43693
  const byFile = /* @__PURE__ */ new Map();
43641
- const statByPath = /* @__PURE__ */ new Map();
43694
+ const identityByPath = /* @__PURE__ */ new Map();
43642
43695
  let filesToScan = requestedPaths;
43643
43696
  if (cache) {
43644
43697
  const uncached = [];
43645
43698
  for (const fsPath of requestedPaths) {
43646
- const stat = statSafe(fsPath);
43647
- if (stat) {
43648
- statByPath.set(fsPath, stat);
43649
- const hit = cache.lookup(fsPath, stat);
43699
+ const identity = readFileIdentity(fsPath);
43700
+ if (identity) {
43701
+ identityByPath.set(fsPath, identity);
43702
+ const hit = cache.lookup(fsPath, identity);
43650
43703
  if (hit !== null) {
43651
43704
  if (hit.length > 0) byFile.set(fsPath, hit);
43652
43705
  continue;
@@ -43689,9 +43742,9 @@ const createScanRunner = (options) => {
43689
43742
  }
43690
43743
  if (cache && result.ok && !result.skipped && !result.didLintFail && result.lintPartialFailures.length === 0) {
43691
43744
  for (const fsPath of filesToScan) {
43692
- const stat = statByPath.get(fsPath);
43693
- if (!stat) continue;
43694
- cache.store(fsPath, stat, byFile.get(fsPath) ?? []);
43745
+ const identity = identityByPath.get(fsPath);
43746
+ if (!identity) continue;
43747
+ cache.store(fsPath, identity, byFile.get(fsPath) ?? []);
43695
43748
  }
43696
43749
  cache.schedulePersist();
43697
43750
  }
@@ -44825,5 +44878,5 @@ const startLanguageServer = () => {
44825
44878
  };
44826
44879
  //#endregion
44827
44880
  export { startLanguageServer };
44828
- !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]="7af68c1a-91df-56eb-982a-161195d345fc")}catch(e){}}();
44829
- //# debugId=7af68c1a-91df-56eb-982a-161195d345fc
44881
+ !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]="f9337714-b38f-5f28-ac00-c9d7d79ebaea")}catch(e){}}();
44882
+ //# debugId=f9337714-b38f-5f28-ac00-c9d7d79ebaea
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.5-dev.dbd4067",
3
+ "version": "0.7.6-dev.0150fc9",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "oxlint-plugin-react-doctor": "0.7.5-dev.dbd4067",
69
- "deslop-js": "0.7.5"
68
+ "deslop-js": "0.7.6",
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.0150fc9"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -74,9 +74,9 @@
74
74
  "@xterm/headless": "^6.0.0",
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
- "@react-doctor/api": "0.7.5",
78
- "@react-doctor/language-server": "0.7.5",
79
- "@react-doctor/core": "0.7.5"
77
+ "@react-doctor/api": "0.7.6",
78
+ "@react-doctor/language-server": "0.7.6",
79
+ "@react-doctor/core": "0.7.6"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"