react-doctor 0.5.6-dev.81bbfcc → 0.5.6-dev.869f220

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/lsp.js CHANGED
@@ -14,7 +14,7 @@ import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { createJiti } from "jiti";
16
16
  import * as Crypto from "node:crypto";
17
- import crypto from "node:crypto";
17
+ import crypto, { createHash } from "node:crypto";
18
18
  import { gzipSync } from "node:zlib";
19
19
  import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
20
20
  import { TextDocument } from "vscode-languageserver-textdocument";
@@ -19286,7 +19286,8 @@ var Diagnostic = class extends Class("Diagnostic")({
19286
19286
  category: String$1,
19287
19287
  fileContext: optional(Literals(["test", "story"])),
19288
19288
  suppressionHint: optional(String$1),
19289
- relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
19289
+ relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
19290
+ fixGroupId: optional(String$1)
19290
19291
  }) {};
19291
19292
  /**
19292
19293
  * Deterministic identity string for a diagnostic. Same diagnostic
@@ -19335,6 +19336,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19335
19336
  score: Unknown,
19336
19337
  skippedChecks: ArraySchema(String$1),
19337
19338
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19339
+ scannedFileCount: optional(Number$1),
19338
19340
  elapsedMilliseconds: Number$1
19339
19341
  }) {};
19340
19342
  /**
@@ -32761,6 +32763,7 @@ const isLargeMinifiedFile = (absolutePath) => {
32761
32763
  if (sizeBytes < 2e4) return false;
32762
32764
  return isMinifiedSource(absolutePath);
32763
32765
  };
32766
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32764
32767
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
32765
32768
  "EACCES",
32766
32769
  "EPERM",
@@ -32770,11 +32773,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32770
32773
  "ELOOP",
32771
32774
  "ENAMETOOLONG"
32772
32775
  ]);
32773
- const isIgnorableReaddirError = (error) => {
32774
- if (typeof error !== "object" || error === null) return false;
32775
- const errorCode = error.code;
32776
- return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
32777
- };
32776
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32778
32777
  const readDirectoryEntries = (directoryPath) => {
32779
32778
  try {
32780
32779
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -32824,7 +32823,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
32824
32823
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
32825
32824
  } catch (error) {
32826
32825
  if (error instanceof SyntaxError) return {};
32827
- if (error instanceof Error && "code" in error) {
32826
+ if (isErrnoException(error)) {
32828
32827
  const { code } = error;
32829
32828
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
32830
32829
  }
@@ -33549,17 +33548,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33549
33548
  return false;
33550
33549
  };
33551
33550
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33552
- const getExpoDependencySpec = (packageJson) => {
33553
- const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
33551
+ const getDependencySpec = (packageJson, packageName) => {
33552
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33554
33553
  return typeof spec === "string" ? spec : null;
33555
33554
  };
33556
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
33555
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33557
33556
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33558
- const getShopifyFlashListDependencySpec = (packageJson) => {
33559
- const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
33560
- return typeof spec === "string" ? spec : null;
33561
- };
33562
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
33557
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33563
33558
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33564
33559
  if (version === null || !isCatalogReference(version)) return version;
33565
33560
  const catalogName = extractCatalogName(version);
@@ -33571,11 +33566,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
33571
33566
  if (!isFile(monorepoPackageJsonPath)) return version;
33572
33567
  return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33573
33568
  };
33574
- const getNextjsDependencySpec = (packageJson) => {
33575
- const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
33576
- return typeof spec === "string" ? spec : null;
33577
- };
33578
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
33569
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33579
33570
  const getPreactVersion = (packageJson) => {
33580
33571
  return {
33581
33572
  ...packageJson.peerDependencies,
@@ -33657,6 +33648,11 @@ const ES_TARGET_YEAR_BY_NAME = {
33657
33648
  esnext: 9999
33658
33649
  };
33659
33650
  /**
33651
+ * tsconfig filenames probed when resolving a project's TypeScript
33652
+ * compiler options — the root config first, then a monorepo base config.
33653
+ */
33654
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33655
+ /**
33660
33656
  * Project-config files that `StagedFiles.materialize` copies into
33661
33657
  * the temp directory alongside staged sources so oxlint resolves
33662
33658
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -33726,6 +33722,13 @@ const APP_ONLY_RULE_KEYS = new Set([
33726
33722
  ]);
33727
33723
  const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33728
33724
  const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33725
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33726
+ "react-doctor/no-derived-state",
33727
+ "react-doctor/no-derived-state-effect",
33728
+ "react-doctor/no-derived-useState",
33729
+ "react-doctor/no-adjust-state-on-prop-change",
33730
+ "react-doctor/no-reset-all-state-on-prop-change"
33731
+ ]);
33729
33732
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33730
33733
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33731
33734
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
@@ -34178,6 +34181,7 @@ const isTailwindAtLeast = (detected, required) => {
34178
34181
  if (detected.major !== required.major) return detected.major > required.major;
34179
34182
  return detected.minor >= required.minor;
34180
34183
  };
34184
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34181
34185
  var InvalidGlobPatternError = class extends Error {
34182
34186
  pattern;
34183
34187
  reason;
@@ -34206,7 +34210,7 @@ const compileGlobPattern = (rawPattern) => {
34206
34210
  try {
34207
34211
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
34208
34212
  } catch (caughtError) {
34209
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
34213
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
34210
34214
  }
34211
34215
  };
34212
34216
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -34302,115 +34306,6 @@ const buildRuleSeverityControls = (config) => {
34302
34306
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34303
34307
  };
34304
34308
  };
34305
- const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34306
- const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34307
- const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34308
- let stringDelimiter = null;
34309
- for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34310
- const character = line[charIndex];
34311
- if (stringDelimiter !== null) {
34312
- if (character === "\\") {
34313
- charIndex++;
34314
- continue;
34315
- }
34316
- if (character === stringDelimiter) stringDelimiter = null;
34317
- continue;
34318
- }
34319
- if (character === "\"" || character === "'" || character === "`") {
34320
- stringDelimiter = character;
34321
- continue;
34322
- }
34323
- if (character === "/" && line[charIndex + 1] === "/") return true;
34324
- }
34325
- return false;
34326
- };
34327
- const findOpenerTagOnLine = (line) => {
34328
- for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34329
- if (match.index === void 0) continue;
34330
- if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34331
- }
34332
- return null;
34333
- };
34334
- const findJsxOpenerSpan = (lines, openerLineIndex) => {
34335
- const openerLine = lines[openerLineIndex];
34336
- if (openerLine === void 0) return null;
34337
- const opener = findOpenerTagOnLine(openerLine);
34338
- if (!opener) return null;
34339
- const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34340
- let braceDepth = 0;
34341
- let innerAngleDepth = 0;
34342
- let stringDelimiter = null;
34343
- for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34344
- const currentLine = lines[lineIndex];
34345
- const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34346
- for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34347
- const character = currentLine[charIndex];
34348
- if (stringDelimiter !== null) {
34349
- if (character === "\\") {
34350
- charIndex++;
34351
- continue;
34352
- }
34353
- if (character === stringDelimiter) stringDelimiter = null;
34354
- continue;
34355
- }
34356
- if (character === "\"" || character === "'" || character === "`") {
34357
- stringDelimiter = character;
34358
- continue;
34359
- }
34360
- if (character === "{") {
34361
- braceDepth++;
34362
- continue;
34363
- }
34364
- if (character === "}") {
34365
- braceDepth--;
34366
- continue;
34367
- }
34368
- if (braceDepth !== 0) continue;
34369
- if (character === "<") {
34370
- const followCharacter = currentLine[charIndex + 1];
34371
- if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34372
- continue;
34373
- }
34374
- if (character !== ">") continue;
34375
- const previousCharacter = currentLine[charIndex - 1];
34376
- const nextCharacter = currentLine[charIndex + 1];
34377
- if (previousCharacter === "=" || nextCharacter === "=") continue;
34378
- if (innerAngleDepth > 0) {
34379
- innerAngleDepth--;
34380
- continue;
34381
- }
34382
- return lineIndex;
34383
- }
34384
- }
34385
- return null;
34386
- };
34387
- const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34388
- for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34389
- const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34390
- if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34391
- }
34392
- return null;
34393
- };
34394
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34395
- const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34396
- const collected = [];
34397
- let isStillInChain = true;
34398
- for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34399
- const candidateLine = lines[candidateIndex];
34400
- if (candidateLine === void 0) break;
34401
- const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34402
- if (match) {
34403
- collected.push({
34404
- commentLineIndex: candidateIndex,
34405
- ruleList: match[1],
34406
- isInChain: isStillInChain
34407
- });
34408
- continue;
34409
- }
34410
- isStillInChain = false;
34411
- }
34412
- return collected;
34413
- };
34414
34309
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34415
34310
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34416
34311
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34535,7 +34430,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34535
34430
  }
34536
34431
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34537
34432
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34538
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34433
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34434
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34435
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34436
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34437
+ if (canonicalCandidate === canonicalTarget) return true;
34438
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34439
+ };
34539
34440
  const getEquivalentRuleKeys = (ruleKey) => {
34540
34441
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34541
34442
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34545,12 +34446,182 @@ const stripDescriptionTail = (ruleList) => {
34545
34446
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34546
34447
  return ruleList.slice(0, descriptionMatch.index);
34547
34448
  };
34548
- const isRuleListedInComment = (ruleList, ruleId) => {
34449
+ const tokenizeRuleList = (ruleList) => {
34549
34450
  const trimmed = ruleList?.trim();
34550
- if (!trimmed) return true;
34451
+ if (!trimmed) return [];
34551
34452
  const ruleSection = stripDescriptionTail(trimmed).trim();
34552
- if (!ruleSection) return true;
34553
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34453
+ if (!ruleSection) return [];
34454
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34455
+ };
34456
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34457
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34458
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34459
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34460
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34461
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34462
+ const candidates = [{
34463
+ line: lines[diagnosticLineIndex],
34464
+ requiredScope: "line"
34465
+ }, {
34466
+ line: lines[diagnosticLineIndex - 1],
34467
+ requiredScope: "next-line"
34468
+ }];
34469
+ for (const { line, requiredScope } of candidates) {
34470
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34471
+ if (!match) continue;
34472
+ const [, tool, scope, ruleList] = match;
34473
+ if (scope !== requiredScope) continue;
34474
+ const tokens = tokenizeRuleList(ruleList);
34475
+ if (tokens.includes(ruleId)) continue;
34476
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34477
+ }
34478
+ return null;
34479
+ };
34480
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34481
+ let openMisname = null;
34482
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34483
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34484
+ const line = lines[lineIndex];
34485
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34486
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34487
+ if (disableMatch) {
34488
+ const [, tool, ruleList] = disableMatch;
34489
+ const tokens = tokenizeRuleList(ruleList);
34490
+ if (tokens.includes(ruleId)) openMisname = null;
34491
+ else {
34492
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34493
+ if (misnamed) openMisname = {
34494
+ tool,
34495
+ token: misnamed
34496
+ };
34497
+ }
34498
+ continue;
34499
+ }
34500
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34501
+ if (enableMatch) {
34502
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34503
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34504
+ }
34505
+ }
34506
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34507
+ };
34508
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34509
+ if (!ruleId.startsWith("react-doctor/")) return null;
34510
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34511
+ };
34512
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34513
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34514
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34515
+ let stringDelimiter = null;
34516
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34517
+ const character = line[charIndex];
34518
+ if (stringDelimiter !== null) {
34519
+ if (character === "\\") {
34520
+ charIndex++;
34521
+ continue;
34522
+ }
34523
+ if (character === stringDelimiter) stringDelimiter = null;
34524
+ continue;
34525
+ }
34526
+ if (character === "\"" || character === "'" || character === "`") {
34527
+ stringDelimiter = character;
34528
+ continue;
34529
+ }
34530
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34531
+ }
34532
+ return false;
34533
+ };
34534
+ const findOpenerTagOnLine = (line) => {
34535
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34536
+ if (match.index === void 0) continue;
34537
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34538
+ }
34539
+ return null;
34540
+ };
34541
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34542
+ const openerLine = lines[openerLineIndex];
34543
+ if (openerLine === void 0) return null;
34544
+ const opener = findOpenerTagOnLine(openerLine);
34545
+ if (!opener) return null;
34546
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34547
+ let braceDepth = 0;
34548
+ let innerAngleDepth = 0;
34549
+ let stringDelimiter = null;
34550
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34551
+ const currentLine = lines[lineIndex];
34552
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34553
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34554
+ const character = currentLine[charIndex];
34555
+ if (stringDelimiter !== null) {
34556
+ if (character === "\\") {
34557
+ charIndex++;
34558
+ continue;
34559
+ }
34560
+ if (character === stringDelimiter) stringDelimiter = null;
34561
+ continue;
34562
+ }
34563
+ if (character === "\"" || character === "'" || character === "`") {
34564
+ stringDelimiter = character;
34565
+ continue;
34566
+ }
34567
+ if (character === "{") {
34568
+ braceDepth++;
34569
+ continue;
34570
+ }
34571
+ if (character === "}") {
34572
+ braceDepth--;
34573
+ continue;
34574
+ }
34575
+ if (braceDepth !== 0) continue;
34576
+ if (character === "<") {
34577
+ const followCharacter = currentLine[charIndex + 1];
34578
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34579
+ continue;
34580
+ }
34581
+ if (character !== ">") continue;
34582
+ const previousCharacter = currentLine[charIndex - 1];
34583
+ const nextCharacter = currentLine[charIndex + 1];
34584
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34585
+ if (innerAngleDepth > 0) {
34586
+ innerAngleDepth--;
34587
+ continue;
34588
+ }
34589
+ return lineIndex;
34590
+ }
34591
+ }
34592
+ return null;
34593
+ };
34594
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34595
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34596
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34597
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34598
+ }
34599
+ return null;
34600
+ };
34601
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34602
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34603
+ const collected = [];
34604
+ let isStillInChain = true;
34605
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34606
+ const candidateLine = lines[candidateIndex];
34607
+ if (candidateLine === void 0) break;
34608
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34609
+ if (match) {
34610
+ collected.push({
34611
+ commentLineIndex: candidateIndex,
34612
+ ruleList: match[1],
34613
+ isInChain: isStillInChain
34614
+ });
34615
+ continue;
34616
+ }
34617
+ isStillInChain = false;
34618
+ }
34619
+ return collected;
34620
+ };
34621
+ const isRuleListedInComment = (ruleList, ruleId) => {
34622
+ const tokens = tokenizeRuleList(ruleList);
34623
+ if (tokens.length === 0) return true;
34624
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34554
34625
  };
34555
34626
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34556
34627
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34594,7 +34665,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34594
34665
  };
34595
34666
  return {
34596
34667
  isSuppressed: false,
34597
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34668
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34598
34669
  };
34599
34670
  };
34600
34671
  /**
@@ -35362,7 +35433,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
35362
35433
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
35363
35434
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
35364
35435
  const jiti = createJiti(import.meta.url);
35365
- const formatError = (error) => error instanceof Error ? error.message : String(error);
35366
35436
  const importDefaultExport = async (jitiInstance, filePath) => {
35367
35437
  const imported = await jitiInstance.import(filePath);
35368
35438
  return imported?.default ?? imported;
@@ -35394,7 +35464,7 @@ const loadModuleConfig = async (filePath) => {
35394
35464
  try {
35395
35465
  return await importDefaultExport(aliasJiti, filePath);
35396
35466
  } catch (retryError) {
35397
- throw new Error(`${formatError(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${formatError(retryError)})`, { cause: retryError });
35467
+ throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
35398
35468
  }
35399
35469
  }
35400
35470
  };
@@ -35443,7 +35513,7 @@ const loadLegacyConfig = (directory) => {
35443
35513
  }
35444
35514
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
35445
35515
  } catch (error) {
35446
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
35516
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
35447
35517
  }
35448
35518
  return {
35449
35519
  status: "invalid",
@@ -35470,7 +35540,7 @@ const loadConfigFromDirectory = async (directory) => {
35470
35540
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
35471
35541
  sawBrokenConfigFile = true;
35472
35542
  } catch (error) {
35473
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
35543
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
35474
35544
  sawBrokenConfigFile = true;
35475
35545
  }
35476
35546
  }
@@ -35524,6 +35594,29 @@ const resolveConfigRootDir = (config, configSourceDirectory) => {
35524
35594
  }
35525
35595
  return resolvedRootDir;
35526
35596
  };
35597
+ const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
35598
+ diagnostic.filePath,
35599
+ `${diagnostic.plugin}/${diagnostic.rule}`,
35600
+ diagnostic.message
35601
+ ])).digest("hex").slice(0, 16);
35602
+ const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
35603
+ const assignFixGroups = (diagnostics) => {
35604
+ const siteCountByGroupId = /* @__PURE__ */ new Map();
35605
+ for (const diagnostic of diagnostics) {
35606
+ if (!isGroupableRule(diagnostic)) continue;
35607
+ const groupId = buildFixGroupId(diagnostic);
35608
+ siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
35609
+ }
35610
+ return diagnostics.map((diagnostic) => {
35611
+ if (!isGroupableRule(diagnostic)) return diagnostic;
35612
+ const groupId = buildFixGroupId(diagnostic);
35613
+ if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
35614
+ return {
35615
+ ...diagnostic,
35616
+ fixGroupId: groupId
35617
+ };
35618
+ });
35619
+ };
35527
35620
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35528
35621
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35529
35622
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36458,15 +36551,10 @@ const buildCapabilities = (project) => {
36458
36551
  }
