react-doctor 0.2.1 → 0.2.2

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,33 +1,12 @@
1
+ import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-q7NCDQ7-.js";
1
2
  import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import fs from "node:fs";
4
5
  import { spawn, spawnSync } from "node:child_process";
5
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, BUILTIN_A11Y_RULES, BUILTIN_REACT_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, YOU_MIGHT_NOT_NEED_EFFECT_RULES } from "oxlint-plugin-react-doctor";
6
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES } from "oxlint-plugin-react-doctor";
7
+ import { gzipSync } from "node:zlib";
6
8
  import os from "node:os";
7
- import * as ts from "typescript";
8
- //#region \0rolldown/runtime.js
9
- var __create$1 = Object.create;
10
- var __defProp$1 = Object.defineProperty;
11
- var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
12
- var __getOwnPropNames$1 = Object.getOwnPropertyNames;
13
- var __getProtoOf$1 = Object.getPrototypeOf;
14
- var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
15
- var __commonJSMin$1 = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
16
- var __copyProps$1 = (to, from, except, desc) => {
17
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames$1(from), i = 0, n = keys.length, key; i < n; i++) {
18
- key = keys[i];
19
- if (!__hasOwnProp$1.call(to, key) && key !== except) __defProp$1(to, key, {
20
- get: ((k) => from[k]).bind(null, key),
21
- enumerable: !(desc = __getOwnPropDesc$1(from, key)) || desc.enumerable
22
- });
23
- }
24
- return to;
25
- };
26
- var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(__getProtoOf$1(mod)) : {}, __copyProps$1(isNodeMode || !mod || !mod.__esModule ? __defProp$1(target, "default", {
27
- value: mod,
28
- enumerable: true
29
- }) : target, mod));
30
- //#endregion
9
+ import * as ts$1 from "typescript";
31
10
  //#region ../types/dist/index.js
32
11
  const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
33
12
  "react-native",
@@ -680,19 +659,6 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
680
659
  }
681
660
  return result;
682
661
  };
683
- const REACT_DEPENDENCY_NAMES = new Set([
684
- "react",
685
- "react-native",
686
- "next"
687
- ]);
688
- const hasReactDependency = (packageJson) => {
689
- const allDependencies = {
690
- ...packageJson.peerDependencies,
691
- ...packageJson.dependencies,
692
- ...packageJson.devDependencies
693
- };
694
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
695
- };
696
662
  const findDependencyInfoFromMonorepoRoot = (directory) => {
697
663
  const monorepoRoot = findMonorepoRoot(directory);
698
664
  if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
@@ -720,13 +686,13 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
720
686
  "peerDependencies"
721
687
  ]
722
688
  }) : null;
723
- const shouldUseReactFallback = leafPackageJson ? hasReactDependency(leafPackageJson) : true;
689
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
724
690
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
725
691
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
726
692
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
727
693
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
728
694
  return {
729
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : null,
695
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
730
696
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
731
697
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
732
698
  };
@@ -793,6 +759,19 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
793
759
  if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
794
760
  return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
795
761
  };
762
+ const REACT_DEPENDENCY_NAMES = new Set([
763
+ "react",
764
+ "react-native",
765
+ "next"
766
+ ]);
767
+ const hasReactDependency = (packageJson) => {
768
+ const allDependencies = {
769
+ ...packageJson.peerDependencies,
770
+ ...packageJson.dependencies,
771
+ ...packageJson.devDependencies
772
+ };
773
+ return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
774
+ };
796
775
  const listWorkspacePackages = (rootDirectory) => {
797
776
  const packageJsonPath = path.join(rootDirectory, "package.json");
798
777
  if (!isFile(packageJsonPath)) return [];
@@ -3012,6 +2991,136 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
3012
2991
  category: diagnostic.category,
3013
2992
  tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
3014
2993
  });
2994
+ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
2995
+ "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
2996
+ "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
2997
+ "effect/no-derived-state": "react-doctor/no-derived-state",
2998
+ "effect/no-event-handler": "react-doctor/no-event-handler",
2999
+ "effect/no-initialize-state": "react-doctor/no-initialize-state",
3000
+ "effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
3001
+ "effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
3002
+ "effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
3003
+ "jsx-a11y/alt-text": "react-doctor/alt-text",
3004
+ "jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
3005
+ "jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
3006
+ "jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
3007
+ "jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
3008
+ "jsx-a11y/aria-props": "react-doctor/aria-props",
3009
+ "jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
3010
+ "jsx-a11y/aria-role": "react-doctor/aria-role",
3011
+ "jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
3012
+ "jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
3013
+ "jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
3014
+ "jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
3015
+ "jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
3016
+ "jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
3017
+ "jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
3018
+ "jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
3019
+ "jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
3020
+ "jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
3021
+ "jsx-a11y/lang": "react-doctor/lang",
3022
+ "jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
3023
+ "jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
3024
+ "jsx-a11y/no-access-key": "react-doctor/no-access-key",
3025
+ "jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
3026
+ "jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
3027
+ "jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
3028
+ "jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
3029
+ "jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
3030
+ "jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
3031
+ "jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
3032
+ "jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
3033
+ "jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
3034
+ "jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
3035
+ "jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
3036
+ "jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
3037
+ "jsx-a11y/scope": "react-doctor/scope",
3038
+ "jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
3039
+ "react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
3040
+ "react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
3041
+ "react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
3042
+ "react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
3043
+ "react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
3044
+ "react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
3045
+ "react-refresh/only-export-components": "react-doctor/only-export-components",
3046
+ "react/button-has-type": "react-doctor/button-has-type",
3047
+ "react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
3048
+ "react/display-name": "react-doctor/display-name",
3049
+ "react/exhaustive-deps": "react-doctor/exhaustive-deps",
3050
+ "react/forbid-component-props": "react-doctor/forbid-component-props",
3051
+ "react/forbid-dom-props": "react-doctor/forbid-dom-props",
3052
+ "react/forbid-elements": "react-doctor/forbid-elements",
3053
+ "react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
3054
+ "react/hook-use-state": "react-doctor/hook-use-state",
3055
+ "react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
3056
+ "react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
3057
+ "react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
3058
+ "react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
3059
+ "react/jsx-fragments": "react-doctor/jsx-fragments",
3060
+ "react/jsx-handler-names": "react-doctor/jsx-handler-names",
3061
+ "react/jsx-key": "react-doctor/jsx-key",
3062
+ "react/jsx-max-depth": "react-doctor/jsx-max-depth",
3063
+ "react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
3064
+ "react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
3065
+ "react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
3066
+ "react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
3067
+ "react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
3068
+ "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
3069
+ "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
3070
+ "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
3071
+ "react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
3072
+ "react/jsx-no-undef": "react-doctor/jsx-no-undef",
3073
+ "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
3074
+ "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
3075
+ "react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
3076
+ "react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
3077
+ "react/no-array-index-key": "react-doctor/no-array-index-key",
3078
+ "react/no-children-prop": "react-doctor/no-children-prop",
3079
+ "react/no-clone-element": "react-doctor/no-clone-element",
3080
+ "react/no-danger": "react-doctor/no-danger",
3081
+ "react/no-danger-with-children": "react-doctor/no-danger-with-children",
3082
+ "react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
3083
+ "react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
3084
+ "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
3085
+ "react/no-find-dom-node": "react-doctor/no-find-dom-node",
3086
+ "react/no-is-mounted": "react-doctor/no-is-mounted",
3087
+ "react/no-multi-comp": "react-doctor/no-multi-comp",
3088
+ "react/no-namespace": "react-doctor/no-namespace",
3089
+ "react/no-react-children": "react-doctor/no-react-children",
3090
+ "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
3091
+ "react/no-render-return-value": "react-doctor/no-render-return-value",
3092
+ "react/no-set-state": "react-doctor/no-set-state",
3093
+ "react/no-string-refs": "react-doctor/no-string-refs",
3094
+ "react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
3095
+ "react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
3096
+ "react/no-unknown-property": "react-doctor/no-unknown-property",
3097
+ "react/no-unsafe": "react-doctor/no-unsafe",
3098
+ "react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
3099
+ "react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
3100
+ "react/only-export-components": "react-doctor/only-export-components",
3101
+ "react/prefer-es6-class": "react-doctor/prefer-es6-class",
3102
+ "react/prefer-function-component": "react-doctor/prefer-function-component",
3103
+ "react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
3104
+ "react/require-render-return": "react-doctor/require-render-return",
3105
+ "react/rules-of-hooks": "react-doctor/rules-of-hooks",
3106
+ "react/self-closing-comp": "react-doctor/self-closing-comp",
3107
+ "react/state-in-constructor": "react-doctor/state-in-constructor",
3108
+ "react/style-prop-object": "react-doctor/style-prop-object",
3109
+ "react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
3110
+ };
3111
+ const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
3112
+ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
3113
+ const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
3114
+ aliases.push(legacyRuleKey);
3115
+ NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
3116
+ }
3117
+ const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
3118
+ const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
3119
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
3120
+ const getEquivalentRuleKeys = (ruleKey) => {
3121
+ const nativeRuleKey = canonicalizeRuleKey(ruleKey);
3122
+ return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
3123
+ };
3015
3124
  /**
3016
3125
  * Resolves the user-configured severity override for a rule.
3017
3126
  * Per-rule overrides win over per-category overrides. Returns
@@ -3020,7 +3129,14 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
3020
3129
  */
