react-doctor 0.2.0 → 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",
@@ -113,12 +92,31 @@ const IGNORED_DIRECTORIES = new Set([
113
92
  "out",
114
93
  "storybook-static"
115
94
  ]);
95
+ const IGNORABLE_READDIR_ERROR_CODES = new Set([
96
+ "EACCES",
97
+ "EPERM",
98
+ "ENOENT",
99
+ "ENOTDIR"
100
+ ]);
101
+ const isIgnorableReaddirError = (error) => {
102
+ if (typeof error !== "object" || error === null) return false;
103
+ const errorCode = error.code;
104
+ return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
105
+ };
106
+ const readDirectoryEntries = (directoryPath) => {
107
+ try {
108
+ return fs.readdirSync(directoryPath, { withFileTypes: true });
109
+ } catch (error) {
110
+ if (isIgnorableReaddirError(error)) return [];
111
+ throw error;
112
+ }
113
+ };
116
114
  const countSourceFilesViaFilesystem = (rootDirectory) => {
117
115
  let count = 0;
118
116
  const stack = [rootDirectory];
119
117
  while (stack.length > 0) {
120
118
  const currentDirectory = stack.pop();
121
- const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
119
+ const entries = readDirectoryEntries(currentDirectory);
122
120
  for (const entry of entries) {
123
121
  if (entry.isDirectory()) {
124
122
  if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
@@ -156,7 +154,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
156
154
  if (error instanceof SyntaxError) return {};
157
155
  if (error instanceof Error && "code" in error) {
158
156
  const { code } = error;
159
- if (code === "EISDIR" || code === "EACCES") return {};
157
+ if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
160
158
  }
161
159
  throw error;
162
160
  }
@@ -527,6 +525,13 @@ const getDependencyDeclaration = ({ packageJson, packageName, sections }) => {
527
525
  version: null
528
526
  };
529
527
  };
528
+ const isDirectory = (directoryPath) => {
529
+ try {
530
+ return fs.statSync(directoryPath).isDirectory();
531
+ } catch {
532
+ return false;
533
+ }
534
+ };
530
535
  const NX_PROJECT_DISCOVERY_DIRS = [
531
536
  "apps",
532
537
  "libs",
@@ -537,8 +542,8 @@ const getNxWorkspaceDirectories = (rootDirectory) => {
537
542
  const collected = [];
538
543
  for (const candidate of NX_PROJECT_DISCOVERY_DIRS) {
539
544
  const candidatePath = path.join(rootDirectory, candidate);
540
- if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isDirectory()) continue;
541
- for (const entry of fs.readdirSync(candidatePath, { withFileTypes: true })) {
545
+ if (!isDirectory(candidatePath)) continue;
546
+ for (const entry of readDirectoryEntries(candidatePath)) {
542
547
  if (!entry.isDirectory()) continue;
543
548
  const projectDirectory = path.join(candidatePath, entry.name);
544
549
  if (isFile(path.join(projectDirectory, "project.json")) || isFile(path.join(projectDirectory, "package.json"))) collected.push(`${candidate}/${entry.name}`);
@@ -580,17 +585,17 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
580
585
  const cleanPattern = pattern.replace(/["']/g, "").replace(/\/\*\*$/, "/*");
581
586
  if (!cleanPattern.includes("*")) {
582
587
  const directoryPath = path.join(rootDirectory, cleanPattern);
583
- if (fs.existsSync(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
588
+ if (isDirectory(directoryPath) && isFile(path.join(directoryPath, "package.json"))) return [directoryPath];
584
589
  return [];
585
590
  }
586
591
  const wildcardIndex = cleanPattern.indexOf("*");
587
592
  const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
588
593
  const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);
589
- if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
594
+ if (!isDirectory(baseDirectory)) return [];
590
595
  const resolved = [];
591
- for (const entry of fs.readdirSync(baseDirectory)) {
592
- const entryPath = path.join(baseDirectory, entry, suffixAfterWildcard);
593
- if (fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory() && isFile(path.join(entryPath, "package.json"))) resolved.push(entryPath);
596
+ for (const entry of readDirectoryEntries(baseDirectory)) {
597
+ const entryPath = path.join(baseDirectory, entry.name, suffixAfterWildcard);
598
+ if (isDirectory(entryPath) && isFile(path.join(entryPath, "package.json"))) resolved.push(entryPath);
594
599
  }
595
600
  return resolved;
596
601
  };
@@ -654,19 +659,6 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
654
659
  }
655
660
  return result;
656
661
  };
657
- const REACT_DEPENDENCY_NAMES = new Set([
658
- "react",
659
- "react-native",
660
- "next"
661
- ]);
662
- const hasReactDependency = (packageJson) => {
663
- const allDependencies = {
664
- ...packageJson.peerDependencies,
665
- ...packageJson.dependencies,
666
- ...packageJson.devDependencies
667
- };
668
- return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
669
- };
670
662
  const findDependencyInfoFromMonorepoRoot = (directory) => {
671
663
  const monorepoRoot = findMonorepoRoot(directory);
672
664
  if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
@@ -694,13 +686,13 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
694
686
  "peerDependencies"
695
687
  ]
696
688
  }) : null;
697
- const shouldUseReactFallback = leafPackageJson ? hasReactDependency(leafPackageJson) : true;
689
+ const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
698
690
  const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
699
691
  const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
700
692
  const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
701
693
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
702
694
  return {
703
- reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : null,
695
+ reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
704
696
  tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
705
697
  framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
706
698
  };
@@ -767,6 +759,19 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
767
759
  if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
768
760
  return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
769
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
+ };
770
775
  const listWorkspacePackages = (rootDirectory) => {
771
776
  const packageJsonPath = path.join(rootDirectory, "package.json");
772
777
  if (!isFile(packageJsonPath)) return [];
@@ -835,7 +840,7 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
835
840
  });
836
841
  }
837
842
  }
838
- const entries = fs.readdirSync(currentDirectory, { withFileTypes: true }).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
843
+ const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
839
844
  for (const entry of entries) {
840
845
  if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
841
846
  pendingDirectories.push(path.join(currentDirectory, entry.name));
@@ -844,7 +849,7 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
844
849
  return packages;
845
850
  };
846
851
  const discoverReactSubprojects = (rootDirectory) => {
847
- if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
852
+ if (!isDirectory(rootDirectory)) return [];
848
853
  const manifestPackages = listManifestWorkspacePackages(rootDirectory);
849
854
  if (manifestPackages.length > 0) return manifestPackages;
850
855
  return discoverReactSubprojectsByFilesystem(rootDirectory);
@@ -2986,6 +2991,136 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
2986
2991
  category: diagnostic.category,
2987
2992
  tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
2988
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
+ };
2989
3124
  /**
2990
3125
  * Resolves the user-configured severity override for a rule.
2991
3126
  * Per-rule overrides win over per-category overrides. Returns
@@ -2994,7 +3129,14 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
2994
3129
  */
2995
3130
  const resolveRuleSeverityOverride = (input, controls) => {
2996
3131
  if (!controls) return void 0;
2997
- 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;
2998
3140
  };
2999
3141
  const SEVERITY_FOR_OVERRIDE = {
3000
3142
  error: "error",
@@ -3203,14 +3345,19 @@ const describeFailure = (error) => {
3203
3345
  if (error instanceof Error && error.message) return error.message;
3204
3346
  return String(error);
3205
3347
  };
3206
- const calculateScore = async (diagnostics) => {
3348
+ const calculateScore = async (diagnostics, options = {}) => {
3207
3349
  const controller = new AbortController();
3208
3350
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3351
+ const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
3209
3352
  try {
3210
- const response = await fetch(SCORE_API_URL, {
3353
+ const compressedBody = gzipSync(JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }));
3354
+ const response = await fetch(requestUrl, {
3211
3355
  method: "POST",
3212
- headers: { "Content-Type": "application/json" },
3213
- body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
3356
+ headers: {
3357
+ "Content-Type": "application/json",
3358
+ "Content-Encoding": "gzip"
3359
+ },
3360
+ body: compressedBody,
3214
3361
  signal: controller.signal
3215
3362
  });
3216
3363
  if (!response.ok) {
@@ -3297,60 +3444,6 @@ const canOxlintExtendConfig = (configPath) => {
3297
3444
  if (extendsEntries.length === 0) return true;
3298
3445
  return extendsEntries.some((entry) => typeof entry === "string" && isLocalPathExtend(entry));
3299
3446
  };
3300
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
3301
- const REDUCED_MOTION_FILE_GLOBS = [
3302
- "*.ts",
3303
- "*.tsx",
3304
- "*.js",
3305
- "*.jsx",
3306
- "*.css",
3307
- "*.scss"
3308
- ];
3309
- const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
3310
- filePath: "package.json",
3311
- plugin: "react-doctor",
3312
- rule: "require-reduced-motion",
3313
- severity: "error",
3314
- message: "Project uses a motion library but has no prefers-reduced-motion handling — required for accessibility (WCAG 2.3.3)",
3315
- help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
3316
- line: 0,
3317
- column: 0,
3318
- category: "Accessibility"
3319
- };
3320
- const checkReducedMotion = (rootDirectory) => {
3321
- const packageJsonPath = path.join(rootDirectory, "package.json");
3322
- if (!isFile(packageJsonPath)) return [];
3323
- let hasMotionLibrary = false;
3324
- try {
3325
- const packageJson = readPackageJson(packageJsonPath);
3326
- const allDependencies = {
3327
- ...packageJson.dependencies,
3328
- ...packageJson.devDependencies
3329
- };
3330
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
3331
- } catch {
3332
- return [];
3333
- }
3334
- if (!hasMotionLibrary) return [];
3335
- const result = spawnSync("git", [
3336
- "grep",
3337
- "-ql",
3338
- "-E",
3339
- REDUCED_MOTION_GREP_PATTERN,
3340
- "--",
3341
- ...REDUCED_MOTION_FILE_GLOBS
3342
- ], {
3343
- cwd: rootDirectory,
3344
- stdio: [
3345
- "ignore",
3346
- "pipe",
3347
- "pipe"
3348
- ]
3349
- });
3350
- if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3351
- if (result.status === 0) return [];
3352
- return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
3353
- };
3354
3447
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
3355
3448
  const FALSY_VALUES = new Set([
3356
3449
  "false",
@@ -3530,6 +3623,148 @@ const collectIgnorePatterns = (rootDirectory) => {
3530
3623
  cachedPatternsByRoot.set(rootDirectory, patterns);
3531
3624
  return patterns;
3532
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
+ };
3533
3768
  const createNodeReadFileLinesSync = (rootDirectory) => {
3534
3769
  return (filePath) => {
3535
3770
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
@@ -3659,7 +3894,7 @@ const isRuleListedInComment = (ruleList, ruleId) => {
3659
3894
  if (!trimmed) return true;
3660
3895
  const ruleSection = stripDescriptionTail(trimmed).trim();
3661
3896
  if (!ruleSection) return true;
3662
- return ruleSection.split(/[,\s]+/).some((token) => token.trim() === ruleId);
3897
+ return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
3663
3898
  };
3664
3899
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
3665
3900
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -3811,6 +4046,10 @@ const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrap
3811
4046
  }
3812
4047
  return false;
3813
4048
  };
4049
+ const isIgnoredRule = (ignoredRules, ruleIdentifier) => {
4050
+ for (const ignoredRule of ignoredRules) if (isSameRuleKey(ignoredRule, ruleIdentifier)) return true;
4051
+ return false;
4052
+ };
3814
4053
  const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
3815
4054
  const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
3816
4055
  const ignoredFilePatterns = compileIgnoredFilePatterns(config);
@@ -3821,8 +4060,7 @@ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLi
3821
4060
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
3822
4061
  const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
3823
4062
  return diagnostics.filter((diagnostic) => {
3824
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
3825
- if (ignoredRules.has(ruleIdentifier)) return false;
4063
+ if (isIgnoredRule(ignoredRules, `${diagnostic.plugin}/${diagnostic.rule}`)) return false;
3826
4064
  if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
3827
4065
  if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
3828
4066
  if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
@@ -3850,11 +4088,24 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
3850
4088
  }] : [diagnostic];
3851
4089
  });
3852
4090
  };
3853
- 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
+ };
3854
4103
  const isTestFilePath = (relativePath) => {
3855
4104
  if (relativePath.length === 0) return false;
3856
4105
  const forwardSlashed = relativePath.replaceAll("\\", "/");
3857
- 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);
3858
4109
  };
3859
4110
  const testFileResultCache = /* @__PURE__ */ new Map();
3860
4111
  const clearAutoSuppressionCaches = () => {
@@ -3862,7 +4113,9 @@ const clearAutoSuppressionCaches = () => {
3862
4113
  };
3863
4114
  const shouldAutoSuppress = (diagnostic) => {
3864
4115
  const filePath = diagnostic.filePath;
3865
- 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;
3866
4119
  let isTest = testFileResultCache.get(filePath);
3867
4120
  if (isTest === void 0) {
3868
4121
  isTest = isTestFilePath(filePath);
@@ -3879,9 +4132,13 @@ const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, rea
3879
4132
  return filterInlineSuppressions(filtered, directory, readFileLinesSync);
3880
4133
  };
3881
4134
  const combineDiagnostics = (input) => {
3882
- const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables } = input;
3883
- const extraDiagnostics = isDiffMode || !includeEnvironmentChecks ? [] : checkReducedMotion(directory);
3884
- 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 });
3885
4142
  };
3886
4143
  const findFirstLintConfigInDirectory = (directory) => {
3887
4144
  for (const filename of ADOPTABLE_LINT_CONFIG_FILENAMES) {
@@ -4004,11 +4261,11 @@ const getUncommittedChangedFiles = (directory) => {
4004
4261
  const getDiffInfo = (directory, explicitBaseBranch) => {
4005
4262
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
4006
4263
  const currentBranch = getCurrentBranch(directory);
4007
- if (!currentBranch) return null;
4264
+ if (!currentBranch && !explicitBaseBranch) return null;
4008
4265
  const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
4009
4266
  if (!baseBranch) return null;
4010
4267
  if (explicitBaseBranch && !branchExists(directory, explicitBaseBranch)) throw new Error(`Diff base branch "${explicitBaseBranch}" does not exist (run \`git fetch\` to update remote refs).`);
4011
- if (currentBranch === baseBranch) {
4268
+ if (currentBranch !== null && currentBranch === baseBranch) {
4012
4269
  const uncommittedFiles = getUncommittedChangedFiles(directory);
4013
4270
  if (uncommittedFiles.length === 0) return null;
4014
4271
  return {
@@ -4035,6 +4292,7 @@ const VALID_RULE_SEVERITIES = [
4035
4292
  ];
4036
4293
  const BOOLEAN_FIELD_NAMES = [
4037
4294
  "lint",
4295
+ "deadCode",
4038
4296
  "verbose",
4039
4297
  "customRulesOnly",
4040
4298
  "share",
@@ -4249,12 +4507,7 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
4249
4507
  while (stack.length > 0) {
4250
4508
  const current = stack.pop();
4251
4509
  if (current === void 0) continue;
4252
- let entries;
4253
- try {
4254
- entries = fs.readdirSync(current, { withFileTypes: true });
4255
- } catch {
4256
- continue;
4257
- }
4510
+ const entries = readDirectoryEntries(current);
4258
4511
  for (const entry of entries) {
4259
4512
  if (entry.isDirectory()) {
4260
4513
  if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
@@ -4312,7 +4565,7 @@ const resolveConfigRootDir = (config, configSourceDirectory) => {
4312
4565
  if (trimmedRootDir.length === 0) return null;
4313
4566
  const resolvedRootDir = path.isAbsolute(trimmedRootDir) ? trimmedRootDir : path.resolve(configSourceDirectory, trimmedRootDir);
4314
4567
  if (resolvedRootDir === configSourceDirectory) return null;
4315
- if (!fs.existsSync(resolvedRootDir) || !fs.statSync(resolvedRootDir).isDirectory()) {
4568
+ if (!isDirectory(resolvedRootDir)) {
4316
4569
  logger.warn(`react-doctor config "rootDir" points to "${rawRootDir}" (resolved to ${resolvedRootDir}), which is not a directory. Ignoring.`);
4317
4570
  return null;
4318
4571
  }
@@ -4345,7 +4598,7 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
4345
4598
  const stack = [rootDirectory];
4346
4599
  while (stack.length > 0) {
4347
4600
  const currentDirectory = stack.pop();
4348
- const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
4601
+ const entries = readDirectoryEntries(currentDirectory);
4349
4602
  for (const entry of entries) {
4350
4603
  const absolutePath = path.join(currentDirectory, entry.name);
4351
4604
  if (entry.isDirectory()) {
@@ -4395,10 +4648,13 @@ const buildCapabilities = (project) => {
4395
4648
  if (project.hasTypeScript) capabilities.add("typescript");
4396
4649
  return capabilities;
4397
4650
  };
4398
- const shouldEnableRule = (requires, tags, capabilities, ignoredTags) => {
4651
+ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
4399
4652
  if (requires) {
4400
4653
  for (const capability of requires) if (!capabilities.has(capability)) return false;
4401
4654
  }
4655
+ if (disabledBy) {
4656
+ for (const capability of disabledBy) if (capabilities.has(capability)) return false;
4657
+ }
4402
4658
  if (tags) {
4403
4659
  for (const tag of tags) if (ignoredTags.has(tag)) return false;
4404
4660
  }
@@ -4431,23 +4687,6 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
4431
4687
  availableRuleNames: readPluginRuleNames(pluginSpecifier)
4432
4688
  };
4433
4689
  };
4434
- const YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE = "effect";
4435
- const resolveYouMightNotNeedEffectPlugin = (customRulesOnly) => {
4436
- if (customRulesOnly) return null;
4437
- let pluginSpecifier;
4438
- try {
4439
- pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-you-might-not-need-an-effect");
4440
- } catch {
4441
- return null;
4442
- }
4443
- return {
4444
- entry: {
4445
- name: YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE,
4446
- specifier: pluginSpecifier
4447
- },
4448
- availableRuleNames: readPluginRuleNames(pluginSpecifier)
4449
- };
4450
- };
4451
4690
  const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
4452
4691
  if (availableRuleNames.size === 0) return rules;
4453
4692
  const ruleKeyPrefix = `${pluginNamespace}/`;
@@ -4466,26 +4705,36 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
4466
4705
  if (!fs.existsSync(rootDirectory)) return rootDirectory;
4467
4706
  return fs.realpathSync(rootDirectory);
4468
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
+ };
4469
4717
  const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls }) => {
4470
4718
  const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
4471
- const reactCompilerRules = reactHooksJsPlugin ? filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames) : {};
4472
- const youMightNotNeedEffectPlugin = resolveYouMightNotNeedEffectPlugin(customRulesOnly);
4473
- 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) : {};
4474
4720
  const jsPlugins = [];
4475
4721
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
4476
- if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
4477
4722
  const capabilities = buildCapabilities(project);
4478
4723
  const enabledReactDoctorRules = {};
4479
- for (const [ruleId, rule] of Object.entries(reactDoctorPlugin.rules)) {
4480
- 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;
4481
4728
  if (rule.framework !== "global" && !rule.requires) continue;
4482
- if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags)) continue;
4483
- const severity = resolveRuleSeverityOverride({
4484
- ruleKey: fullKey,
4729
+ if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
4730
+ const explicitSeverity = resolveRuleSeverityOverride({
4731
+ ruleKey: registryEntry.key,
4485
4732
  category: rule.category
4486
- }, severityControls) ?? rule.severity;
4733
+ }, severityControls);
4734
+ if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
4735
+ const severity = explicitSeverity ?? rule.severity;
4487
4736
  if (severity === "off") continue;
4488
- enabledReactDoctorRules[fullKey] = severity;
4737
+ enabledReactDoctorRules[registryEntry.key] = severity;
4489
4738
  }
4490
4739
  return {
4491
4740
  ...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
@@ -4498,7 +4747,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
4498
4747
  style: "off",
4499
4748
  nursery: "off"
4500
4749
  },
4501
- plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
4750
+ plugins: [],
4502
4751
  jsPlugins: [...jsPlugins, pluginPath],
4503
4752
  settings: { "react-doctor": {
4504
4753
  framework: project.framework,
@@ -4506,10 +4755,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
4506
4755
  ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
4507
4756
  } },
4508
4757
  rules: {
4509
- ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
4510
- ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
4511
4758
  ...reactCompilerRules,
4512
- ...youMightNotNeedEffectRules,
4513
4759
  ...enabledReactDoctorRules
4514
4760
  }
4515
4761
  };
@@ -4530,43 +4776,43 @@ const REACT_USE_BINDING_RESOLUTION = {
4530
4776
  isReactNamespaceBinding: false
4531
4777
  };
4532
4778
  const getScriptKind = (filename) => {
4533
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
4534
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
4535
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
4536
- 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;
4537
4783
  };
4538
4784
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
4539
4785
  const unwrapExpression = (expression) => {
4540
4786
  let currentExpression = expression;
4541
- 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;
4542
4788
  return currentExpression;
4543
4789
  };
4544
4790
  const getStaticPropertyName = (node) => {
4545
4791
  if (!node) return null;
4546
- if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
4547
- 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)) {
4548
4794
  const expression = unwrapExpression(node.expression);
4549
- if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
4795
+ if (ts$1.isStringLiteral(expression) || ts$1.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
4550
4796
  }
4551
4797
  return null;
4552
4798
  };
4553
4799
  const findBindingIdentifier = (bindingName, identifierName) => {
4554
- if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName ? bindingName : null;
4800
+ if (ts$1.isIdentifier(bindingName)) return bindingName.text === identifierName ? bindingName : null;
4555
4801
  for (const element of bindingName.elements) {
4556
- if (ts.isOmittedExpression(element)) continue;
4802
+ if (ts$1.isOmittedExpression(element)) continue;
4557
4803
  const nestedIdentifier = findBindingIdentifier(element.name, identifierName);
4558
4804
  if (nestedIdentifier) return nestedIdentifier;
4559
4805
  }
4560
4806
  return null;
4561
4807
  };
4562
4808
  const bindingNameHasIdentifier = (bindingName, identifierName) => {
4563
- if (ts.isIdentifier(bindingName)) return bindingName.text === identifierName;
4809
+ if (ts$1.isIdentifier(bindingName)) return bindingName.text === identifierName;
4564
4810
  return bindingName.elements.some((element) => {
4565
- if (ts.isOmittedExpression(element)) return false;
4811
+ if (ts$1.isOmittedExpression(element)) return false;
4566
4812
  return bindingNameHasIdentifier(element.name, identifierName);
4567
4813
  });
4568
4814
  };
4569
- const getDirectBindingIdentifier = (bindingName) => ts.isIdentifier(bindingName) ? bindingName : null;
4815
+ const getDirectBindingIdentifier = (bindingName) => ts$1.isIdentifier(bindingName) ? bindingName : null;
4570
4816
  const isReactUseObjectBindingElement = (bindingElement) => {
4571
4817
  const bindingIdentifier = getDirectBindingIdentifier(bindingElement.name);
4572
4818
  if (!bindingIdentifier) return false;
@@ -4575,12 +4821,12 @@ const isReactUseObjectBindingElement = (bindingElement) => {
4575
4821
  };
4576
4822
  const isReactRequireCall = (expression) => {
4577
4823
  const unwrappedExpression = unwrapExpression(expression);
4578
- 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;
4579
4825
  };
4580
4826
  const getModuleSource = (node) => {
4581
4827
  let currentNode = node;
4582
4828
  while (currentNode) {
4583
- 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;
4584
4830
  currentNode = currentNode.parent;
4585
4831
  }
4586
4832
  return null;
@@ -4597,40 +4843,40 @@ const isReactObjectBindingName = (bindingPattern, identifierName) => bindingPatt
4597
4843
  return isReactUseObjectBindingElement(bindingElement);
4598
4844
  });
4599
4845
  const isReactRequireBindingDeclaration = (node, identifierName) => {
4600
- if (!ts.isVariableDeclaration(node)) return false;
4846
+ if (!ts$1.isVariableDeclaration(node)) return false;
4601
4847
  if (!node.initializer) return false;
4602
4848
  if (!isReactRequireCall(node.initializer)) return false;
4603
- if (ts.isIdentifier(node.name)) return node.name.text === identifierName;
4604
- 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);
4605
4851
  };
4606
4852
  const collectReactImportBindings = (sourceFile) => {
4607
4853
  const namespaceNames = /* @__PURE__ */ new Set();
4608
4854
  const useImportNames = /* @__PURE__ */ new Set();
4609
4855
  for (const statement of sourceFile.statements) {
4610
- if (ts.isImportDeclaration(statement)) {
4611
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
4856
+ if (ts$1.isImportDeclaration(statement)) {
4857
+ if (!ts$1.isStringLiteral(statement.moduleSpecifier)) continue;
4612
4858
  if (statement.moduleSpecifier.text !== REACT_MODULE_SOURCE) continue;
4613
4859
  const importClause = statement.importClause;
4614
4860
  if (!importClause) continue;
4615
4861
  if (importClause.name) namespaceNames.add(importClause.name.text);
4616
4862
  const namedBindings = importClause.namedBindings;
4617
4863
  if (!namedBindings) continue;
4618
- if (ts.isNamespaceImport(namedBindings)) {
4864
+ if (ts$1.isNamespaceImport(namedBindings)) {
4619
4865
  namespaceNames.add(namedBindings.name.text);
4620
4866
  continue;
4621
4867
  }
4622
4868
  for (const importSpecifier of namedBindings.elements) if (getImportedName(importSpecifier) === USE_IDENTIFIER) useImportNames.add(importSpecifier.name.text);
4623
4869
  continue;
4624
4870
  }
4625
- if (!ts.isVariableStatement(statement)) continue;
4871
+ if (!ts$1.isVariableStatement(statement)) continue;
4626
4872
  for (const declaration of statement.declarationList.declarations) {
4627
4873
  if (!declaration.initializer) continue;
4628
4874
  if (!isReactRequireCall(declaration.initializer)) continue;
4629
- if (ts.isIdentifier(declaration.name)) {
4875
+ if (ts$1.isIdentifier(declaration.name)) {
4630
4876
  namespaceNames.add(declaration.name.text);
4631
4877
  continue;
4632
4878
  }
4633
- if (ts.isObjectBindingPattern(declaration.name)) collectReactObjectBindingNames(declaration.name, useImportNames);
4879
+ if (ts$1.isObjectBindingPattern(declaration.name)) collectReactObjectBindingNames(declaration.name, useImportNames);
4634
4880
  }
4635
4881
  }
4636
4882
  return {
@@ -4641,24 +4887,24 @@ const collectReactImportBindings = (sourceFile) => {
4641
4887
  const findBindingElement = (identifier) => {
4642
4888
  let currentNode = identifier.parent;
4643
4889
  while (currentNode) {
4644
- if (ts.isBindingElement(currentNode)) return currentNode;
4645
- 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;
4646
4892
  currentNode = currentNode.parent;
4647
4893
  }
4648
4894
  return null;
4649
4895
  };
4650
4896
  const declarationBindsIdentifier = (node, identifierName) => {
4651
- if (ts.isVariableDeclaration(node) || ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName);
4652
- 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;
4653
4899
  return false;
4654
4900
  };
4655
- 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);
4656
4902
  const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
4657
4903
  if (isReactRequireBindingDeclaration(node, identifierName)) return false;
4658
4904
  if (declarationBindsIdentifier(node, identifierName)) return true;
4659
4905
  if (node !== scopeNode && isScopeBoundary(node)) return false;
4660
4906
  let didFindBinding = false;
4661
- ts.forEachChild(node, (child) => {
4907
+ ts$1.forEachChild(node, (child) => {
4662
4908
  if (didFindBinding) return;
4663
4909
  didFindBinding = scopeContainsNonImportBinding(child, scopeNode, identifierName);
4664
4910
  });
@@ -4678,26 +4924,26 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
4678
4924
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
4679
4925
  const unwrappedExpression = unwrapExpression(expression);
4680
4926
  if (isReactRequireCall(unwrappedExpression)) return true;
4681
- if (!ts.isIdentifier(unwrappedExpression)) return false;
4927
+ if (!ts$1.isIdentifier(unwrappedExpression)) return false;
4682
4928
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
4683
4929
  return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactNamespaceBinding ?? false;
4684
4930
  };
4685
4931
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
4686
4932
  if (!expression) return false;
4687
4933
  const unwrappedExpression = unwrapExpression(expression);
4688
- if (ts.isIdentifier(unwrappedExpression)) {
4934
+ if (ts$1.isIdentifier(unwrappedExpression)) {
4689
4935
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
4690
4936
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
4691
4937
  return resolveIdentifierBinding(unwrappedExpression, reactImportBindings, sourceFile, visitedDeclarations)?.isReactUseBinding ?? false;
4692
4938
  }
4693
- if (ts.isPropertyAccessExpression(unwrappedExpression) && unwrappedExpression.name.text === USE_IDENTIFIER && isReactNamespaceExpression(unwrappedExpression.expression, reactImportBindings, sourceFile, visitedDeclarations)) return true;
4694
- 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);
4695
4941
  return false;
4696
4942
  };
4697
4943
  const isReactUseObjectBinding = (identifier, variableDeclaration, reactImportBindings, sourceFile, visitedDeclarations) => {
4698
4944
  const bindingElement = findBindingElement(identifier);
4699
4945
  if (!bindingElement) return false;
4700
- if (!ts.isObjectBindingPattern(bindingElement.parent)) return false;
4946
+ if (!ts$1.isObjectBindingPattern(bindingElement.parent)) return false;
4701
4947
  if (!variableDeclaration.initializer) return false;
4702
4948
  if (!isReactNamespaceExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, visitedDeclarations)) return false;
4703
4949
  return isReactUseObjectBindingElement(bindingElement);
@@ -4709,23 +4955,23 @@ const getVariableDeclarationResolution = (variableDeclaration, identifierName, r
4709
4955
  const nestedVisitedDeclarations = new Set(visitedDeclarations);
4710
4956
  nestedVisitedDeclarations.add(variableDeclaration);
4711
4957
  return {
4712
- 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)),
4713
4959
  isReactUseBinding: isReactUseExpression(variableDeclaration.initializer, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations)) || isReactUseObjectBinding(bindingIdentifier, variableDeclaration, reactImportBindings, sourceFile, new Set(nestedVisitedDeclarations))
4714
4960
  };
4715
4961
  };
4716
4962
  const getImportResolution = (node, identifierName) => {
4717
- 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;
4718
- if (ts.isNamespaceImport(node) && node.name.text === identifierName) return getModuleSource(node) === REACT_MODULE_SOURCE ? REACT_NAMESPACE_BINDING_RESOLUTION : LOCAL_BINDING_RESOLUTION;
4719
- 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;
4720
4966
  return null;
4721
4967
  };
4722
4968
  const getDeclarationResolution = (node, identifierName, reactImportBindings, sourceFile, visitedDeclarations) => {
4723
4969
  const importResolution = getImportResolution(node, identifierName);
4724
4970
  if (importResolution) return importResolution;
4725
- if (ts.isVariableDeclaration(node)) return getVariableDeclarationResolution(node, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4726
- if (ts.isParameter(node)) return bindingNameHasIdentifier(node.name, identifierName) ? LOCAL_BINDING_RESOLUTION : null;
4727
- if (ts.isFunctionDeclaration(node) && node.name?.text === identifierName) return LOCAL_BINDING_RESOLUTION;
4728
- 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;
4729
4975
  return null;
4730
4976
  };
4731
4977
  const isNestedScopeBoundary = (node, scopeNode) => node !== scopeNode && isScopeBoundary(node);
@@ -4734,14 +4980,14 @@ const findResolutionInSubtree = (node, scopeNode, identifierName, reactImportBin
4734
4980
  if (declarationResolution) return declarationResolution;
4735
4981
  if (isNestedScopeBoundary(node, scopeNode)) return null;
4736
4982
  let resolution = null;
4737
- ts.forEachChild(node, (child) => {
4983
+ ts$1.forEachChild(node, (child) => {
4738
4984
  if (resolution) return;
4739
4985
  resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4740
4986
  });
4741
4987
  return resolution;
4742
4988
  };
4743
4989
  const findResolutionInFunctionParameters = (node, identifierName, reactImportBindings) => {
4744
- if (!ts.isFunctionLike(node)) return null;
4990
+ if (!ts$1.isFunctionLike(node)) return null;
4745
4991
  for (const parameter of node.parameters) {
4746
4992
  const parameterResolution = getDeclarationResolution(parameter, identifierName, reactImportBindings, parameter.getSourceFile(), /* @__PURE__ */ new Set());
4747
4993
  if (parameterResolution) return parameterResolution;
@@ -4752,7 +4998,7 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
4752
4998
  const parameterResolution = findResolutionInFunctionParameters(scopeNode, identifierName, reactImportBindings);
4753
4999
  if (parameterResolution) return parameterResolution;
4754
5000
  let resolution = null;
4755
- ts.forEachChild(scopeNode, (child) => {
5001
+ ts$1.forEachChild(scopeNode, (child) => {
4756
5002
  if (resolution) return;
4757
5003
  resolution = findResolutionInSubtree(child, scopeNode, identifierName, reactImportBindings, sourceFile, visitedDeclarations);
4758
5004
  });
@@ -4770,22 +5016,22 @@ const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, v
4770
5016
  }
4771
5017
  return null;
4772
5018
  };
4773
- 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;
4774
5020
  const findUseCallIdentifier = (sourceFile, useOffset) => {
4775
5021
  let matchedIdentifier = null;
4776
5022
  const visit = (node) => {
4777
5023
  if (matchedIdentifier) return;
4778
- if (ts.isIdentifier(node) && isUseCallIdentifier(node) && node.getStart(sourceFile) === useOffset) {
5024
+ if (ts$1.isIdentifier(node) && isUseCallIdentifier(node) && node.getStart(sourceFile) === useOffset) {
4779
5025
  matchedIdentifier = node;
4780
5026
  return;
4781
5027
  }
4782
- ts.forEachChild(node, visit);
5028
+ ts$1.forEachChild(node, visit);
4783
5029
  };
4784
5030
  visit(sourceFile);
4785
5031
  return matchedIdentifier;
4786
5032
  };
4787
5033
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
4788
- 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));
4789
5035
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset(sourceText, utf8Offset));
4790
5036
  if (!useIdentifier) return null;
4791
5037
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -4887,7 +5133,13 @@ const SANITIZED_ENV = (() => {
4887
5133
  }
4888
5134
  return sanitized;
4889
5135
  })();
4890
- 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
+ })();
4891
5143
  const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
4892
5144
  const child = spawn(nodeBinaryPath, args, {
4893
5145
  cwd: rootDirectory,
@@ -5067,6 +5319,7 @@ const runOxlint = async (options) => {
5067
5319
  const spawnLintBatches = async () => {
5068
5320
  const allDiagnostics = [];
5069
5321
  const droppedFiles = [];
5322
+ let firstDropReason = null;
5070
5323
  const spawnLintBatch = async (batch) => {
5071
5324
  const batchArgs = [...baseArgs, ...batch];
5072
5325
  try {
@@ -5075,6 +5328,7 @@ const runOxlint = async (options) => {
5075
5328
  if (!isSplittableOxlintBatchError(error)) throw error;
5076
5329
  if (batch.length <= 1) {
5077
5330
  droppedFiles.push(...batch);
5331
+ if (firstDropReason === null && error instanceof Error) firstDropReason = error.message;
5078
5332
  return [];
5079
5333
  }
5080
5334
  const splitIndex = Math.ceil(batch.length / 2);
@@ -5086,7 +5340,8 @@ const runOxlint = async (options) => {
5086
5340
  const previewCount = 3;
5087
5341
  const previewFiles = droppedFiles.slice(0, previewCount).join(", ");
5088
5342
  const remainderHint = droppedFiles.length > previewCount ? `, +${droppedFiles.length - previewCount} more` : "";
5089
- 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}`);
5090
5345
  }
5091
5346
  return dedupeDiagnostics(allDiagnostics);
5092
5347
  };
@@ -5133,7 +5388,8 @@ const toJsonReport = (result, options) => buildJsonReport({
5133
5388
  result: {
5134
5389
  diagnostics: result.diagnostics,
5135
5390
  score: result.score,
5136
- skippedChecks: [],
5391
+ skippedChecks: result.skippedChecks,
5392
+ ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
5137
5393
  project: result.project,
5138
5394
  elapsedMilliseconds: result.elapsedMilliseconds
5139
5395
  }
@@ -5156,32 +5412,54 @@ const diagnose = async (directory, options = {}) => {
5156
5412
  const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
5157
5413
  const readFileLinesSync = createNodeReadFileLinesSync(resolvedDirectory);
5158
5414
  const effectiveLint = options.lint ?? userConfig?.lint ?? true;
5415
+ const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
5159
5416
  const effectiveRespectInlineDisables = options.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true;
5160
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]);
5161
5441
  const diagnostics = combineDiagnostics({
5162
- lintDiagnostics: effectiveLint ? await runOxlint({
5163
- rootDirectory: resolvedDirectory,
5164
- project: projectInfo,
5165
- includePaths: lintIncludePaths,
5166
- customRulesOnly: userConfig?.customRulesOnly ?? false,
5167
- respectInlineDisables: effectiveRespectInlineDisables,
5168
- adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
5169
- ignoredTags,
5170
- userConfig
5171
- }).catch((error) => {
5172
- console.error("Lint failed:", error);
5173
- return EMPTY_DIAGNOSTICS;
5174
- }) : EMPTY_DIAGNOSTICS,
5442
+ lintDiagnostics,
5175
5443
  directory: resolvedDirectory,
5176
5444
  isDiffMode,
5177
5445
  userConfig,
5178
5446
  readFileLinesSync,
5179
- respectInlineDisables: effectiveRespectInlineDisables
5447
+ respectInlineDisables: effectiveRespectInlineDisables,
5448
+ extraDiagnostics: deadCodeDiagnostics
5180
5449
  });
5181
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
+ }
5182
5458
  return {
5183
5459
  diagnostics,
5184
- score: await calculateScore(diagnostics),
5460
+ score,
5461
+ skippedChecks,
5462
+ ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
5185
5463
  project: projectInfo,
5186
5464
  elapsedMilliseconds
5187
5465
  };