36459
36552
  if (project.tailwindVersion !== null) {
36460
36553
  capabilities.add("tailwind");
36461
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
36462
- if (isTailwindAtLeast(tailwind, {
36554
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
36463
36555
  major: 3,
36464
36556
  minor: 4
36465
36557
  })) capabilities.add("tailwind:3.4");
36466
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
36467
- major: 4,
36468
- minor: 0
36469
- })) capabilities.add("tailwind:4");
36470
36558
  }
36471
36559
  if (project.zodVersion !== null) {
36472
36560
  capabilities.add("zod");
@@ -36655,7 +36743,7 @@ const readIgnoreFile = (filePath) => {
36655
36743
  try {
36656
36744
  content = NFS.readFileSync(filePath, "utf-8");
36657
36745
  } catch (error) {
36658
- const errnoCode = error?.code;
36746
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
36659
36747
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
36660
36748
  return [];
36661
36749
  }
@@ -36696,8 +36784,8 @@ const collectIgnorePatterns = (rootDirectory) => {
36696
36784
  cachedPatternsByRoot.set(rootDirectory, patterns);
36697
36785
  return patterns;
36698
36786
  };
36787
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36699
36788
  const KNIP_JSON_FILENAME = "knip.json";
36700
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36701
36789
  const readJsonFileSafe = (filePath) => {
36702
36790
  let rawContents;
36703
36791
  try {
@@ -36713,10 +36801,10 @@ const readJsonFileSafe = (filePath) => {
36713
36801
  };
36714
36802
  const readKnipConfig = (rootDirectory) => {
36715
36803
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
36716
- if (isRecord$1(knipJson)) return knipJson;
36804
+ if (isRecord(knipJson)) return knipJson;
36717
36805
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
36718
- const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
36719
- return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
36806
+ const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
36807
+ return isRecord(packageKnipConfig) ? packageKnipConfig : null;
36720
36808
  };
36721
36809
  const normalizePatternList = (value) => {
36722
36810
  if (typeof value === "string" && value.length > 0) return [value];
@@ -36728,10 +36816,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
36728
36816
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
36729
36817
  };
36730
36818
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
36731
- if (!isRecord$1(workspaces)) return [];
36819
+ if (!isRecord(workspaces)) return [];
36732
36820
  const patterns = [];
36733
36821
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
36734
- if (!isRecord$1(workspaceConfig)) continue;
36822
+ if (!isRecord(workspaceConfig)) continue;
36735
36823
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
36736
36824
  }
36737
36825
  return patterns;
@@ -36776,8 +36864,6 @@ const toCanonicalPath = (filePath) => {
36776
36864
  };
36777
36865
  const DEAD_CODE_PLUGIN = "deslop";
36778
36866
  const DEAD_CODE_CATEGORY = "Maintainability";
36779
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
36780
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36781
36867
  const DEAD_CODE_WORKER_SCRIPT = `
36782
36868
  const inputChunks = [];
36783
36869
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -36835,7 +36921,7 @@ process.stdin.on("end", () => {
36835
36921
  });
36836
36922
  `;
36837
36923
  const resolveTsConfigPath = (rootDirectory) => {
36838
- for (const filename of TSCONFIG_FILENAMES$1) {
36924
+ for (const filename of TSCONFIG_FILENAMES) {
36839
36925
  const candidate = Path.join(rootDirectory, filename);
36840
36926
  if (NFS.existsSync(candidate)) return candidate;
36841
36927
  }
@@ -37216,15 +37302,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37216
37302
  })()) }));
