react-doctor 0.5.6-dev.431e515 → 0.5.6-dev.44db3e0

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]="3b9fafb6-a936-52a2-96df-a26760fdafc1")}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]="24b11cb3-4e8c-544c-bacf-6393dd5abd59")}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";
@@ -19298,6 +19298,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19298
19298
  score: Unknown,
19299
19299
  skippedChecks: ArraySchema(String$1),
19300
19300
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19301
+ scannedFileCount: optional(Number$1),
19301
19302
  elapsedMilliseconds: Number$1
19302
19303
  }) {};
19303
19304
  /**
@@ -32724,6 +32725,7 @@ const isLargeMinifiedFile = (absolutePath) => {
32724
32725
  if (sizeBytes < 2e4) return false;
32725
32726
  return isMinifiedSource(absolutePath);
32726
32727
  };
32728
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32727
32729
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
32728
32730
  "EACCES",
32729
32731
  "EPERM",
@@ -32733,11 +32735,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32733
32735
  "ELOOP",
32734
32736
  "ENAMETOOLONG"
32735
32737
  ]);
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
- };
32738
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32741
32739
  const readDirectoryEntries = (directoryPath) => {
32742
32740
  try {
32743
32741
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -32787,7 +32785,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
32787
32785
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
32788
32786
  } catch (error) {
32789
32787
  if (error instanceof SyntaxError) return {};
32790
- if (error instanceof Error && "code" in error) {
32788
+ if (isErrnoException(error)) {
32791
32789
  const { code } = error;
32792
32790
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
32793
32791
  }
@@ -33512,17 +33510,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33512
33510
  return false;
33513
33511
  };
33514
33512
  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;
33513
+ const getDependencySpec = (packageJson, packageName) => {
33514
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33517
33515
  return typeof spec === "string" ? spec : null;
33518
33516
  };
33519
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
33517
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33520
33518
  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);
33519
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33526
33520
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33527
33521
  if (version === null || !isCatalogReference(version)) return version;
33528
33522
  const catalogName = extractCatalogName(version);
@@ -33534,11 +33528,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
33534
33528
  if (!isFile(monorepoPackageJsonPath)) return version;
33535
33529
  return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33536
33530
  };
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);
33531
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33542
33532
  const getPreactVersion = (packageJson) => {
33543
33533
  return {
33544
33534
  ...packageJson.peerDependencies,
@@ -33620,6 +33610,11 @@ const ES_TARGET_YEAR_BY_NAME = {
33620
33610
  esnext: 9999
33621
33611
  };
33622
33612
  /**
33613
+ * tsconfig filenames probed when resolving a project's TypeScript
33614
+ * compiler options — the root config first, then a monorepo base config.
33615
+ */
33616
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33617
+ /**
33623
33618
  * Project-config files that `StagedFiles.materialize` copies into
33624
33619
  * the temp directory alongside staged sources so oxlint resolves
33625
33620
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -34118,6 +34113,7 @@ const isTailwindAtLeast = (detected, required) => {
34118
34113
  if (detected.major !== required.major) return detected.major > required.major;
34119
34114
  return detected.minor >= required.minor;
34120
34115
  };
34116
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34121
34117
  var InvalidGlobPatternError = class extends Error {
34122
34118
  pattern;
34123
34119
  reason;
@@ -34146,7 +34142,7 @@ const compileGlobPattern = (rawPattern) => {
34146
34142
  try {
34147
34143
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
34148
34144
  } catch (caughtError) {
34149
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
34145
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
34150
34146
  }
34151
34147
  };
34152
34148
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -34242,115 +34238,6 @@ const buildRuleSeverityControls = (config) => {
34242
34238
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34243
34239
  };
34244
34240
  };
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
34241
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34355
34242
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34356
34243
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34475,7 +34362,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34475
34362
  }
34476
34363
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34477
34364
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34478
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34365
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34366
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34367
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34368
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34369
+ if (canonicalCandidate === canonicalTarget) return true;
34370
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34371
+ };
34479
34372
  const getEquivalentRuleKeys = (ruleKey) => {
34480
34373
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34481
34374
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34485,12 +34378,182 @@ const stripDescriptionTail = (ruleList) => {
34485
34378
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34486
34379
  return ruleList.slice(0, descriptionMatch.index);
34487
34380
  };
34488
- const isRuleListedInComment = (ruleList, ruleId) => {
34381
+ const tokenizeRuleList = (ruleList) => {
34489
34382
  const trimmed = ruleList?.trim();
34490
- if (!trimmed) return true;
34383
+ if (!trimmed) return [];
34491
34384
  const ruleSection = stripDescriptionTail(trimmed).trim();
34492
- if (!ruleSection) return true;
34493
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34385
+ if (!ruleSection) return [];
34386
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34387
+ };
34388
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34389
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34390
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34391
+ 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}\`.`;
34392
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34393
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34394
+ const candidates = [{
34395
+ line: lines[diagnosticLineIndex],
34396
+ requiredScope: "line"
34397
+ }, {
34398
+ line: lines[diagnosticLineIndex - 1],
34399
+ requiredScope: "next-line"
34400
+ }];
34401
+ for (const { line, requiredScope } of candidates) {
34402
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34403
+ if (!match) continue;
34404
+ const [, tool, scope, ruleList] = match;
34405
+ if (scope !== requiredScope) continue;
34406
+ const tokens = tokenizeRuleList(ruleList);
34407
+ if (tokens.includes(ruleId)) continue;
34408
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34409
+ }
34410
+ return null;
34411
+ };
34412
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34413
+ let openMisname = null;
34414
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34415
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34416
+ const line = lines[lineIndex];
34417
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34418
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34419
+ if (disableMatch) {
34420
+ const [, tool, ruleList] = disableMatch;
34421
+ const tokens = tokenizeRuleList(ruleList);
34422
+ if (tokens.includes(ruleId)) openMisname = null;
34423
+ else {
34424
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34425
+ if (misnamed) openMisname = {
34426
+ tool,
34427
+ token: misnamed
34428
+ };
34429
+ }
34430
+ continue;
34431
+ }
34432
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34433
+ if (enableMatch) {
34434
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34435
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34436
+ }
34437
+ }
34438
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34439
+ };
34440
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34441
+ if (!ruleId.startsWith("react-doctor/")) return null;
34442
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34443
+ };
34444
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34445
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34446
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34447
+ let stringDelimiter = null;
34448
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34449
+ const character = line[charIndex];
34450
+ if (stringDelimiter !== null) {
34451
+ if (character === "\\") {
34452
+ charIndex++;
34453
+ continue;
34454
+ }
34455
+ if (character === stringDelimiter) stringDelimiter = null;
34456
+ continue;
34457
+ }
34458
+ if (character === "\"" || character === "'" || character === "`") {
34459
+ stringDelimiter = character;
34460
+ continue;
34461
+ }
34462
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34463
+ }
34464
+ return false;
34465
+ };
34466
+ const findOpenerTagOnLine = (line) => {
34467
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34468
+ if (match.index === void 0) continue;
34469
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34470
+ }
34471
+ return null;
34472
+ };
34473
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34474
+ const openerLine = lines[openerLineIndex];
34475
+ if (openerLine === void 0) return null;
34476
+ const opener = findOpenerTagOnLine(openerLine);
34477
+ if (!opener) return null;
34478
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34479
+ let braceDepth = 0;
34480
+ let innerAngleDepth = 0;
34481
+ let stringDelimiter = null;
34482
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34483
+ const currentLine = lines[lineIndex];
34484
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34485
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34486
+ const character = currentLine[charIndex];
34487
+ if (stringDelimiter !== null) {
34488
+ if (character === "\\") {
34489
+ charIndex++;
34490
+ continue;
34491
+ }
34492
+ if (character === stringDelimiter) stringDelimiter = null;
34493
+ continue;
34494
+ }
34495
+ if (character === "\"" || character === "'" || character === "`") {
34496
+ stringDelimiter = character;
34497
+ continue;
34498
+ }
34499
+ if (character === "{") {
34500
+ braceDepth++;
34501
+ continue;
34502
+ }
34503
+ if (character === "}") {
34504
+ braceDepth--;
34505
+ continue;
34506
+ }
34507
+ if (braceDepth !== 0) continue;
34508
+ if (character === "<") {
34509
+ const followCharacter = currentLine[charIndex + 1];
34510
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34511
+ continue;
34512
+ }
34513
+ if (character !== ">") continue;
34514
+ const previousCharacter = currentLine[charIndex - 1];
34515
+ const nextCharacter = currentLine[charIndex + 1];
34516
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34517
+ if (innerAngleDepth > 0) {
34518
+ innerAngleDepth--;
34519
+ continue;
34520
+ }
34521
+ return lineIndex;
34522
+ }
34523
+ }
34524
+ return null;
34525
+ };
34526
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34527
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34528
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34529
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34530
+ }
34531
+ return null;
34532
+ };
34533
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34534
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34535
+ const collected = [];
34536
+ let isStillInChain = true;
34537
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34538
+ const candidateLine = lines[candidateIndex];
34539
+ if (candidateLine === void 0) break;
34540
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34541
+ if (match) {
34542
+ collected.push({
34543
+ commentLineIndex: candidateIndex,
34544
+ ruleList: match[1],
34545
+ isInChain: isStillInChain
34546
+ });
34547
+ continue;
34548
+ }
34549
+ isStillInChain = false;
34550
+ }
34551
+ return collected;
34552
+ };
34553
+ const isRuleListedInComment = (ruleList, ruleId) => {
34554
+ const tokens = tokenizeRuleList(ruleList);
34555
+ if (tokens.length === 0) return true;
34556
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34494
34557
  };
34495
34558
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34496
34559
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34534,7 +34597,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34534
34597
  };
34535
34598
  return {
34536
34599
  isSuppressed: false,
34537
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34600
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34538
34601
  };
34539
34602
  };
34540
34603
  /**
@@ -35328,7 +35391,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
35328
35391
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
35329
35392
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
35330
35393
  const jiti = createJiti(import.meta.url);
35331
- const formatError = (error) => error instanceof Error ? error.message : String(error);
35332
35394
  const importDefaultExport = async (jitiInstance, filePath) => {
35333
35395
  const imported = await jitiInstance.import(filePath);
35334
35396
  return imported?.default ?? imported;
@@ -35360,7 +35422,7 @@ const loadModuleConfig = async (filePath) => {
35360
35422
  try {
35361
35423
  return await importDefaultExport(aliasJiti, filePath);
35362
35424
  } 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 });
35425
+ 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
35426
  }
35365
35427
  }
35366
35428
  };
@@ -35409,7 +35471,7 @@ const loadLegacyConfig = (directory) => {
35409
35471
  }
35410
35472
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
35411
35473
  } catch (error) {
35412
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
35474
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
35413
35475
  }
35414
35476
  return {
35415
35477
  status: "invalid",
@@ -35436,7 +35498,7 @@ const loadConfigFromDirectory = async (directory) => {
35436
35498
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
35437
35499
  sawBrokenConfigFile = true;
35438
35500
  } catch (error) {
35439
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
35501
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
35440
35502
  sawBrokenConfigFile = true;
35441
35503
  }
35442
35504
  }
@@ -36472,15 +36534,10 @@ const buildCapabilities = (project) => {
36472
36534
  }
36473
36535
  if (project.tailwindVersion !== null) {
36474
36536
  capabilities.add("tailwind");
36475
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
36476
- if (isTailwindAtLeast(tailwind, {
36537
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
36477
36538
  major: 3,
36478
36539
  minor: 4
36479
36540
  })) capabilities.add("tailwind:3.4");
36480
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
36481
- major: 4,
36482
- minor: 0
36483
- })) capabilities.add("tailwind:4");
36484
36541
  }
36485
36542
  if (project.zodVersion !== null) {
36486
36543
  capabilities.add("zod");
@@ -36669,7 +36726,7 @@ const readIgnoreFile = (filePath) => {
36669
36726
  try {
36670
36727
  content = NFS.readFileSync(filePath, "utf-8");
36671
36728
  } catch (error) {
36672
- const errnoCode = error?.code;
36729
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
36673
36730
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
36674
36731
  return [];
36675
36732
  }
@@ -36710,8 +36767,8 @@ const collectIgnorePatterns = (rootDirectory) => {
36710
36767
  cachedPatternsByRoot.set(rootDirectory, patterns);
36711
36768
  return patterns;
36712
36769
  };
36770
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36713
36771
  const KNIP_JSON_FILENAME = "knip.json";
36714
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36715
36772
  const readJsonFileSafe = (filePath) => {
36716
36773
  let rawContents;
36717
36774
  try {
@@ -36727,10 +36784,10 @@ const readJsonFileSafe = (filePath) => {
36727
36784
  };
36728
36785
  const readKnipConfig = (rootDirectory) => {
36729
36786
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
36730
- if (isRecord$1(knipJson)) return knipJson;
36787
+ if (isRecord(knipJson)) return knipJson;
36731
36788
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
36732
- const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
36733
- return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
36789
+ const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
36790
+ return isRecord(packageKnipConfig) ? packageKnipConfig : null;
36734
36791
  };
36735
36792
  const normalizePatternList = (value) => {
36736
36793
  if (typeof value === "string" && value.length > 0) return [value];
@@ -36742,10 +36799,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
36742
36799
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
36743
36800
  };
36744
36801
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
36745
- if (!isRecord$1(workspaces)) return [];
36802
+ if (!isRecord(workspaces)) return [];
36746
36803
  const patterns = [];
36747
36804
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
36748
- if (!isRecord$1(workspaceConfig)) continue;
36805
+ if (!isRecord(workspaceConfig)) continue;
36749
36806
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
36750
36807
  }
36751
36808
  return patterns;
@@ -36790,8 +36847,6 @@ const toCanonicalPath = (filePath) => {
36790
36847
  };
36791
36848
  const DEAD_CODE_PLUGIN = "deslop";
36792
36849
  const DEAD_CODE_CATEGORY = "Maintainability";
36793
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
36794
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36795
36850
  const DEAD_CODE_WORKER_SCRIPT = `
36796
36851
  const inputChunks = [];
36797
36852
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -36849,7 +36904,7 @@ process.stdin.on("end", () => {
36849
36904
  });
36850
36905
  `;
36851
36906
  const resolveTsConfigPath = (rootDirectory) => {
36852
- for (const filename of TSCONFIG_FILENAMES$1) {
36907
+ for (const filename of TSCONFIG_FILENAMES) {
36853
36908
  const candidate = Path.join(rootDirectory, filename);
36854
36909
  if (NFS.existsSync(candidate)) return candidate;
36855
36910
  }
@@ -37230,15 +37285,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37230
37285
  })()) }));
37231
37286
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
37232
37287
  };
37233
- const createNodeReadFileLinesSync = (rootDirectory) => {
37234
- return (filePath) => {
37235
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37236
- try {
37237
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37238
- } catch {
37239
- return null;
37240
- }
37241
- };
37288
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
37289
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37290
+ try {
37291
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37292
+ } catch {
37293
+ return null;
37294
+ }
37242
37295
  };
37243
37296
  var Files = class Files extends Service()("react-doctor/Files") {
37244
37297
  static layerNode = succeed$3(Files, Files.of({
@@ -37449,7 +37502,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
37449
37502
  directory: input.directory,
37450
37503
  cause
37451
37504
  }) });
37452
- }));
37505
+ }), withSpan("git.exec", { attributes: {
37506
+ "git.command": input.command,
37507
+ "git.subcommand": input.args[0] ?? ""
37508
+ } }));
37453
37509
  const runGit = (directory, args) => runCommand({
37454
37510
  command: "git",
37455
37511
  args,
@@ -37477,7 +37533,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37477
37533
  ]);
37478
37534
  if (candidates.status !== 0) return null;
37479
37535
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
37480
- });
37536
+ }).pipe(withSpan("Git.defaultBranch"));
37481
37537
  const branchExists = (directory, branch) => runGit(directory, [
37482
37538
  "rev-parse",
37483
37539
  "--verify",
@@ -37524,7 +37580,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37524
37580
  const result = resultOption.value;
37525
37581
  if (result.status !== 0) return null;
37526
37582
  return parseGithubViewerPermission(result.stdout);
37527
- }).pipe(catch_$1(() => succeed$2(null)));
37583
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
37528
37584
  /**
37529
37585
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
37530
37586
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -37638,7 +37694,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37638
37694
  changedFiles: splitNullSeparated(diff.stdout),
37639
37695
  isCurrentChanges: false
37640
37696
  };
37641
- }),
37697
+ }).pipe(withSpan("Git.diffSelection")),
37642
37698
  stagedFilePaths: (directory) => runGit(directory, [
37643
37699
  "diff",
37644
37700
  "--cached",
@@ -37680,7 +37736,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37680
37736
  status: result.status,
37681
37737
  stdout: result.stdout
37682
37738
  };
37683
- }),
37739
+ }).pipe(withSpan("Git.grep")),
37684
37740
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
37685
37741
  if (files.length === 0) return [];
37686
37742
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -37696,7 +37752,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37696
37752
  ]);
37697
37753
  if (result.status !== 0) return null;
37698
37754
  return parseChangedLineRanges(result.stdout);
37699
- })
37755
+ }).pipe(withSpan("Git.changedLineRanges"))
37700
37756
  });
37701
37757
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37702
37758
  /**
@@ -37911,7 +37967,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37911
37967
  for (const [absolutePath, originalContent] of originalContents) try {
37912
37968
  NFS.writeFileSync(absolutePath, originalContent);
37913
37969
  } catch (error) {
37914
- 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`);
37970
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37915
37971
  }
37916
37972
  };
37917
37973
  const onExit = () => restore();
@@ -38017,7 +38073,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
38017
38073
  try {
38018
38074
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
38019
38075
  } catch (error) {
38020
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
38076
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
38021
38077
  return null;
38022
38078
  }
38023
38079
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -38150,7 +38206,6 @@ const resolveOxlintBinary = () => {
38150
38206
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
38151
38207
  };
38152
38208
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
38153
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
38154
38209
  const resolveTsConfigRelativePath = (rootDirectory) => {
38155
38210
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
38156
38211
  return null;
@@ -38522,7 +38577,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
38522
38577
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
38523
38578
  let currentNode = identifier.parent;
38524
38579
  while (currentNode) {
38525
- if (isScopeNode(currentNode)) {
38580
+ if (isScopeBoundary(currentNode)) {
38526
38581
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
38527
38582
  }
38528
38583
  if (currentNode === sourceFile) return false;
@@ -38613,11 +38668,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
38613
38668
  });
38614
38669
  return resolution;
38615
38670
  };
38616
- const isScopeNode = isScopeBoundary;
38617
38671
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
38618
38672
  let currentNode = identifier.parent;
38619
38673
  while (currentNode) {
38620
- if (isScopeNode(currentNode)) {
38674
+ if (isScopeBoundary(currentNode)) {
38621
38675
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
38622
38676
  if (resolution) return resolution;
38623
38677
  }
@@ -38865,7 +38919,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38865
38919
  child.kill("SIGKILL");
38866
38920
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38867
38921
  kind: "timeout",
38868
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
38922
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
38869
38923
  }) }));
38870
38924
  }, spawnTimeoutMs);
38871
38925
  timeoutHandle.unref?.();
@@ -39987,7 +40041,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39987
40041
  }))))))));
39988
40042
  const deadCodeFailureState = yield* get$2(deadCodeFailure);
39989
40043
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
39990
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
40044
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
39991
40045
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
39992
40046
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39993
40047
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
@@ -40201,7 +40255,7 @@ const materializeSourceTree = (input) => gen(function* () {
40201
40255
  static layerNode = effect(StagedFiles, gen(function* () {
40202
40256
  const git = yield* Git;
40203
40257
  return StagedFiles.of({
40204
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
40258
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
40205
40259
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
40206
40260
  directory,
40207
40261
  files: stagedFiles,
@@ -40211,7 +40265,7 @@ const materializeSourceTree = (input) => gen(function* () {
40211
40265
  tempDirectory: tree.tempDirectory,
40212
40266
  stagedFiles: tree.materializedFiles,
40213
40267
  cleanup: tree.cleanup
40214
- })))
40268
+ })), withSpan("StagedFiles.materialize"))
40215
40269
  });
40216
40270
  }));
40217
40271
  /**
@@ -40343,6 +40397,7 @@ const buildJsonReport = (input) => {
40343
40397
  score: result.score,
40344
40398
  skippedChecks: result.skippedChecks,
40345
40399
  ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
40400
+ ...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
40346
40401
  elapsedMilliseconds: result.elapsedMilliseconds
40347
40402
  }));
40348
40403
  const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
@@ -40609,4 +40664,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40609
40664
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40610
40665
 
40611
40666
  //# sourceMappingURL=index.js.map
40612
- //# debugId=3b9fafb6-a936-52a2-96df-a26760fdafc1
40667
+ //# debugId=24b11cb3-4e8c-544c-bacf-6393dd5abd59