react-doctor 0.5.6-dev.f45cb29 → 0.5.7-dev.242bf69

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/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]="a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd")}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]="03972752-ed17-5a0b-a633-71b652d2f457")}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 * as NFS from "node:fs";
@@ -17,6 +17,7 @@ import * as NodeUrl from "node:url";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
+ import { createHash } from "node:crypto";
20
21
  import { gzipSync } from "node:zlib";
21
22
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
22
23
  /**
@@ -19256,7 +19257,8 @@ var Diagnostic = class extends Class("Diagnostic")({
19256
19257
  category: String$1,
19257
19258
  fileContext: optional(Literals(["test", "story"])),
19258
19259
  suppressionHint: optional(String$1),
19259
- relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
19260
+ relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
19261
+ fixGroupId: optional(String$1)
19260
19262
  }) {};
19261
19263
  const JsonReportMode = Literals([
19262
19264
  "full",
@@ -19298,6 +19300,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19298
19300
  score: Unknown,
19299
19301
  skippedChecks: ArraySchema(String$1),
19300
19302
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19303
+ scannedFileCount: optional(Number$1),
19301
19304
  elapsedMilliseconds: Number$1
19302
19305
  }) {};
19303
19306
  /**
@@ -32724,6 +32727,7 @@ const isLargeMinifiedFile = (absolutePath) => {
32724
32727
  if (sizeBytes < 2e4) return false;
32725
32728
  return isMinifiedSource(absolutePath);
32726
32729
  };
32730
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32727
32731
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
32728
32732
  "EACCES",
32729
32733
  "EPERM",
@@ -32733,11 +32737,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32733
32737
  "ELOOP",
32734
32738
  "ENAMETOOLONG"
32735
32739
  ]);
32736
- const isIgnorableReaddirError = (error) => {
32737
- if (typeof error !== "object" || error === null) return false;
32738
- const errorCode = error.code;
32739
- return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
32740
- };
32740
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32741
32741
  const readDirectoryEntries = (directoryPath) => {
32742
32742
  try {
32743
32743
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -32787,7 +32787,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
32787
32787
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
32788
32788
  } catch (error) {
32789
32789
  if (error instanceof SyntaxError) return {};
32790
- if (error instanceof Error && "code" in error) {
32790
+ if (isErrnoException(error)) {
32791
32791
  const { code } = error;
32792
32792
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
32793
32793
  }
@@ -33512,17 +33512,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33512
33512
  return false;
33513
33513
  };
33514
33514
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33515
- const getExpoDependencySpec = (packageJson) => {
33516
- const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
33515
+ const getDependencySpec = (packageJson, packageName) => {
33516
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33517
33517
  return typeof spec === "string" ? spec : null;
33518
33518
  };
33519
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
33519
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33520
33520
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33521
- const getShopifyFlashListDependencySpec = (packageJson) => {
33522
- const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
33523
- return typeof spec === "string" ? spec : null;
33524
- };
33525
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
33521
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33526
33522
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33527
33523
  if (version === null || !isCatalogReference(version)) return version;
33528
33524
  const catalogName = extractCatalogName(version);
@@ -33534,11 +33530,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
33534
33530
  if (!isFile(monorepoPackageJsonPath)) return version;
33535
33531
  return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33536
33532
  };
33537
- const getNextjsDependencySpec = (packageJson) => {
33538
- const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
33539
- return typeof spec === "string" ? spec : null;
33540
- };
33541
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
33533
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33542
33534
  const getPreactVersion = (packageJson) => {
33543
33535
  return {
33544
33536
  ...packageJson.peerDependencies,
@@ -33620,6 +33612,11 @@ const ES_TARGET_YEAR_BY_NAME = {
33620
33612
  esnext: 9999
33621
33613
  };
33622
33614
  /**
33615
+ * tsconfig filenames probed when resolving a project's TypeScript
33616
+ * compiler options — the root config first, then a monorepo base config.
33617
+ */
33618
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33619
+ /**
33623
33620
  * Project-config files that `StagedFiles.materialize` copies into
33624
33621
  * the temp directory alongside staged sources so oxlint resolves
33625
33622
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -33666,6 +33663,13 @@ const APP_ONLY_RULE_KEYS = new Set([
33666
33663
  ]);
33667
33664
  const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33668
33665
  const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33666
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33667
+ "react-doctor/no-derived-state",
33668
+ "react-doctor/no-derived-state-effect",
33669
+ "react-doctor/no-derived-useState",
33670
+ "react-doctor/no-adjust-state-on-prop-change",
33671
+ "react-doctor/no-reset-all-state-on-prop-change"
33672
+ ]);
33669
33673
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33670
33674
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33671
33675
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
@@ -34118,6 +34122,7 @@ const isTailwindAtLeast = (detected, required) => {
34118
34122
  if (detected.major !== required.major) return detected.major > required.major;
34119
34123
  return detected.minor >= required.minor;
34120
34124
  };
34125
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34121
34126
  var InvalidGlobPatternError = class extends Error {
34122
34127
  pattern;
34123
34128
  reason;
@@ -34146,7 +34151,7 @@ const compileGlobPattern = (rawPattern) => {
34146
34151
  try {
34147
34152
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
34148
34153
  } catch (caughtError) {
34149
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
34154
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
34150
34155
  }
34151
34156
  };
34152
34157
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -34242,115 +34247,6 @@ const buildRuleSeverityControls = (config) => {
34242
34247
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34243
34248
  };
34244
34249
  };
34245
- const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34246
- const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34247
- const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34248
- let stringDelimiter = null;
34249
- for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34250
- const character = line[charIndex];
34251
- if (stringDelimiter !== null) {
34252
- if (character === "\\") {
34253
- charIndex++;
34254
- continue;
34255
- }
34256
- if (character === stringDelimiter) stringDelimiter = null;
34257
- continue;
34258
- }
34259
- if (character === "\"" || character === "'" || character === "`") {
34260
- stringDelimiter = character;
34261
- continue;
34262
- }
34263
- if (character === "/" && line[charIndex + 1] === "/") return true;
34264
- }
34265
- return false;
34266
- };
34267
- const findOpenerTagOnLine = (line) => {
34268
- for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34269
- if (match.index === void 0) continue;
34270
- if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34271
- }
34272
- return null;
34273
- };
34274
- const findJsxOpenerSpan = (lines, openerLineIndex) => {
34275
- const openerLine = lines[openerLineIndex];
34276
- if (openerLine === void 0) return null;
34277
- const opener = findOpenerTagOnLine(openerLine);
34278
- if (!opener) return null;
34279
- const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34280
- let braceDepth = 0;
34281
- let innerAngleDepth = 0;
34282
- let stringDelimiter = null;
34283
- for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34284
- const currentLine = lines[lineIndex];
34285
- const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34286
- for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34287
- const character = currentLine[charIndex];
34288
- if (stringDelimiter !== null) {
34289
- if (character === "\\") {
34290
- charIndex++;
34291
- continue;
34292
- }
34293
- if (character === stringDelimiter) stringDelimiter = null;
34294
- continue;
34295
- }
34296
- if (character === "\"" || character === "'" || character === "`") {
34297
- stringDelimiter = character;
34298
- continue;
34299
- }
34300
- if (character === "{") {
34301
- braceDepth++;
34302
- continue;
34303
- }
34304
- if (character === "}") {
34305
- braceDepth--;
34306
- continue;
34307
- }
34308
- if (braceDepth !== 0) continue;
34309
- if (character === "<") {
34310
- const followCharacter = currentLine[charIndex + 1];
34311
- if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34312
- continue;
34313
- }
34314
- if (character !== ">") continue;
34315
- const previousCharacter = currentLine[charIndex - 1];
34316
- const nextCharacter = currentLine[charIndex + 1];
34317
- if (previousCharacter === "=" || nextCharacter === "=") continue;
34318
- if (innerAngleDepth > 0) {
34319
- innerAngleDepth--;
34320
- continue;
34321
- }
34322
- return lineIndex;
34323
- }
34324
- }
34325
- return null;
34326
- };
34327
- const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34328
- for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34329
- const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34330
- if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34331
- }
34332
- return null;
34333
- };
34334
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34335
- const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34336
- const collected = [];
34337
- let isStillInChain = true;
34338
- for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34339
- const candidateLine = lines[candidateIndex];
34340
- if (candidateLine === void 0) break;
34341
- const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34342
- if (match) {
34343
- collected.push({
34344
- commentLineIndex: candidateIndex,
34345
- ruleList: match[1],
34346
- isInChain: isStillInChain
34347
- });
34348
- continue;
34349
- }
34350
- isStillInChain = false;
34351
- }
34352
- return collected;
34353
- };
34354
34250
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34355
34251
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34356
34252
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34475,7 +34371,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34475
34371
  }
34476
34372
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34477
34373
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34478
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34374
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34375
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34376
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34377
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34378
+ if (canonicalCandidate === canonicalTarget) return true;
34379
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34380
+ };
34479
34381
  const getEquivalentRuleKeys = (ruleKey) => {
34480
34382
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34481
34383
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34485,12 +34387,182 @@ const stripDescriptionTail = (ruleList) => {
34485
34387
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34486
34388
  return ruleList.slice(0, descriptionMatch.index);
34487
34389
  };
34488
- const isRuleListedInComment = (ruleList, ruleId) => {
34390
+ const tokenizeRuleList = (ruleList) => {
34489
34391
  const trimmed = ruleList?.trim();
34490
- if (!trimmed) return true;
34392
+ if (!trimmed) return [];
34491
34393
  const ruleSection = stripDescriptionTail(trimmed).trim();
34492
- if (!ruleSection) return true;
34493
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34394
+ if (!ruleSection) return [];
34395
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34396
+ };
34397
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34398
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34399
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34400
+ 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}\`.`;
34401
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34402
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34403
+ const candidates = [{
34404
+ line: lines[diagnosticLineIndex],
34405
+ requiredScope: "line"
34406
+ }, {
34407
+ line: lines[diagnosticLineIndex - 1],
34408
+ requiredScope: "next-line"
34409
+ }];
34410
+ for (const { line, requiredScope } of candidates) {
34411
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34412
+ if (!match) continue;
34413
+ const [, tool, scope, ruleList] = match;
34414
+ if (scope !== requiredScope) continue;
34415
+ const tokens = tokenizeRuleList(ruleList);
34416
+ if (tokens.includes(ruleId)) continue;
34417
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34418
+ }
34419
+ return null;
34420
+ };
34421
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34422
+ let openMisname = null;
34423
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34424
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34425
+ const line = lines[lineIndex];
34426
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34427
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34428
+ if (disableMatch) {
34429
+ const [, tool, ruleList] = disableMatch;
34430
+ const tokens = tokenizeRuleList(ruleList);
34431
+ if (tokens.includes(ruleId)) openMisname = null;
34432
+ else {
34433
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34434
+ if (misnamed) openMisname = {
34435
+ tool,
34436
+ token: misnamed
34437
+ };
34438
+ }
34439
+ continue;
34440
+ }
34441
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34442
+ if (enableMatch) {
34443
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34444
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34445
+ }
34446
+ }
34447
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34448
+ };
34449
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34450
+ if (!ruleId.startsWith("react-doctor/")) return null;
34451
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34452
+ };
34453
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34454
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34455
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34456
+ let stringDelimiter = null;
34457
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34458
+ const character = line[charIndex];
34459
+ if (stringDelimiter !== null) {
34460
+ if (character === "\\") {
34461
+ charIndex++;
34462
+ continue;
34463
+ }
34464
+ if (character === stringDelimiter) stringDelimiter = null;
34465
+ continue;
34466
+ }
34467
+ if (character === "\"" || character === "'" || character === "`") {
34468
+ stringDelimiter = character;
34469
+ continue;
34470
+ }
34471
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34472
+ }
34473
+ return false;
34474
+ };
34475
+ const findOpenerTagOnLine = (line) => {
34476
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34477
+ if (match.index === void 0) continue;
34478
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34479
+ }
34480
+ return null;
34481
+ };
34482
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34483
+ const openerLine = lines[openerLineIndex];
34484
+ if (openerLine === void 0) return null;
34485
+ const opener = findOpenerTagOnLine(openerLine);
34486
+ if (!opener) return null;
34487
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34488
+ let braceDepth = 0;
34489
+ let innerAngleDepth = 0;
34490
+ let stringDelimiter = null;
34491
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34492
+ const currentLine = lines[lineIndex];
34493
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34494
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34495
+ const character = currentLine[charIndex];
34496
+ if (stringDelimiter !== null) {
34497
+ if (character === "\\") {
34498
+ charIndex++;
34499
+ continue;
34500
+ }
34501
+ if (character === stringDelimiter) stringDelimiter = null;
34502
+ continue;
34503
+ }
34504
+ if (character === "\"" || character === "'" || character === "`") {
34505
+ stringDelimiter = character;
34506
+ continue;
34507
+ }
34508
+ if (character === "{") {
34509
+ braceDepth++;
34510
+ continue;
34511
+ }
34512
+ if (character === "}") {
34513
+ braceDepth--;
34514
+ continue;
34515
+ }
34516
+ if (braceDepth !== 0) continue;
34517
+ if (character === "<") {
34518
+ const followCharacter = currentLine[charIndex + 1];
34519
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34520
+ continue;
34521
+ }
34522
+ if (character !== ">") continue;
34523
+ const previousCharacter = currentLine[charIndex - 1];
34524
+ const nextCharacter = currentLine[charIndex + 1];
34525
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34526
+ if (innerAngleDepth > 0) {
34527
+ innerAngleDepth--;
34528
+ continue;
34529
+ }
34530
+ return lineIndex;
34531
+ }
34532
+ }
34533
+ return null;
34534
+ };
34535
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34536
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34537
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34538
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34539
+ }
34540
+ return null;
34541
+ };
34542
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34543
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34544
+ const collected = [];
34545
+ let isStillInChain = true;
34546
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34547
+ const candidateLine = lines[candidateIndex];
34548
+ if (candidateLine === void 0) break;
34549
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34550
+ if (match) {
34551
+ collected.push({
34552
+ commentLineIndex: candidateIndex,
34553
+ ruleList: match[1],
34554
+ isInChain: isStillInChain
34555
+ });
34556
+ continue;
34557
+ }
34558
+ isStillInChain = false;
34559
+ }
34560
+ return collected;
34561
+ };
34562
+ const isRuleListedInComment = (ruleList, ruleId) => {
34563
+ const tokens = tokenizeRuleList(ruleList);
34564
+ if (tokens.length === 0) return true;
34565
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34494
34566
  };
34495
34567
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34496
34568
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34534,7 +34606,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34534
34606
  };
34535
34607
  return {
34536
34608
  isSuppressed: false,
34537
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34609
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34538
34610
  };
34539
34611
  };
34540
34612
  /**
@@ -35328,7 +35400,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
35328
35400
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
35329
35401
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
35330
35402
  const jiti = createJiti(import.meta.url);
35331
- const formatError = (error) => error instanceof Error ? error.message : String(error);
35332
35403
  const importDefaultExport = async (jitiInstance, filePath) => {
35333
35404
  const imported = await jitiInstance.import(filePath);
35334
35405
  return imported?.default ?? imported;
@@ -35360,7 +35431,7 @@ const loadModuleConfig = async (filePath) => {
35360
35431
  try {
35361
35432
  return await importDefaultExport(aliasJiti, filePath);
35362
35433
  } catch (retryError) {
35363
- throw new Error(`${formatError(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${formatError(retryError)})`, { cause: retryError });
35434
+ throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
35364
35435
  }
35365
35436
  }
35366
35437
  };
@@ -35409,7 +35480,7 @@ const loadLegacyConfig = (directory) => {
35409
35480
  }
35410
35481
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
35411
35482
  } catch (error) {
35412
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
35483
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
35413
35484
  }
35414
35485
  return {
35415
35486
  status: "invalid",
@@ -35436,7 +35507,7 @@ const loadConfigFromDirectory = async (directory) => {
35436
35507
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
35437
35508
  sawBrokenConfigFile = true;
35438
35509
  } catch (error) {
35439
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
35510
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
35440
35511
  sawBrokenConfigFile = true;
35441
35512
  }
35442
35513
  }
@@ -35538,6 +35609,29 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
35538
35609
  didRedirectViaRootDir: redirectedDirectory !== null
35539
35610
  };
35540
35611
  };
35612
+ const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
35613
+ diagnostic.filePath,
35614
+ `${diagnostic.plugin}/${diagnostic.rule}`,
35615
+ diagnostic.message
35616
+ ])).digest("hex").slice(0, 16);
35617
+ const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
35618
+ const assignFixGroups = (diagnostics) => {
35619
+ const siteCountByGroupId = /* @__PURE__ */ new Map();
35620
+ for (const diagnostic of diagnostics) {
35621
+ if (!isGroupableRule(diagnostic)) continue;
35622
+ const groupId = buildFixGroupId(diagnostic);
35623
+ siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
35624
+ }
35625
+ return diagnostics.map((diagnostic) => {
35626
+ if (!isGroupableRule(diagnostic)) return diagnostic;
35627
+ const groupId = buildFixGroupId(diagnostic);
35628
+ if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
35629
+ return {
35630
+ ...diagnostic,
35631
+ fixGroupId: groupId
35632
+ };
35633
+ });
35634
+ };
35541
35635
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35542
35636
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35543
35637
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36664,7 +36758,7 @@ const readIgnoreFile = (filePath) => {
36664
36758
  try {
36665
36759
  content = NFS.readFileSync(filePath, "utf-8");
36666
36760
  } catch (error) {
36667
- const errnoCode = error?.code;
36761
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
36668
36762
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
36669
36763
  return [];
36670
36764
  }
@@ -36705,8 +36799,8 @@ const collectIgnorePatterns = (rootDirectory) => {
36705
36799
  cachedPatternsByRoot.set(rootDirectory, patterns);
36706
36800
  return patterns;
36707
36801
  };
36802
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36708
36803
  const KNIP_JSON_FILENAME = "knip.json";
36709
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36710
36804
  const readJsonFileSafe = (filePath) => {
36711
36805
  let rawContents;
36712
36806
  try {
@@ -36722,10 +36816,10 @@ const readJsonFileSafe = (filePath) => {
36722
36816
  };
36723
36817
  const readKnipConfig = (rootDirectory) => {
36724
36818
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
36725
- if (isRecord$1(knipJson)) return knipJson;
36819
+ if (isRecord(knipJson)) return knipJson;
36726
36820
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
36727
- const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
36728
- return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
36821
+ const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
36822
+ return isRecord(packageKnipConfig) ? packageKnipConfig : null;
36729
36823
  };
36730
36824
  const normalizePatternList = (value) => {
36731
36825
  if (typeof value === "string" && value.length > 0) return [value];
@@ -36737,10 +36831,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
36737
36831
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
36738
36832
  };
36739
36833
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
36740
- if (!isRecord$1(workspaces)) return [];
36834
+ if (!isRecord(workspaces)) return [];
36741
36835
  const patterns = [];
36742
36836
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
36743
- if (!isRecord$1(workspaceConfig)) continue;
36837
+ if (!isRecord(workspaceConfig)) continue;
36744
36838
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
36745
36839
  }
36746
36840
  return patterns;
@@ -36785,8 +36879,6 @@ const toCanonicalPath = (filePath) => {
36785
36879
  };
36786
36880
  const DEAD_CODE_PLUGIN = "deslop";
36787
36881
  const DEAD_CODE_CATEGORY = "Maintainability";
36788
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
36789
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36790
36882
  const DEAD_CODE_WORKER_SCRIPT = `
36791
36883
  const inputChunks = [];
36792
36884
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -36844,7 +36936,7 @@ process.stdin.on("end", () => {
36844
36936
  });
36845
36937
  `;
36846
36938
  const resolveTsConfigPath = (rootDirectory) => {
36847
- for (const filename of TSCONFIG_FILENAMES$1) {
36939
+ for (const filename of TSCONFIG_FILENAMES) {
36848
36940
  const candidate = Path.join(rootDirectory, filename);
36849
36941
  if (NFS.existsSync(candidate)) return candidate;
36850
36942
  }
@@ -37225,15 +37317,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37225
37317
  })()) }));