37217
37303
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
37218
37304
  };
37219
- const createNodeReadFileLinesSync = (rootDirectory) => {
37220
- return (filePath) => {
37221
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37222
- try {
37223
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37224
- } catch {
37225
- return null;
37226
- }
37227
- };
37305
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
37306
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37307
+ try {
37308
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37309
+ } catch {
37310
+ return null;
37311
+ }
37228
37312
  };
37229
37313
  var Files = class Files extends Service()("react-doctor/Files") {
37230
37314
  static layerNode = succeed$3(Files, Files.of({
@@ -37435,7 +37519,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
37435
37519
  directory: input.directory,
37436
37520
  cause
37437
37521
  }) });
37438
- }));
37522
+ }), withSpan("git.exec", { attributes: {
37523
+ "git.command": input.command,
37524
+ "git.subcommand": input.args[0] ?? ""
37525
+ } }));
37439
37526
  const runGit = (directory, args) => runCommand({
37440
37527
  command: "git",
37441
37528
  args,
@@ -37463,7 +37550,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37463
37550
  ]);
37464
37551
  if (candidates.status !== 0) return null;
37465
37552
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
37466
- });
37553
+ }).pipe(withSpan("Git.defaultBranch"));
37467
37554
  const branchExists = (directory, branch) => runGit(directory, [
37468
37555
  "rev-parse",
37469
37556
  "--verify",
@@ -37510,7 +37597,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37510
37597
  const result = resultOption.value;
37511
37598
  if (result.status !== 0) return null;
37512
37599
  return parseGithubViewerPermission(result.stdout);
37513
- }).pipe(catch_$1(() => succeed$2(null)));
37600
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
37514
37601
  /**
37515
37602
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
37516
37603
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -37624,7 +37711,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37624
37711
  changedFiles: splitNullSeparated(diff.stdout),
37625
37712
  isCurrentChanges: false
37626
37713
  };
37627
- }),
37714
+ }).pipe(withSpan("Git.diffSelection")),
37628
37715
  stagedFilePaths: (directory) => runGit(directory, [
37629
37716
  "diff",
37630
37717
  "--cached",
@@ -37666,7 +37753,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37666
37753
  status: result.status,
37667
37754
  stdout: result.stdout
37668
37755
  };
37669
- }),
37756
+ }).pipe(withSpan("Git.grep")),
37670
37757
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
37671
37758
  if (files.length === 0) return [];
37672
37759
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -37682,7 +37769,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37682
37769
  ]);
37683
37770
  if (result.status !== 0) return null;
37684
37771
  return parseChangedLineRanges(result.stdout);
37685
- })
37772
+ }).pipe(withSpan("Git.changedLineRanges"))
37686
37773
  });
37687
37774
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37688
37775
  /**
@@ -37897,7 +37984,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37897
37984
  for (const [absolutePath, originalContent] of originalContents) try {
37898
37985
  NFS.writeFileSync(absolutePath, originalContent);
37899
37986
  } catch (error) {
37900
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37987
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37901
37988
  }
37902
37989
  };
37903
37990
  const onExit = () => restore();
@@ -38003,7 +38090,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
38003
38090
  try {
38004
38091
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
38005
38092
  } catch (error) {
38006
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
38093
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
38007
38094
  return null;
38008
38095
  }
38009
38096
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -38075,8 +38162,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38075
38162
  }
38076
38163
  return enabled;
38077
38164
  };
38078
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38079
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38165
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38166
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38080
38167
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38081
38168
  const jsPlugins = [];
38082
38169
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38136,7 +38223,6 @@ const resolveOxlintBinary = () => {
38136
38223
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
38137
38224
  };
38138
38225
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
38139
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
38140
38226
  const resolveTsConfigRelativePath = (rootDirectory) => {
38141
38227
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
38142
38228
  return null;
@@ -38508,7 +38594,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
38508
38594
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
38509
38595
  let currentNode = identifier.parent;
38510
38596
  while (currentNode) {
38511
- if (isScopeNode(currentNode)) {
38597
+ if (isScopeBoundary(currentNode)) {
38512
38598
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
38513
38599
  }
38514
38600
  if (currentNode === sourceFile) return false;
@@ -38599,11 +38685,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
38599
38685
  });
38600
38686
  return resolution;
38601
38687
  };
38602
- const isScopeNode = isScopeBoundary;
38603
38688
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
38604
38689
  let currentNode = identifier.parent;
38605
38690
  while (currentNode) {
38606
- if (isScopeNode(currentNode)) {
38691
+ if (isScopeBoundary(currentNode)) {
38607
38692
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
38608
38693
  if (resolution) return resolution;
38609
38694
  }
@@ -38773,9 +38858,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38773
38858
  try {
38774
38859
  parsed = JSON.parse(sanitizedStdout);
38775
38860
  } catch {
38776
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38861
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38777
38862
  }
38778
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38863
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38779
38864
  const minifiedFileCache = /* @__PURE__ */ new Map();