3021
3130
  const resolveRuleSeverityOverride = (input, controls) => {
3022
3131
  if (!controls) return void 0;
3023
- return controls.rules?.[input.ruleKey] ?? (input.category !== void 0 ? controls.categories?.[input.category] : void 0);
3132
+ const exactRuleOverride = controls.rules?.[input.ruleKey];
3133
+ if (exactRuleOverride !== void 0) return exactRuleOverride;
3134
+ for (const equivalentRuleKey of getEquivalentRuleKeys(input.ruleKey)) {
3135
+ if (equivalentRuleKey === input.ruleKey) continue;
3136
+ const equivalentRuleOverride = controls.rules?.[equivalentRuleKey];
3137
+ if (equivalentRuleOverride !== void 0) return equivalentRuleOverride;
3138
+ }
3139
+ return input.category !== void 0 ? controls.categories?.[input.category] : void 0;
3024
3140
  };
3025
3141
  const SEVERITY_FOR_OVERRIDE = {
3026
3142
  error: "error",
@@ -3229,14 +3345,19 @@ const describeFailure = (error) => {
3229
3345
  if (error instanceof Error && error.message) return error.message;
3230
3346
  return String(error);
3231
3347
  };
3232
- const calculateScore = async (diagnostics) => {
3348
+ const calculateScore = async (diagnostics, options = {}) => {
3233
3349
  const controller = new AbortController();
3234
3350
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3351
+ const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
3235
3352
  try {
3236
- const response = await fetch(SCORE_API_URL, {
3353
+ const compressedBody = gzipSync(JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }));
3354
+ const response = await fetch(requestUrl, {
3237
3355
  method: "POST",
3238
- headers: { "Content-Type": "application/json" },
3239
- body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
3356
+ headers: {
3357
+ "Content-Type": "application/json",
3358
+ "Content-Encoding": "gzip"
3359
+ },
3360
+ body: compressedBody,
3240
3361
  signal: controller.signal
3241
3362
  });
3242
3363
  if (!response.ok) {
@@ -3323,60 +3444,6 @@ const canOxlintExtendConfig = (configPath) => {
3323
3444
  if (extendsEntries.length === 0) return true;
3324
3445
  return extendsEntries.some((entry) => typeof entry === "string" && isLocalPathExtend(entry));
3325
3446
  };
3326
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
3327
- const REDUCED_MOTION_FILE_GLOBS = [
3328
- "*.ts",
3329
- "*.tsx",
3330
- "*.js",
3331
- "*.jsx",
3332
- "*.css",
3333
- "*.scss"
3334
- ];
3335
- const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
3336
- filePath: "package.json",
3337
- plugin: "react-doctor",
3338
- rule: "require-reduced-motion",
3339
- severity: "error",
3340
- message: "Project uses a motion library but has no prefers-reduced-motion handling — required for accessibility (WCAG 2.3.3)",
3341
- help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
3342
- line: 0,
3343
- column: 0,
3344
- category: "Accessibility"
3345
- };
3346
- const checkReducedMotion = (rootDirectory) => {
3347
- const packageJsonPath = path.join(rootDirectory, "package.json");
3348
- if (!isFile(packageJsonPath)) return [];
3349
- let hasMotionLibrary = false;
3350
- try {
3351
- const packageJson = readPackageJson(packageJsonPath);
3352
- const allDependencies = {
3353
- ...packageJson.dependencies,
3354
- ...packageJson.devDependencies
3355
- };
3356
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
3357
- } catch {
3358
- return [];
3359
- }
3360
- if (!hasMotionLibrary) return [];
3361
- const result = spawnSync("git", [
3362
- "grep",
3363
- "-ql",
3364
- "-E",
3365
- REDUCED_MOTION_GREP_PATTERN,
3366
- "--",
3367
- ...REDUCED_MOTION_FILE_GLOBS
3368
- ], {
3369
- cwd: rootDirectory,
3370
- stdio: [
3371
- "ignore",
3372
- "pipe",
3373
- "pipe"
3374
- ]
3375
- });
3376
- if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3377
- if (result.status === 0) return [];
3378
- return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3379
- };
3380
3447
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
3381
3448
  const FALSY_VALUES = new Set([
3382
3449
  "false",
@@ -3556,6 +3623,148 @@ const collectIgnorePatterns = (rootDirectory) => {
3556
3623
  cachedPatternsByRoot.set(rootDirectory, patterns);
3557
3624
  return patterns;
3558
3625
  };
3626
+ const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
3627
+ const resolveTsConfigPath = (rootDirectory) => {
3628
+ for (const filename of TSCONFIG_FILENAMES$1) {
3629
+ const candidate = path.join(rootDirectory, filename);
3630
+ if (fs.existsSync(candidate)) return candidate;
3631
+ }
3632
+ };
3633
+ const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
3634
+ const seen = /* @__PURE__ */ new Set();
3635
+ const sources = [
3636
+ readIgnoreFile(path.join(rootDirectory, ".gitignore")),
3637
+ collectIgnorePatterns(rootDirectory),
3638
+ userConfig?.ignore?.files ?? []
3639
+ ];
3640
+ for (const source of sources) for (const pattern of source) seen.add(pattern);
3641
+ return [...seen].filter((pattern) => pattern.length > 0);
3642
+ };
3643
+ const toRelativeFilePath = (rootDirectory, filePath) => {
3644
+ const relative = toRelativePath(filePath, rootDirectory);
3645
+ return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
3646
+ };
3647
+ const checkDeadCode = async (options) => {
3648
+ const { rootDirectory, userConfig } = options;
3649
+ if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
3650
+ const { analyze, defineConfig } = await import("./dist-BPzE37C6.js");
3651
+ const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
3652
+ const result = await analyze(defineConfig({
3653
+ rootDir: rootDirectory,
3654
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
3655
+ ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
3656
+ }));
3657
+ const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
3658
+ const diagnostics = [];
3659
+ for (const unusedFile of result.unusedFiles) diagnostics.push({
3660
+ filePath: toRelative(unusedFile.path),
3661
+ plugin: "deslop",
3662
+ rule: "unused-file",
3663
+ severity: "warning",
3664
+ message: "Unused file — not reachable from any entry point",
3665
+ help: "Delete the file if it is truly unreachable, or import it from an entry point.",
3666
+ line: 0,
3667
+ column: 0,
3668
+ category: "Dead Code"
3669
+ });
3670
+ for (const unusedExport of result.unusedExports) {
3671
+ const label = unusedExport.isTypeOnly ? "type export" : "export";
3672
+ diagnostics.push({
3673
+ filePath: toRelative(unusedExport.path),
3674
+ plugin: "deslop",
3675
+ rule: unusedExport.isTypeOnly ? "unused-type" : "unused-export",
3676
+ severity: "warning",
3677
+ message: `Unused ${label}: \`${unusedExport.name}\``,
3678
+ help: "Drop the `export` keyword (or remove the declaration) if no other module uses this symbol.",
3679
+ line: unusedExport.line,
3680
+ column: unusedExport.column,
3681
+ category: "Dead Code"
3682
+ });
3683
+ }
3684
+ for (const unusedDependency of result.unusedDependencies) {
3685
+ const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
3686
+ diagnostics.push({
3687
+ filePath: "package.json",
3688
+ plugin: "deslop",
3689
+ rule: unusedDependency.isDevDependency ? "unused-dev-dependency" : "unused-dependency",
3690
+ severity: "warning",
3691
+ message: `Unused ${label}: \`${unusedDependency.name}\``,
3692
+ help: "Remove the dependency from package.json if it is genuinely unused.",
3693
+ line: 0,
3694
+ column: 0,
3695
+ category: "Dead Code"
3696
+ });
3697
+ }
3698
+ for (const cycle of result.circularDependencies) {
3699
+ if (cycle.files.length === 0) continue;
3700
+ diagnostics.push({
3701
+ filePath: toRelative(cycle.files[0]),
3702
+ plugin: "deslop",
3703
+ rule: "circular-dependency",
3704
+ severity: "warning",
3705
+ message: `Circular import cycle: ${cycle.files.map(toRelative).join(" → ")}`,
3706
+ help: "Break the cycle by extracting the shared code into a third module that both files import.",
3707
+ line: 0,
3708
+ column: 0,
3709
+ category: "Dead Code"
3710
+ });
3711
+ }
3712
+ return diagnostics;
3713
+ };
3714
+ const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
3715
+ const REDUCED_MOTION_FILE_GLOBS = [
3716
+ "*.ts",
3717
+ "*.tsx",
3718
+ "*.js",
3719
+ "*.jsx",
3720
+ "*.css",
3721
+ "*.scss"
3722
+ ];
3723
+ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
3724
+ filePath: "package.json",
3725
+ plugin: "react-doctor",
3726
+ rule: "require-reduced-motion",
3727
+ severity: "error",
3728
+ message: "Project uses a motion library but has no prefers-reduced-motion handling — required for accessibility (WCAG 2.3.3)",
3729
+ help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
3730
+ line: 0,
3731
+ column: 0,
3732
+ category: "Accessibility"
3733
+ };
3734
+ const checkReducedMotion = (rootDirectory) => {
3735
+ const packageJsonPath = path.join(rootDirectory, "package.json");
3736
+ if (!isFile(packageJsonPath)) return [];
3737
+ let hasMotionLibrary = false;
3738
+ try {
3739
+ const packageJson = readPackageJson(packageJsonPath);
3740
+ const allDependencies = {
3741
+ ...packageJson.dependencies,
3742
+ ...packageJson.devDependencies
3743
+ };
3744
+ hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
3745
+ } catch {
3746
+ return [];
3747
+ }
3748
+ if (!hasMotionLibrary) return [];
3749
+ const result = spawnSync("git", [
3750
+ "grep",
3751
+ "-ql",
3752
+ "-E",
3753
+ REDUCED_MOTION_GREP_PATTERN,
3754
+ "--",
3755
+ ...REDUCED_MOTION_FILE_GLOBS
3756
+ ], {
3757
+ cwd: rootDirectory,
3758
+ stdio: [
3759
+ "ignore",
3760
+ "pipe",
3761
+ "pipe"
3762
+ ]
3763
+ });
3764
+ if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3765
+ if (result.status === 0) return [];
3766
+ return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3767
+ };
3559
3768
  const createNodeReadFileLinesSync = (rootDirectory) => {
3560
3769
  return (filePath) => {
3561
3770
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
@@ -3685,7 +3894,7 @@ const isRuleListedInComment = (ruleList, ruleId) => {
3685
3894
  if (!trimmed) return true;
3686
3895
  const ruleSection = stripDescriptionTail(trimmed).trim();
3687
3896
  if (!ruleSection) return true;
3688
- return ruleSection.split(/[,\s]+/).some((token) => token.trim() === ruleId);
3897
+ return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
3689
3898
  };
3690
3899
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
3691
3900
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -3837,6 +4046,10 @@ const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrap
3837
4046
  }
3838
4047
  return false;
3839
4048
  };
4049
+ const isIgnoredRule = (ignoredRules, ruleIdentifier) => {
4050
+ for (const ignoredRule of ignoredRules) if (isSameRuleKey(ignoredRule, ruleIdentifier)) return true;
4051
+ return false;
4052
+ };
3840
4053
  const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
3841
4054
  const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
3842
4055
  const ignoredFilePatterns = compileIgnoredFilePatterns(config);
@@ -3847,8 +4060,7 @@ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLi
3847
4060
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
3848
4061
  const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
3849
4062
  return diagnostics.filter((diagnostic) => {
3850
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
3851
- if (ignoredRules.has(ruleIdentifier)) return false;
4063
+ if (isIgnoredRule(ignoredRules, `${diagnostic.plugin}/${diagnostic.rule}`)) return false;
3852
4064
  if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
3853
4065
  if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
3854
4066
  if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
@@ -3876,11 +4088,24 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
3876
4088
  }] : [diagnostic];
3877
4089
  });
3878
4090
  };
3879
- const TEST_FILE_PATH_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\/|\.(?:test|spec|stories|story|fixture|fixtures)\.(?:[cm]?[jt]sx?)$/;
4091
+ const TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\//;
4092
+ const TEST_FILE_SUFFIX_PATTERN = /\.(?:test|spec|stories|story|fixture|fixtures)\.(?:[cm]?[jt]sx?)$/;
4093
+ const FIXTURE_PROJECT_PATTERN = /\/(?:fixtures|__fixtures__)\//;
4094
+ const SOURCE_ROOT_PATTERN = /\/(?:src|app|lib|components|pages|features|modules|packages|apps|frontend|client)\//g;
4095
+ const stripAboveSourceRoot = (relativePath) => {
4096
+ const fixtureMatch = FIXTURE_PROJECT_PATTERN.exec(relativePath);
4097
+ if (fixtureMatch === null) return relativePath;
4098
+ let lastIdx = -1;
4099
+ for (const match of relativePath.matchAll(SOURCE_ROOT_PATTERN)) if (match.index !== void 0 && match.index > lastIdx) lastIdx = match.index;
4100
+ if (lastIdx >= 0) return relativePath.slice(lastIdx);
4101
+ return relativePath.slice(fixtureMatch.index + fixtureMatch[0].length - 1);
4102
+ };
3880
4103
  const isTestFilePath = (relativePath) => {
3881
4104
  if (relativePath.length === 0) return false;
3882
4105
  const forwardSlashed = relativePath.replaceAll("\\", "/");
3883
- return TEST_FILE_PATH_PATTERN.test(forwardSlashed);
4106
+ if (TEST_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return true;
4107
+ const scoped = stripAboveSourceRoot(forwardSlashed);
4108
+ return TEST_FILE_DIRECTORY_PATTERN.test(scoped);
3884
4109
  };
3885
4110
  const testFileResultCache = /* @__PURE__ */ new Map();
3886
4111
  const clearAutoSuppressionCaches = () => {
@@ -3888,7 +4113,9 @@ const clearAutoSuppressionCaches = () => {
3888
4113
  };
3889
4114
  const shouldAutoSuppress = (diagnostic) => {
3890
4115
  const filePath = diagnostic.filePath;
3891
- if ((diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule] : null)?.tags?.includes("test-noise")) {
4116
+ const rule = diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule] : null;
4117
+ if (rule?.tags?.includes("test-noise")) {
4118
+ if (rule.tags.includes("migration-hint")) return false;
3892
4119
  let isTest = testFileResultCache.get(filePath);
3893
4120
  if (isTest === void 0) {
3894
4121
  isTest = isTestFilePath(filePath);
@@ -3905,9 +4132,13 @@ const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, rea
3905
4132
  return filterInlineSuppressions(filtered, directory, readFileLinesSync);
3906
4133
  };
3907
4134
  const combineDiagnostics = (input) => {
3908
- const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables } = input;
3909
- const extraDiagnostics = isDiffMode || !includeEnvironmentChecks ? [] : checkReducedMotion(directory);
3910
- return mergeAndFilterDiagnostics([...lintDiagnostics, ...extraDiagnostics], directory, userConfig, readFileLinesSync, { respectInlineDisables });
4135
+ const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables, extraDiagnostics = [] } = input;
4136
+ const environmentDiagnostics = isDiffMode || !includeEnvironmentChecks ? [] : checkReducedMotion(directory);
4137
+ return mergeAndFilterDiagnostics([
4138
+ ...lintDiagnostics,
4139
+ ...environmentDiagnostics,
4140
+ ...extraDiagnostics
4141
+ ], directory, userConfig, readFileLinesSync, { respectInlineDisables });
3911
4142
  };
3912
4143
  const findFirstLintConfigInDirectory = (directory) => {
3913
4144
  for (const filename of ADOPTABLE_LINT_CONFIG_FILENAMES) {
@@ -4030,11 +4261,11 @@ const getUncommittedChangedFiles = (directory) => {
4030
4261
  const getDiffInfo = (directory, explicitBaseBranch) => {
4031
4262
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
4032
4263
  const currentBranch = getCurrentBranch(directory);
4033
- if (!currentBranch) return null;
4264
+ if (!currentBranch && !explicitBaseBranch) return null;
4034
4265
  const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
4035
4266
  if (!baseBranch) return null;
4036
4267
  if (explicitBaseBranch && !branchExists(directory, explicitBaseBranch)) throw new Error(`Diff base branch "${explicitBaseBranch}" does not exist (run \`git fetch\` to update remote refs).`);
4037
- if (currentBranch === baseBranch) {
4268
+ if (currentBranch !== null && currentBranch === baseBranch) {
4038
4269
  const uncommittedFiles = getUncommittedChangedFiles(directory);
4039
4270
  if (uncommittedFiles.length === 0) return null;
4040
4271
  return {
@@ -4061,6 +4292,7 @@ const VALID_RULE_SEVERITIES = [
4061
4292
  ];
4062
4293
  const BOOLEAN_FIELD_NAMES = [
4063
4294
  "lint",
4295
+ "deadCode",
4064
4296
  "verbose",
4065
4297
  "customRulesOnly",
4066
4298
  "share",
@@ -4416,10 +4648,13 @@ const buildCapabilities = (project) => {
4416
4648
  if (project.hasTypeScript) capabilities.add("typescript");
4417
4649
  return capabilities;
4418
4650
  };
4419
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags) => {
4651
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
4420
4652
  if (requires) {
4421
4653
  for (const capability of requires) if (!capabilities.has(capability)) return false;
4422
4654
  }
4655
+ if (disabledBy) {
4656
+ for (const capability of disabledBy) if (capabilities.has(capability)) return false;
4657
+ }
4423
4658
  if (tags) {
4424
4659
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
4425
4660
  }
@@ -4452,23 +4687,6 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
4452
4687
  availableRuleNames: readPluginRuleNames(pluginSpecifier)
4453
4688
  };
4454
4689
  };
4455
- const YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE = "effect";
4456
- const resolveYouMightNotNeedEffectPlugin = (customRulesOnly) => {
4457
- if (customRulesOnly) return null;
4458
- let pluginSpecifier;
4459
- try {
4460
- pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-you-might-not-need-an-effect");
4461
- } catch {
4462
- return null;
4463
- }
4464
- return {
4465
- entry: {
4466
- name: YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE,
4467
- specifier: pluginSpecifier
4468
- },
4469
- availableRuleNames: readPluginRuleNames(pluginSpecifier)
4470
- };
4471
- };
4472
4690
  const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
4473
4691
  if (availableRuleNames.size === 0) return rules;
4474
4692
  const ruleKeyPrefix = `${pluginNamespace}/`;
@@ -4487,26 +4705,36 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
4487
4705
  if (!fs.existsSync(rootDirectory)) return rootDirectory;
4488
4706
  return fs.realpathSync(rootDirectory);
4489
4707
  };
4708
+ const applyRuleSeverityControls = (rules, severityControls) => {
4709
+ const enabledRules = {};
4710
+ for (const [ruleKey, defaultSeverity] of Object.entries(rules)) {
4711
+ const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? defaultSeverity;
4712
+ if (severity === "off") continue;
4713
+ enabledRules[ruleKey] = severity;
4714
+ }
4715
+ return enabledRules;
4716
+ };
4490
4717
  const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls }) => {
4491
4718
  const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
4492
- const reactCompilerRules = reactHooksJsPlugin ? filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames) : {};
4493
- const youMightNotNeedEffectPlugin = resolveYouMightNotNeedEffectPlugin(customRulesOnly);
4494
- const youMightNotNeedEffectRules = youMightNotNeedEffectPlugin ? filterRulesToAvailable(YOU_MIGHT_NOT_NEED_EFFECT_RULES, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE, youMightNotNeedEffectPlugin.availableRuleNames) : {};
4719
+ const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
4495
4720
  const jsPlugins = [];
4496
4721
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
4497
- if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
4498
4722
  const capabilities = buildCapabilities(project);
4499
4723
  const enabledReactDoctorRules = {};
4500
- for (const [ruleId, rule] of Object.entries(reactDoctorPlugin.rules)) {
4501
- const fullKey = `react-doctor/${ruleId}`;
4724
+ for (const registryEntry of REACT_DOCTOR_RULES) {
4725
+ const rule = reactDoctorPlugin.rules[registryEntry.id];
4726
+ if (!rule) continue;
4727
+ if (customRulesOnly && registryEntry.originallyExternal) continue;
4502
4728
  if (rule.framework !== "global" && !rule.requires) continue;
4503
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags)) continue;
4504
- const severity = resolveRuleSeverityOverride({
4505
- ruleKey: fullKey,
4729
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
4730
+ const explicitSeverity = resolveRuleSeverityOverride({
4731
+ ruleKey: registryEntry.key,
4506
4732
  category: rule.category
4507
- }, severityControls) ?? rule.severity;
4733
+ }, severityControls);
4734
+ if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
4735
+ const severity = explicitSeverity ?? rule.severity;
4508
4736
  if (severity === "off") continue;
4509
- enabledReactDoctorRules[fullKey] = severity;
4737
+ enabledReactDoctorRules[registryEntry.key] = severity;
4510
4738
  }
4511
4739
  return {
4512
4740
  ...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
@@ -4519,7 +4747,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
4519
4747
  style: "off",
4520
4748
  nursery: "off"
4521
4749
  },
4522
- plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
4750
+ plugins: [],
4523
4751
  jsPlugins: [...jsPlugins, pluginPath],
4524
4752
  settings: { "react-doctor": {
4525
4753
  framework: project.framework,
@@ -4527,10 +4755,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
4527
4755
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
4528
4756
  } },
4529
4757
  rules: {
4530
- ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
4531
- ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
4532
4758
  ...reactCompilerRules,
4533
- ...youMightNotNeedEffectRules,
4534
4759
  ...enabledReactDoctorRules
4535
4760
  }
4536
4761
  };
@@ -4551,43 +4776,43 @@ const REACT_USE_BINDING_RESOLUTION = {
4551
4776
  isReactNamespaceBinding: false
4552
4777
  };
4553
4778
  const getScriptKind = (filename) => {
4554
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
4555
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
4556
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
4557
- return ts.ScriptKind.JS;
4779
+ if (filename.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
4780
+ if (filename.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
4781
+ if (filename.endsWith(".ts")) return ts$1.ScriptKind.TS;
4782
+ return ts$1.ScriptKind.JS;
4558
4783
  };
4559
4784
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
4560
4785
  const unwrapExpression = (expression) => {
4561
4786
  let currentExpression = expression;
4562
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
4787
+ while (ts$1.isParenthesizedExpression(currentExpression) || ts$1.isAsExpression(currentExpression) || ts$1.isSatisfiesExpression(currentExpression) || ts$1.isNonNullExpression(currentExpression) || ts$1.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
4563
4788
  return currentExpression;
4564
4789
  };
4565
4790
  const getStaticPropertyName = (node) => {
4566
4791
  if (!node) return null;
4567
- if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
4568
- if (ts.isComputedPropertyName(node)) {
4792
+ if (ts$1.isIdentifier(node) || ts$1.isStringLiteral(node) || ts$1.isNumericLiteral(node)) return node.text;
4793
+ if (ts$1.isComputedPropertyName(node)) {
4569
4794
  const expression = unwrapExpression(node.expression);
4570
- if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
4795
+ if (ts$1.isStringLiteral(expression) || ts$1.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
4571
4796
  }
4572
4797
  return null;
4573
4798
  };
4574
4799
  const findBindingIdentifier = (bindingName, identifierName) => {
4575
- if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName ? bindingName : null;
4800
+ if (ts$1.isIdentifier(bindingName)) return bindingName.text === identifierName ? bindingName : null;
4576
4801
  for (const element of bindingName.elements) {
4577
- if (ts.isOmittedExpression(element)) continue;
4802
+ if (ts$1.isOmittedExpression(element)) continue;
4578
4803
  const nestedIdentifier = findBindingIdentifier(element.name, identifierName);
4579
4804
  if (nestedIdentifier) return nestedIdentifier;
4580
4805
  }
4581
4806
  return null;
4582
4807
  };
4583
4808
  const bindingNameHasIdentifier = (bindingName, identifierName) => {
4584
- if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName;
4809
+ if (ts$1.isIdentifier(bindingName)) return bindingName.text === identifierName;
4585
4810
  return bindingName.elements.some((element) => {
4586
- if (ts.isOmittedExpression(element)) return false;
4811
+ if (ts$1.isOmittedExpression(element)) return false;
4587
4812
  return bindingNameHasIdentifier(element.name, identifierName);
4588
4813
  });
4589
4814
  };
4590
- const getDirectBindingIdentifier = (bindingName) => ts.isIdentifier(bindingName) ? bindingName : null;
4815
+ const getDirectBindingIdentifier = (bindingName) => ts$1.isIdentifier(bindingName) ? bindingName : null;
4591
4816
  const isReactUseObjectBindingElement = (bindingElement) => {
4592
4817
  const bindingIdentifier = getDirectBindingIdentifier(bindingElement.name);
4593
4818
  if (!bindingIdentifier) return false;
@@ -4596,12 +4821,12 @@ const isReactUseObjectBindingElement = (bindingElement) => {
4596
4821
  };
4597
4822
  const isReactRequireCall = (expression) => {
4598
4823
  const unwrappedExpression = unwrapExpression(expression);
4599
- return ts.isCallExpression(unwrappedExpression) && ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === REQUIRE_IDENTIFIER && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && unwrappedExpression.arguments[0].text === REACT_MODULE_SOURCE;
4824
+ return ts$1.isCallExpression(unwrappedExpression) && ts$1.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === REQUIRE_IDENTIFIER && unwrappedExpression.arguments.length === 1 && ts$1.isStringLiteral(unwrappedExpression.arguments[0]) && unwrappedExpression.arguments[0].text === REACT_MODULE_SOURCE;
4600
4825
  };
4601
4826
  const getModuleSource = (node) => {
4602
4827
  let currentNode = node;
4603
4828
  while (currentNode) {
4604
- if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
4829
+ if (ts$1.isImportDeclaration(currentNode) && ts$1.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
4605
4830
  currentNode = currentNode.parent;
4606
4831
  }
4607
4832
  return null;
@@ -4618,40 +4843,40 @@ const isReactObjectBindingName = (bindingPattern, identifierName) => bindingPatt
4618
4843
  return isReactUseObjectBindingElement(bindingElement);
4619
4844
  });
4620
4845
  const isReactRequireBindingDeclaration = (node, identifierName) => {
4621
- if (!ts.isVariableDeclaration(node)) return false;
4846
+ if (!ts$1.isVariableDeclaration(node)) return false;
4622
4847
  if (!node.initializer) return false;
4623
4848
  if (!isReactRequireCall(node.initializer)) return false;
4624
- if (ts.isIdentifier(node.name)) return node.name.text === identifierName;
4625
- return ts.isObjectBindingPattern(node.name) && isReactObjectBindingName(node.name, identifierName);
4849
+ if (ts$1.isIdentifier(node.name)) return node.name.text === identifierName;
4850
+ return ts$1.isObjectBindingPattern(node.name) && isReactObjectBindingName(node.name, identifierName);
4626
4851
  };
4627
4852
  const collectReactImportBindings = (sourceFile) => {
4628
4853
  const namespaceNames = /* @__PURE__ */ new Set();
4629
4854
  const useImportNames = /* @__PURE__ */ new Set();
4630
4855
  for (const statement of sourceFile.statements) {
4631
- if (ts.isImportDeclaration(statement)) {
4632
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
4856
+ if (ts$1.isImportDeclaration(statement)) {
4857
+ if (!ts$1.isStringLiteral(statement.moduleSpecifier)) continue;
4633
4858
  if (statement.moduleSpecifier.text !== REACT_MODULE_SOURCE) continue;
4634
4859
  const importClause = statement.importClause;
4635
4860
  if (!importClause) continue;
4636
4861
  if (importClause.name) namespaceNames.add(importClause.name.text);
4637
4862
  const namedBindings = importClause.namedBindings;
4638
4863
  if (!namedBindings) continue;
4639
- if (ts.isNamespaceImport(namedBindings)) {
4864
+ if (ts$1.isNamespaceImport(namedBindings)) {
4640
4865
  namespaceNames.add(namedBindings.name.text);
4641
4866
  continue;
4642
4867
  }
4643
4868
  for (const importSpecifier of namedBindings.elements) if (getImportedName(importSpecifier) === USE_IDENTIFIER) useImportNames.add(importSpecifier.name.text);
4644
4869
  continue;
4645
4870
  }
4646
- if (!ts.isVariableStatement(statement)) continue;
4871
+ if (!ts$1.isVariableStatement(statement)) continue;
4647
4872
  for (const declaration of statement.declarationList.declarations) {
4648
4873
  if (!declaration.initializer) continue;
4649
4874
  if (!isReactRequireCall(declaration.initializer)) continue;
4650
- if (ts.isIdentifier(declaration.name)) {
4875
+ if (ts$1.isIdentifier(declaration.name)) {
4651
4876
  namespaceNames.add(declaration.name.text);
4652
4877
  continue;
4653
4878
  }
4654
- if (ts.isObjectBindingPattern(declaration.name)) collectReactObjectBindingNames(declaration.name, useImportNames);
4879
+ if (ts$1.isObjectBindingPattern(declaration.name)) collectReactObjectBindingNames(declaration.name, useImportNames);
4655
4880
  }
4656
4881
  }
4657
4882
  return {
@@ -4662,24 +4887,24 @@ const collectReactImportBindings = (sourceFile) => {
4662
4887
  const findBindingElement = (identifier) => {
4663
4888
  let currentNode = identifier.parent;
4664
4889
  while (currentNode) {
4665
- if (ts.isBindingElement(currentNode)) return currentNode;
4666
- if (ts.isVariableDeclaration(currentNode) || ts.isParameter(currentNode)) return null;
4890
+ if (ts$1.isBindingElement(currentNode)) return currentNode;
4891
+ if (ts$1.isVariableDeclaration(currentNode) || ts$1.isParameter(currentNode)) return null;
4667
4892
  currentNode = currentNode.parent;
4668
4893
  }
4669
4894
  return null;
4670
4895
  };
4671
4896
  const declarationBindsIdentifier = (node, identifierName) => {
4672
- if (ts.isVariableDeclaration(node) || ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName);
4673
- if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) return node.name?.text === identifierName;
4897
+ if (ts$1.isVariableDeclaration(node) || ts$1.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName);
4898
+ if (ts$1.isFunctionDeclaration(node) || ts$1.isClassDeclaration(node)) return node.name?.text === identifierName;
4674
4899
  return false;
4675
4900
  };
4676
- const isScopeBoundary = (node) => ts.isFunctionLike(node) || ts.isClassLike(node) || ts.isBlock(node) || ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isCatchClause(node) || ts.isSourceFile(node) || ts.isModuleBlock(node);
4901
+ const isScopeBoundary = (node) => ts$1.isFunctionLike(node) || ts$1.isClassLike(node) || ts$1.isBlock(node) || ts$1.isForStatement(node) || ts$1.isForInStatement(node) || ts$1.isForOfStatement(node) || ts$1.isCatchClause(node) || ts$1.isSourceFile(node) || ts$1.isModuleBlock(node);
4677
4902
  const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
4678
4903
  if (isReactRequireBindingDeclaration(node, identifierName)) return false;
4679
4904
  if (declarationBindsIdentifier(node, identifierName)) return true;
4680
4905
  if (node !== scopeNode && isScopeBoundary(node)) return false;
4681
4906
  let didFindBinding = false;
4682
- ts.forEachChild(node, (child) => {
4907
+ ts$1.forEachChild(node, (child) => {
4683
4908
  if (didFindBinding) return;
4684
4909
  didFindBinding = scopeContainsNonImportBinding(child, scopeNode, identifierName);
4685
4910
  });
@@ -4699,26 +4924,26 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
4699
4924
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
4700
4925
  const unwrappedExpression = unwrapExpression(expression);
4701
4926
  if (isReactRequireCall(unwrappedExpression)) return true;
4702
- if (!ts.isIdentifier(unwrappedExpression)) return false;
4927
+ if (!ts$1.isIdentifier(unwrappedExpression)) return false;
4703
4928
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
4704
4929
  return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactNamespaceBinding ?? false;
4705
4930
  };
4706
4931
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
4707
4932
  if (!expression) return false;
4708
4933
  const unwrappedExpression = unwrapExpression(expression);
4709
- if (ts.isIdentifier(unwrappedExpression)) {
4934
+ if (ts$1.isIdentifier(unwrappedExpression)) {
4710
4935
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
4711
4936
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
4712
4937
  return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactUseBinding ?? false;
4713
4938
  }
4714
- if (ts.isPropertyAccessExpression(unwrappedExpression) && unwrappedExpression.name.text === USE_IDENTIFIER && isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations)) return true;
4715
- if (ts.isElementAccessExpression(unwrappedExpression) && ts.isStringLiteral(unwrappedExpression.argumentExpression) && unwrappedExpression.argumentExpression.text === USE_IDENTIFIER) return isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations);
4939
+ if (ts$1.isPropertyAccessExpression(unwrappedExpression) && unwrappedExpression.name.text === USE_IDENTIFIER && isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations)) return true;
4940
+ if (ts$1.isElementAccessExpression(unwrappedExpression) && ts$1.isStringLiteral(unwrappedExpression.argumentExpression) && unwrappedExpression.argumentExpression.text === USE_IDENTIFIER) return isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations);
4716
4941
  return false;
4717
4942
  };
4718
4943
  const isReactUseObjectBinding = (identifier, variableDeclaration, reactImportBindings, sourceFile, visitedDeclarations) => {
4719
4944
  const bindingElement = findBindingElement(identifier);
4720
4945
  if (!bindingElement) return false;
4721
- if (!ts.isObjectBindingPattern(bindingElement.parent)) return false;
4946
+ if (!ts$1.isObjectBindingPattern(bindingElement.parent)) return false;
4722
4947
  if (!variableDeclaration.initializer) return false;
4723
4948
  if (!isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, visitedDeclarations)) return false;
4724
4949
  return isReactUseObjectBindingElement(bindingElement);
@@ -4730,23 +4955,23 @@ const getVariableDeclarationResolution = (variableDeclaration, identifierName, r
4730
4955
  const nestedVisitedDeclarations = new Set(visitedDeclarations);
4731
4956
  nestedVisitedDeclarations.add(variableDeclaration);
4732
4957
  return {
4733
- isReactNamespaceBinding: ts.isIdentifier(variableDeclaration.name) && variableDeclaration.initializer !== void 0 && isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)),
4958
+ isReactNamespaceBinding: ts$1.isIdentifier(variableDeclaration.name) && variableDeclaration.initializer !== void 0 && isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)),
4734
4959
  isReactUseBinding: isReactUseExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)) || isReactUseObjectBinding(bindingIdentifier, variableDeclaration, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations))
4735
4960
  };
4736
4961
  };
4737
4962
  const getImportResolution = (node, identifierName) => {
4738
- if (ts.isImportSpecifier(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE && getImportedName(node) === USE_IDENTIFIER ? REACT_USE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4739
- if (ts.isNamespaceImport(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4740
- if (ts.isImportClause(node) && node.name?.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4963
+ if (ts$1.isImportSpecifier(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE && getImportedName(node) === USE_IDENTIFIER ? REACT_USE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4964
+ if (ts$1.isNamespaceImport(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4965
+ if (ts$1.isImportClause(node) && node.name?.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4741
4966
  return null;
4742
4967
  };
4743
4968
  const getDeclarationResolution = (node, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
4744
4969
  const importResolution = getImportResolution(node, identifierName);
4745
4970
  if (importResolution) return importResolution;
4746
- if (ts.isVariableDeclaration(node)) return getVariableDeclarationResolution(node, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4747
- if (ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName) ? LOCAL_BINDING_RESOLUTION : null;
4748
- if (ts.isFunctionDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
4749
- if (ts.isClassDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
4971
+ if (ts$1.isVariableDeclaration(node)) return getVariableDeclarationResolution(node, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4972
+ if (ts$1.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName) ? LOCAL_BINDING_RESOLUTION : null;
4973
+ if (ts$1.isFunctionDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
4974
+ if (ts$1.isClassDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
4750
4975
  return null;
4751
4976
  };
4752
4977
  const isNestedScopeBoundary = (node, scopeNode) => node !== scopeNode && isScopeBoundary(node);
@@ -4755,14 +4980,14 @@ const findResolutionInSubtree = (node, scopeNode, identifierName, reactImportBin
4755
4980
  if (declarationResolution) return declarationResolution;
4756
4981
  if (isNestedScopeBoundary(node, scopeNode)) return null;
4757
4982
  let resolution = null;
4758
- ts.forEachChild(node, (child) => {
4983
+ ts$1.forEachChild(node, (child) => {
4759
4984
  if (resolution) return;
4760
4985
  resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4761
4986
  });
4762
4987
  return resolution;
4763
4988
  };
4764
4989
  const findResolutionInFunctionParameters = (node, identifierName, reactImportBindings) => {
4765
- if (!ts.isFunctionLike(node)) return null;
4990
+ if (!ts$1.isFunctionLike(node)) return null;
4766
4991
  for (const parameter of node.parameters) {
4767
4992
  const parameterResolution = getDeclarationResolution(parameter, identifierName, reactImportBindings, parameter.getSourceFile(), /* @__PURE__ */ new Set());
4768
4993
  if (parameterResolution) return parameterResolution;
@@ -4773,7 +4998,7 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
4773
4998
  const parameterResolution = findResolutionInFunctionParameters(scopeNode, identifierName, reactImportBindings);
4774
4999
  if (parameterResolution) return parameterResolution;
4775
5000
  let resolution = null;
4776
- ts.forEachChild(scopeNode, (child) => {
5001
+ ts$1.forEachChild(scopeNode, (child) => {
4777
5002
  if (resolution) return;
4778
5003
  resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4779
5004
  });
@@ -4791,22 +5016,22 @@ const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, v
4791
5016
  }
4792
5017
  return null;
4793
5018
  };
4794
- const isUseCallIdentifier = (node) => node.text === USE_IDENTIFIER && ts.isCallExpression(node.parent) && node.parent.expression === node;
5019
+ const isUseCallIdentifier = (node) => node.text === USE_IDENTIFIER && ts$1.isCallExpression(node.parent) && node.parent.expression === node;
4795
5020
  const findUseCallIdentifier = (sourceFile, useOffset) => {
4796
5021
  let matchedIdentifier = null;
4797
5022
  const visit = (node) => {
4798
5023
  if (matchedIdentifier) return;
4799
- if (ts.isIdentifier(node) && isUseCallIdentifier(node) && node.getStart(sourceFile) === useOffset) {
5024
+ if (ts$1.isIdentifier(node) && isUseCallIdentifier(node) && node.getStart(sourceFile) === useOffset) {
4800
5025
  matchedIdentifier = node;
4801
5026
  return;
4802
5027
  }
4803
- ts.forEachChild(node, visit);
5028
+ ts$1.forEachChild(node, visit);
4804
5029
  };
4805
5030
  visit(sourceFile);
4806
5031
  return matchedIdentifier;
4807
5032
  };
4808
5033
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
4809
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filename));
5034
+ const sourceFile = ts$1.createSourceFile(filename, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind(filename));
4810
5035
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
4811
5036
  if (!useIdentifier) return null;
4812
5037
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -4908,7 +5133,13 @@ const SANITIZED_ENV = (() => {
4908
5133
  }
4909
5134
  return sanitized;
4910
5135
  })();
4911
- const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
5136
+ const OXLINT_SPAWN_TIMEOUT_MS = (() => {
5137
+ const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
5138
+ if (raw === void 0) return 6e4;
5139
+ const parsed = Number(raw);
5140
+ if (!Number.isFinite(parsed) || parsed <= 0) return 6e4;
5141
+ return parsed;
5142
+ })();
4912
5143
  const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
4913
5144
  const child = spawn(nodeBinaryPath, args, {
4914
5145
  cwd: rootDirectory,
@@ -5088,6 +5319,7 @@ const runOxlint = async (options) => {
5088
5319
  const spawnLintBatches = async () => {
5089
5320
  const allDiagnostics = [];
5090
5321
  const droppedFiles = [];
5322
+ let firstDropReason = null;
5091
5323
  const spawnLintBatch = async (batch) => {
5092
5324
  const batchArgs = [...baseArgs, ...batch];
5093
5325
  try {
@@ -5096,6 +5328,7 @@ const runOxlint = async (options) => {
5096
5328
  if (!isSplittableOxlintBatchError(error)) throw error;
5097
5329
  if (batch.length <= 1) {
5098
5330
  droppedFiles.push(...batch);
5331
+ if (firstDropReason === null && error instanceof Error) firstDropReason = error.message;
5099
5332
  return [];
5100
5333
  }
5101
5334
  const splitIndex = Math.ceil(batch.length / 2);
@@ -5107,7 +5340,8 @@ const runOxlint = async (options) => {
5107
5340
  const previewCount = 3;
5108
5341
  const previewFiles = droppedFiles.slice(0, previewCount).join(", ");
5109
5342
  const remainderHint = droppedFiles.length > previewCount ? `, +${droppedFiles.length - previewCount} more` : "";
5110
- onPartialFailure(`${droppedFiles.length} file(s) exceeded the ${OXLINT_SPAWN_TIMEOUT_MS / 1e3}s per-batch oxlint budget and were skipped (${previewFiles}${remainderHint})`);
5343
+ const reasonHint = firstDropReason ? ` first failure: ${firstDropReason}` : "";
5344
+ onPartialFailure(`${droppedFiles.length} file(s) failed to lint and were skipped (${previewFiles}${remainderHint})${reasonHint}`);
5111
5345
  }
5112
5346
  return dedupeDiagnostics(allDiagnostics);
5113
5347
  };
@@ -5154,7 +5388,8 @@ const toJsonReport = (result, options) => buildJsonReport({
5154
5388
  result: {
5155
5389
  diagnostics: result.diagnostics,
5156
5390
  score: result.score,
5157
- skippedChecks: [],
5391
+ skippedChecks: result.skippedChecks,
5392
+ ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
5158
5393
  project: result.project,
5159
5394
  elapsedMilliseconds: result.elapsedMilliseconds
5160
5395
  }
@@ -5177,32 +5412,54 @@ const diagnose = async (directory, options = {}) => {
5177
5412
  const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
5178
5413
  const readFileLinesSync = createNodeReadFileLinesSync(resolvedDirectory);
5179
5414
  const effectiveLint = options.lint ?? userConfig?.lint ?? true;
5415
+ const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
5180
5416
  const effectiveRespectInlineDisables = options.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true;
5181
5417
  const ignoredTags = new Set(userConfig?.ignore?.tags ?? []);
5418
+ const lintPromise = effectiveLint ? runOxlint({
5419
+ rootDirectory: resolvedDirectory,
5420
+ project: projectInfo,
5421
+ includePaths: lintIncludePaths,
5422
+ customRulesOnly: userConfig?.customRulesOnly ?? false,
5423
+ respectInlineDisables: effectiveRespectInlineDisables,
5424
+ adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
5425
+ ignoredTags,
5426
+ userConfig
5427
+ }).catch((error) => {
5428
+ console.error("Lint failed:", error);
5429
+ return EMPTY_DIAGNOSTICS;
5430
+ }) : Promise.resolve(EMPTY_DIAGNOSTICS);
5431
+ const shouldRunDeadCode = effectiveDeadCode && !isDiffMode;
5432
+ let deadCodeFailureReason = null;
5433
+ const deadCodePromise = shouldRunDeadCode ? checkDeadCode({
5434
+ rootDirectory: resolvedDirectory,
5435
+ userConfig
5436
+ }).catch((error) => {
5437
+ deadCodeFailureReason = error instanceof Error ? error.message : String(error);
5438
+ return EMPTY_DIAGNOSTICS;
5439
+ }) : Promise.resolve(EMPTY_DIAGNOSTICS);
5440
+ const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
5182
5441
  const diagnostics = combineDiagnostics({
5183
- lintDiagnostics: effectiveLint ? await runOxlint({
5184
- rootDirectory: resolvedDirectory,
5185
- project: projectInfo,
5186
- includePaths: lintIncludePaths,
5187
- customRulesOnly: userConfig?.customRulesOnly ?? false,
5188
- respectInlineDisables: effectiveRespectInlineDisables,
5189
- adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
5190
- ignoredTags,
5191
- userConfig
5192
- }).catch((error) => {
5193
- console.error("Lint failed:", error);
5194
- return EMPTY_DIAGNOSTICS;
5195
- }) : EMPTY_DIAGNOSTICS,
5442
+ lintDiagnostics,
5196
5443
  directory: resolvedDirectory,
5197
5444
  isDiffMode,
5198
5445
  userConfig,
5199
5446
  readFileLinesSync,
5200
- respectInlineDisables: effectiveRespectInlineDisables
5447
+ respectInlineDisables: effectiveRespectInlineDisables,
5448
+ extraDiagnostics: deadCodeDiagnostics
5201
5449
  });
5202
5450
  const elapsedMilliseconds = globalThis.performance.now() - startTime;
5451
+ const score = await calculateScore(diagnostics);
5452
+ const skippedChecks = [];
5453
+ const skippedCheckReasons = {};
5454
+ if (deadCodeFailureReason !== null) {
5455
+ skippedChecks.push("dead-code");
5456
+ skippedCheckReasons["dead-code"] = deadCodeFailureReason;
5457
+ }
5203
5458
  return {
5204
5459
  diagnostics,
5205
- score: await calculateScore(diagnostics),
5460
+ score,
5461
+ skippedChecks,
5462
+ ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
5206
5463
  project: projectInfo,
5207
5464
  elapsedMilliseconds
5208
5465
  };