37226
37318
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
37227
37319
  };
37228
- const createNodeReadFileLinesSync = (rootDirectory) => {
37229
- return (filePath) => {
37230
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37231
- try {
37232
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37233
- } catch {
37234
- return null;
37235
- }
37236
- };
37320
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
37321
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37322
+ try {
37323
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37324
+ } catch {
37325
+ return null;
37326
+ }
37237
37327
  };
37238
37328
  var Files = class Files extends Service()("react-doctor/Files") {
37239
37329
  static layerNode = succeed$3(Files, Files.of({
@@ -37444,7 +37534,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
37444
37534
  directory: input.directory,
37445
37535
  cause
37446
37536
  }) });
37447
- }));
37537
+ }), withSpan("git.exec", { attributes: {
37538
+ "git.command": input.command,
37539
+ "git.subcommand": input.args[0] ?? ""
37540
+ } }));
37448
37541
  const runGit = (directory, args) => runCommand({
37449
37542
  command: "git",
37450
37543
  args,
@@ -37472,7 +37565,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37472
37565
  ]);
37473
37566
  if (candidates.status !== 0) return null;
37474
37567
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
37475
- });
37568
+ }).pipe(withSpan("Git.defaultBranch"));
37476
37569
  const branchExists = (directory, branch) => runGit(directory, [
37477
37570
  "rev-parse",
37478
37571
  "--verify",
@@ -37519,7 +37612,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37519
37612
  const result = resultOption.value;
37520
37613
  if (result.status !== 0) return null;
37521
37614
  return parseGithubViewerPermission(result.stdout);
37522
- }).pipe(catch_$1(() => succeed$2(null)));
37615
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
37523
37616
  /**
37524
37617
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
37525
37618
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -37633,7 +37726,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37633
37726
  changedFiles: splitNullSeparated(diff.stdout),
37634
37727
  isCurrentChanges: false
37635
37728
  };
37636
- }),
37729
+ }).pipe(withSpan("Git.diffSelection")),
37637
37730
  stagedFilePaths: (directory) => runGit(directory, [
37638
37731
  "diff",
37639
37732
  "--cached",
@@ -37675,7 +37768,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37675
37768
  status: result.status,
37676
37769
  stdout: result.stdout
37677
37770
  };
37678
- }),
37771
+ }).pipe(withSpan("Git.grep")),
37679
37772
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
37680
37773
  if (files.length === 0) return [];
37681
37774
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -37691,7 +37784,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37691
37784
  ]);
37692
37785
  if (result.status !== 0) return null;
37693
37786
  return parseChangedLineRanges(result.stdout);
37694
- })
37787
+ }).pipe(withSpan("Git.changedLineRanges"))
37695
37788
  });
37696
37789
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37697
37790
  /**
@@ -37906,7 +37999,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37906
37999
  for (const [absolutePath, originalContent] of originalContents) try {
37907
38000
  NFS.writeFileSync(absolutePath, originalContent);
37908
38001
  } catch (error) {
37909
- 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`);
38002
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37910
38003
  }
37911
38004
  };
37912
38005
  const onExit = () => restore();
@@ -38012,7 +38105,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
38012
38105
  try {
38013
38106
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
38014
38107
  } catch (error) {
38015
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
38108
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
38016
38109
  return null;
38017
38110
  }
38018
38111
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -38084,8 +38177,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38084
38177
  }
38085
38178
  return enabled;
38086
38179
  };
38087
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38088
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38180
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38181
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38089
38182
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38090
38183
  const jsPlugins = [];
38091
38184
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38145,7 +38238,6 @@ const resolveOxlintBinary = () => {
38145
38238
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
38146
38239
  };
38147
38240
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
38148
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
38149
38241
  const resolveTsConfigRelativePath = (rootDirectory) => {
38150
38242
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
38151
38243
  return null;
@@ -38517,7 +38609,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
38517
38609
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
38518
38610
  let currentNode = identifier.parent;
38519
38611
  while (currentNode) {
38520
- if (isScopeNode(currentNode)) {
38612
+ if (isScopeBoundary(currentNode)) {
38521
38613
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
38522
38614
  }
38523
38615
  if (currentNode === sourceFile) return false;
@@ -38608,11 +38700,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
38608
38700
  });
38609
38701
  return resolution;
38610
38702
  };
38611
- const isScopeNode = isScopeBoundary;
38612
38703
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
38613
38704
  let currentNode = identifier.parent;
38614
38705
  while (currentNode) {
38615
- if (isScopeNode(currentNode)) {
38706
+ if (isScopeBoundary(currentNode)) {
38616
38707
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
38617
38708
  if (resolution) return resolution;
38618
38709
  }
@@ -38782,9 +38873,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38782
38873
  try {
38783
38874
  parsed = JSON.parse(sanitizedStdout);
38784
38875
  } catch {
38785
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38876
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38786
38877
  }
38787
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38878
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38788
38879
  const minifiedFileCache = /* @__PURE__ */ new Map();