38780
38865
  const isMinifiedDiagnosticFile = (filename) => {
38781
38866
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -38851,7 +38936,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38851
38936
  child.kill("SIGKILL");
38852
38937
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38853
38938
  kind: "timeout",
38854
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
38939
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
38855
38940
  }) }));
38856
38941
  }, spawnTimeoutMs);
38857
38942
  timeoutHandle.unref?.();
@@ -39066,6 +39151,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39066
39151
  NFS.closeSync(fileHandle);
39067
39152
  }
39068
39153
  };
39154
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39155
+ /**
39156
+ * Detects an oxlint config-load crash caused by the optional
39157
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39158
+ * builds the partial-failure note for it; returns `null` when the failure
39159
+ * was anything else.
39160
+ *
39161
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39162
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39163
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39164
+ * config load on it, leaving the plugin in would drop every curated
39165
+ * react-doctor diagnostic too — so the caller retries with the plugin
39166
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39167
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39168
+ */
39169
+ const reactHooksJsPluginDropNote = (error) => {
39170
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39171
+ const { preview } = error.reason;
39172
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39173
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39174
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39175
+ };
39069
39176
  /**
39070
39177
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39071
39178
  *
@@ -39093,15 +39200,16 @@ const runOxlint = async (options) => {
39093
39200
  const pluginPath = resolvePluginPath();
39094
39201
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39095
39202
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39096
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39203
+ const buildConfig = (overrides) => createOxlintConfig({
39097
39204
  pluginPath,
39098
39205
  project,
39099
39206
  customRulesOnly,
39100
- extendsPaths: extendsForThisAttempt,
39207
+ extendsPaths: overrides.extendsPaths,
39101
39208
  ignoredTags,
39102
39209
  serverAuthFunctionNames,
39103
39210
  severityControls,
39104
- userPlugins
39211
+ userPlugins,
39212
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39105
39213
  });
39106
39214
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39107
39215
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39137,12 +39245,22 @@ const runOxlint = async (options) => {
39137
39245
  outputMaxBytes,
39138
39246
  concurrency: options.concurrency
39139
39247
  });
39140
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39248
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39141
39249
  try {
39142
39250
  return await runBatches();
39143
39251
  } catch (error) {
39252
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39253
+ if (reactHooksJsDropNote !== null) {
39254
+ writeOxlintConfig(configPath, buildConfig({
39255
+ extendsPaths,
39256
+ disableReactHooksJsPlugin: true
39257
+ }));
39258
+ const diagnostics = await runBatches();
39259
+ onPartialFailure?.(reactHooksJsDropNote);
39260
+ return diagnostics;
39261
+ }
39144
39262
  if (extendsPaths.length === 0) throw error;
39145
- writeOxlintConfig(configPath, buildConfig([]));
39263
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39146
39264
  return await runBatches();
39147
39265
  }
39148
39266
  } finally {
@@ -39940,17 +40058,17 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39940
40058
  }))))))));
39941
40059
  const deadCodeFailureState = yield* get$2(deadCodeFailure);
39942
40060
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
39943
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
40061
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
39944
40062
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
39945
40063
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39946
40064
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
39947
40065
  yield* reporterService.finalize;
39948
- const finalDiagnostics = [
40066
+ const finalDiagnostics = assignFixGroups([
39949
40067
  ...envCollected,
39950
40068
  ...supplyChainCollected,
39951
40069
  ...lintCollected,
39952
40070
  ...deadCodeCollected
39953
- ];
40071
+ ]);
39954
40072
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
39955
40073
  const scoreMetadata = {
39956
40074
  ...repo !== null ? { repo } : {},
@@ -40154,7 +40272,7 @@ const materializeSourceTree = (input) => gen(function* () {
40154
40272
  static layerNode = effect(StagedFiles, gen(function* () {
40155
40273
  const git = yield* Git;
40156
40274
  return StagedFiles.of({
40157
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
40275
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
40158
40276
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
40159
40277
  directory,
40160
40278
  files: stagedFiles,
@@ -40164,7 +40282,7 @@ const materializeSourceTree = (input) => gen(function* () {
40164
40282
  tempDirectory: tree.tempDirectory,
40165
40283
  stagedFiles: tree.materializedFiles,
40166
40284
  cleanup: tree.cleanup
40167
- })))
40285
+ })), withSpan("StagedFiles.materialize"))
40168
40286
  });
40169
40287
  }));
40170
40288
  /**
@@ -40232,7 +40350,10 @@ const runEditorScan = async (input) => {
40232
40350
  isCi: false,
40233
40351
  resolveLocalGithubViewerPermission: false,
40234
40352
  skipJsxIncludeFilter: true
40235
- }).pipe(provide(layers), provide(layerOtlp)));
40353
+ }).pipe(withSpan("runEditorScan", { attributes: {
40354
+ "editor.lint": lint,
40355
+ "editor.runDeadCode": runDeadCode
40356
+ } }), provide(layers), provide(layerOtlp)));
40236
40357
  if (isSuccess(exit)) {
40237
40358
  const output = exit.value;
40238
40359
  return {
@@ -40262,7 +40383,7 @@ const runEditorScan = async (input) => {
40262
40383
  didDeadCodeFail: false,
40263
40384
  deadCodeFailureReason: null,
40264
40385
  lintPartialFailures: [],
40265
- error: error instanceof Error ? error.message : String(error)
40386
+ error: messageFromUnknown(error)
40266
40387
  };
40267
40388
  };
40268
40389
  /**
@@ -40544,7 +40665,6 @@ const toLspDiagnostic = (input) => {
40544
40665
  data
40545
40666
  };
40546
40667
  };
40547
- const toUri = (absoluteFilePath) => fsPathToUri(absoluteFilePath);
40548
40668
  /**
40549
40669
  * Owns the published-diagnostic state. Maps scan outcomes to LSP
40550
40670
  * diagnostics, publishes complete per-URI replacement sets (so the
@@ -40574,7 +40694,7 @@ var DiagnosticsManager = class {
40574
40694
  const isProtectedPath = (fsPath) => protectOpen && this.isOpen(fsPath);
40575
40695
  for (const [fsPath, coreDiagnostics] of outcome.byFile) {
40576
40696
  if (isProtectedPath(fsPath)) continue;
40577
- const uri = toUri(fsPath);
40697
+ const uri = fsPathToUri(fsPath);
40578
40698
  const text = this.textProvider(fsPath);
40579
40699
  const lspDiagnostics = coreDiagnostics.map((diagnostic) => toLspDiagnostic({
40580
40700
  diagnostic,
@@ -40596,7 +40716,7 @@ var DiagnosticsManager = class {
40596
40716
  for (const fsPath of outcome.requestedPaths) {
40597
40717
  if (isProtectedPath(fsPath)) continue;
40598
40718
  if (outcome.byFile.has(fsPath)) continue;
40599
- const uri = toUri(fsPath);
40719
+ const uri = fsPathToUri(fsPath);
40600
40720
  if (this.byUri.has(uri)) this.byUri.delete(uri);
40601
40721
  this.publish(uri, []);
40602
40722
  }
@@ -40621,7 +40741,7 @@ var DiagnosticsManager = class {
40621
40741
  const set = this.projectUris.get(project) ?? /* @__PURE__ */ new Set();
40622
40742
  for (const uri of liveUris) set.add(uri);
40623
40743
  for (const fsPath of outcome.requestedPaths) {
40624
- const uri = toUri(fsPath);
40744
+ const uri = fsPathToUri(fsPath);
40625
40745
  if (!liveUris.has(uri)) set.delete(uri);
40626
40746
  }
40627
40747
  this.projectUris.set(project, set);
@@ -40649,7 +40769,7 @@ var DiagnosticsManager = class {
40649
40769
  const tracked = this.projectUris.get(project);
40650
40770
  if (!tracked) return;
40651
40771
  const liveUris = /* @__PURE__ */ new Set();
40652
- for (const fsPath of liveFsPaths) liveUris.add(toUri(fsPath));
40772
+ for (const fsPath of liveFsPaths) liveUris.add(fsPathToUri(fsPath));
40653
40773
  for (const uri of [...tracked]) {
40654
40774
  if (liveUris.has(uri)) continue;
40655
40775
  this.byUri.delete(uri);
@@ -40946,7 +41066,7 @@ const createProjectGraph = (options) => {
40946
41066
  });
40947
41067
  }
40948
41068
  } catch (error) {
40949
- logger.warn(`Project discovery failed for ${root}: ${error instanceof Error ? error.message : String(error)}`);
41069
+ logger.warn(`Project discovery failed for ${root}: ${messageFromUnknown(error)}`);
40950
41070
  }