38789
38880
  const isMinifiedDiagnosticFile = (filename) => {
38790
38881
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -38860,7 +38951,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38860
38951
  child.kill("SIGKILL");
38861
38952
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38862
38953
  kind: "timeout",
38863
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
38954
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
38864
38955
  }) }));
38865
38956
  }, spawnTimeoutMs);
38866
38957
  timeoutHandle.unref?.();
@@ -39075,6 +39166,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39075
39166
  NFS.closeSync(fileHandle);
39076
39167
  }
39077
39168
  };
39169
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39170
+ /**
39171
+ * Detects an oxlint config-load crash caused by the optional
39172
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39173
+ * builds the partial-failure note for it; returns `null` when the failure
39174
+ * was anything else.
39175
+ *
39176
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39177
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39178
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39179
+ * config load on it, leaving the plugin in would drop every curated
39180
+ * react-doctor diagnostic too — so the caller retries with the plugin
39181
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39182
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39183
+ */
39184
+ const reactHooksJsPluginDropNote = (error) => {
39185
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39186
+ const { preview } = error.reason;
39187
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39188
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39189
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39190
+ };
39078
39191
  /**
39079
39192
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39080
39193
  *
@@ -39102,15 +39215,16 @@ const runOxlint = async (options) => {
39102
39215
  const pluginPath = resolvePluginPath();
39103
39216
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39104
39217
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39105
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39218
+ const buildConfig = (overrides) => createOxlintConfig({
39106
39219
  pluginPath,
39107
39220
  project,
39108
39221
  customRulesOnly,
39109
- extendsPaths: extendsForThisAttempt,
39222
+ extendsPaths: overrides.extendsPaths,
39110
39223
  ignoredTags,
39111
39224
  serverAuthFunctionNames,
39112
39225
  severityControls,
39113
- userPlugins
39226
+ userPlugins,
39227
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39114
39228
  });
39115
39229
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39116
39230
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39146,12 +39260,22 @@ const runOxlint = async (options) => {
39146
39260
  outputMaxBytes,
39147
39261
  concurrency: options.concurrency
39148
39262
  });
39149
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39263
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39150
39264
  try {
39151
39265
  return await runBatches();
39152
39266
  } catch (error) {
39267
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39268
+ if (reactHooksJsDropNote !== null) {
39269
+ writeOxlintConfig(configPath, buildConfig({
39270
+ extendsPaths,
39271
+ disableReactHooksJsPlugin: true
39272
+ }));
39273
+ const diagnostics = await runBatches();
39274
+ onPartialFailure?.(reactHooksJsDropNote);
39275
+ return diagnostics;
39276
+ }
39153
39277
  if (extendsPaths.length === 0) throw error;
39154
- writeOxlintConfig(configPath, buildConfig([]));
39278
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39155
39279
  return await runBatches();
39156
39280
  }
39157
39281
  } finally {
@@ -39949,17 +40073,17 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39949
40073
  }))))))));
39950
40074
  const deadCodeFailureState = yield* get$2(deadCodeFailure);
39951
40075
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
39952
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
40076
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
39953
40077
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
39954
40078
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39955
40079
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
39956
40080
  yield* reporterService.finalize;
39957
- const finalDiagnostics = [
40081
+ const finalDiagnostics = assignFixGroups([
39958
40082
  ...envCollected,
39959
40083
  ...supplyChainCollected,
39960
40084
  ...lintCollected,
39961
40085
  ...deadCodeCollected
39962
- ];
40086
+ ]);
39963
40087
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
39964
40088
  const scoreMetadata = {
39965
40089
  ...repo !== null ? { repo } : {},
@@ -40163,7 +40287,7 @@ const materializeSourceTree = (input) => gen(function* () {
40163
40287
  static layerNode = effect(StagedFiles, gen(function* () {
40164
40288
  const git = yield* Git;
40165
40289
  return StagedFiles.of({
40166
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
40290
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
40167
40291
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
40168
40292
  directory,
40169
40293
  files: stagedFiles,
@@ -40173,7 +40297,7 @@ const materializeSourceTree = (input) => gen(function* () {
40173
40297
  tempDirectory: tree.tempDirectory,
40174
40298
  stagedFiles: tree.materializedFiles,
40175
40299
  cleanup: tree.cleanup
40176
- })))
40300
+ })), withSpan("StagedFiles.materialize"))
40177
40301
  });
40178
40302
  }));
40179
40303
  /**
@@ -40305,6 +40429,7 @@ const buildJsonReport = (input) => {
40305
40429
  score: result.score,
40306
40430
  skippedChecks: result.skippedChecks,
40307
40431
  ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
40432
+ ...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
40308
40433
  elapsedMilliseconds: result.elapsedMilliseconds
40309
40434
  }));
40310
40435
  const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
@@ -40571,4 +40696,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40571
40696
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40572
40697
 
40573
40698
  //# sourceMappingURL=index.js.map
40574
- //# debugId=a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd
40699
+ //# debugId=03972752-ed17-5a0b-a633-71b652d2f457