40951
41071
  return [...seen.values()].sort((first, second) => second.directory.length - first.directory.length);
40952
41072
  };
@@ -40974,6 +41094,11 @@ const createProjectGraph = (options) => {
40974
41094
  }
40975
41095
  };
40976
41096
  };
41097
+ const toProjectRelative = (projectDirectory, filePath) => {
41098
+ const relative = Path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41099
+ if (relative.length === 0 || relative.startsWith("../") || Path.isAbsolute(relative)) return null;
41100
+ return relative;
41101
+ };
40977
41102
  const resolveCacheFilePath = (projectDirectory) => {
40978
41103
  const nodeModules = path.join(projectDirectory, "node_modules");
40979
41104
  if (fs.existsSync(nodeModules)) return path.join(nodeModules, ".cache", "react-doctor", "lint-cache.json");
@@ -41008,7 +41133,7 @@ const createLintCache = (input) => {
41008
41133
  fs.writeFileSync(tempPath, JSON.stringify(payload));
41009
41134
  fs.renameSync(tempPath, cacheFilePath);
41010
41135
  } catch (error) {
41011
- logger.warn(`Failed to persist lint cache: ${error instanceof Error ? error.message : String(error)}`);
41136
+ logger.warn(`Failed to persist lint cache: ${messageFromUnknown(error)}`);
41012
41137
  }
41013
41138
  };
41014
41139
  return {
@@ -41034,11 +41159,6 @@ const createLintCache = (input) => {
41034
41159
  };
41035
41160
  const OVERLAY_TEMP_PREFIX = "react-doctor-lsp-";
41036
41161
  const OVERLAY_CONFIG_FILENAMES = [...new Set([...STAGED_FILES_PROJECT_CONFIG_FILENAMES, ...ADOPTABLE_LINT_CONFIG_FILENAMES])];
41037
- const toProjectRelative$1 = (projectDirectory, filePath) => {
41038
- const relative = path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41039
- if (relative.length === 0 || relative.startsWith("../") || path.isAbsolute(relative)) return null;
41040
- return relative;
41041
- };
41042
41162
  /**
41043
41163
  * Writes the live (possibly unsaved) content of the target files into a
41044
41164
  * throwaway temp tree that mirrors the project, alongside the well-known
@@ -41052,7 +41172,7 @@ const materializeOverlay = (input) => {
41052
41172
  const relativePaths = [];
41053
41173
  try {
41054
41174
  for (const filePath of input.files) {
41055
- const relative = toProjectRelative$1(input.projectDirectory, filePath);
41175
+ const relative = toProjectRelative(input.projectDirectory, filePath);
41056
41176
  if (relative === null) continue;
41057
41177
  const content = input.readText(filePath);
41058
41178
  if (content === null) continue;
@@ -41100,11 +41220,6 @@ const materializeOverlay = (input) => {
41100
41220
  throw error;
41101
41221
  }
41102
41222
  };
41103
- const toProjectRelative = (projectDirectory, filePath) => {
41104
- const relative = path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41105
- if (relative.length === 0 || relative.startsWith("../") || path.isAbsolute(relative)) return null;
41106
- return relative;
41107
- };
41108
41223
  /**
41109
41224
  * Resolves a diagnostic's (possibly relative, possibly overlay-temp)
41110
41225
  * file path back to the canonical absolute path inside the real project.
@@ -41326,7 +41441,7 @@ const createScheduler = (options) => {
41326
41441
  if (outcome && !token.isCancelled) options.onResult(outcome);
41327
41442
  }).catch((error) => {
41328
41443
  if (options.onError) options.onError(error, request);
41329
- else logger.error(`Scan failed: ${error instanceof Error ? error.message : String(error)}`);
41444
+ else logger.error(`Scan failed: ${messageFromUnknown(error)}`);
41330
41445
  }).finally(() => {
41331
41446
  running -= 1;
41332
41447
  if (isBackground) runningBackground -= 1;
@@ -41713,7 +41828,7 @@ const createServer = (connection, options = {}) => {
41713
41828
  maybeWarnLintUnavailable(outcome);
41714
41829
  if (outcome.request.priority === "background") scanTelemetry.accumulate(outcome);
41715
41830
  },
41716
- onError: (error, request) => logger.error(`Scan of ${request.projectDirectory} threw: ${error instanceof Error ? error.message : String(error)}`),
41831
+ onError: (error, request) => logger.error(`Scan of ${request.projectDirectory} threw: ${messageFromUnknown(error)}`),
41717
41832
  onIdleChange: (idle) => {
41718
41833
  setBusy(!idle);
41719
41834
  if (idle) scanTelemetry.finish();
@@ -42361,5 +42476,5 @@ const startLanguageServer = () => {
42361
42476
  };
42362
42477
  //#endregion
42363
42478
  export { startLanguageServer };
42364
- !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]="9421149c-0ed9-5e00-81da-4f7dd8faf332")}catch(e){}}();
42365
- //# debugId=9421149c-0ed9-5e00-81da-4f7dd8faf332
42479
+ !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]="6fbf847f-43b8-5c5c-ba97-c26c2e08e250")}catch(e){}}();
42480
+ //# debugId=6fbf847f-43b8-5c5c-ba97-c26c2e08e250