react-doctor 0.2.1 → 0.2.3
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/README.md +66 -13
- package/dist/cli.js +7605 -1368
- package/dist/index.d.ts +27 -2
- package/dist/index.js +416 -137
- package/package.json +11 -16
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS,
|
|
5
|
+
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";
|
|
6
|
+
import { gzipSync } from "node:zlib";
|
|
6
7
|
import os from "node:os";
|
|
7
8
|
import * as ts from "typescript";
|
|
8
9
|
//#region \0rolldown/runtime.js
|
|
@@ -680,19 +681,6 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
|
|
|
680
681
|
}
|
|
681
682
|
return result;
|
|
682
683
|
};
|
|
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
684
|
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
697
685
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
698
686
|
if (!monorepoRoot) return EMPTY_DEPENDENCY_INFO;
|
|
@@ -720,13 +708,13 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
720
708
|
"peerDependencies"
|
|
721
709
|
]
|
|
722
710
|
}) : null;
|
|
723
|
-
const shouldUseReactFallback =
|
|
711
|
+
const shouldUseReactFallback = !leafReactDeclaration?.hasDeclaration;
|
|
724
712
|
const shouldUseTailwindFallback = leafTailwindDeclaration?.hasDeclaration ?? true;
|
|
725
713
|
const reactCatalogVersion = shouldUseReactFallback ? resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, leafReactDeclaration?.catalogReference) : null;
|
|
726
714
|
const tailwindCatalogVersion = shouldUseTailwindFallback ? resolveCatalogVersion(rootPackageJson, "tailwindcss", monorepoRoot, leafTailwindDeclaration?.catalogReference) : null;
|
|
727
715
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
728
716
|
return {
|
|
729
|
-
reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion :
|
|
717
|
+
reactVersion: shouldUseReactFallback ? reactCatalogVersion ?? rootInfo.reactVersion ?? workspaceInfo.reactVersion : rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
730
718
|
tailwindVersion: shouldUseTailwindFallback ? tailwindCatalogVersion ?? rootInfo.tailwindVersion ?? workspaceInfo.tailwindVersion : null,
|
|
731
719
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
732
720
|
};
|
|
@@ -793,6 +781,19 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
793
781
|
if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
|
|
794
782
|
return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
|
|
795
783
|
};
|
|
784
|
+
const REACT_DEPENDENCY_NAMES = new Set([
|
|
785
|
+
"react",
|
|
786
|
+
"react-native",
|
|
787
|
+
"next"
|
|
788
|
+
]);
|
|
789
|
+
const hasReactDependency = (packageJson) => {
|
|
790
|
+
const allDependencies = {
|
|
791
|
+
...packageJson.peerDependencies,
|
|
792
|
+
...packageJson.dependencies,
|
|
793
|
+
...packageJson.devDependencies
|
|
794
|
+
};
|
|
795
|
+
return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
|
|
796
|
+
};
|
|
796
797
|
const listWorkspacePackages = (rootDirectory) => {
|
|
797
798
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
798
799
|
if (!isFile(packageJsonPath)) return [];
|
|
@@ -3012,6 +3013,136 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
|
|
|
3012
3013
|
category: diagnostic.category,
|
|
3013
3014
|
tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
|
|
3014
3015
|
});
|
|
3016
|
+
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
3017
|
+
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
3018
|
+
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
3019
|
+
"effect/no-derived-state": "react-doctor/no-derived-state",
|
|
3020
|
+
"effect/no-event-handler": "react-doctor/no-event-handler",
|
|
3021
|
+
"effect/no-initialize-state": "react-doctor/no-initialize-state",
|
|
3022
|
+
"effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
|
|
3023
|
+
"effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
|
|
3024
|
+
"effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
|
|
3025
|
+
"jsx-a11y/alt-text": "react-doctor/alt-text",
|
|
3026
|
+
"jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
|
|
3027
|
+
"jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
|
|
3028
|
+
"jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
|
|
3029
|
+
"jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
|
|
3030
|
+
"jsx-a11y/aria-props": "react-doctor/aria-props",
|
|
3031
|
+
"jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
|
|
3032
|
+
"jsx-a11y/aria-role": "react-doctor/aria-role",
|
|
3033
|
+
"jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
|
|
3034
|
+
"jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
|
|
3035
|
+
"jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
|
|
3036
|
+
"jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
|
|
3037
|
+
"jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
|
|
3038
|
+
"jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
|
|
3039
|
+
"jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
|
|
3040
|
+
"jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
|
|
3041
|
+
"jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
|
|
3042
|
+
"jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
|
|
3043
|
+
"jsx-a11y/lang": "react-doctor/lang",
|
|
3044
|
+
"jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
|
|
3045
|
+
"jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
|
|
3046
|
+
"jsx-a11y/no-access-key": "react-doctor/no-access-key",
|
|
3047
|
+
"jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
|
|
3048
|
+
"jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
|
|
3049
|
+
"jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
|
|
3050
|
+
"jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
|
|
3051
|
+
"jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
|
|
3052
|
+
"jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
|
|
3053
|
+
"jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
|
|
3054
|
+
"jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
|
|
3055
|
+
"jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
|
|
3056
|
+
"jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
|
|
3057
|
+
"jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
|
|
3058
|
+
"jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
|
|
3059
|
+
"jsx-a11y/scope": "react-doctor/scope",
|
|
3060
|
+
"jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
|
|
3061
|
+
"react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
|
|
3062
|
+
"react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
|
|
3063
|
+
"react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
|
|
3064
|
+
"react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
|
|
3065
|
+
"react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
|
|
3066
|
+
"react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
|
|
3067
|
+
"react-refresh/only-export-components": "react-doctor/only-export-components",
|
|
3068
|
+
"react/button-has-type": "react-doctor/button-has-type",
|
|
3069
|
+
"react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
|
|
3070
|
+
"react/display-name": "react-doctor/display-name",
|
|
3071
|
+
"react/exhaustive-deps": "react-doctor/exhaustive-deps",
|
|
3072
|
+
"react/forbid-component-props": "react-doctor/forbid-component-props",
|
|
3073
|
+
"react/forbid-dom-props": "react-doctor/forbid-dom-props",
|
|
3074
|
+
"react/forbid-elements": "react-doctor/forbid-elements",
|
|
3075
|
+
"react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
|
|
3076
|
+
"react/hook-use-state": "react-doctor/hook-use-state",
|
|
3077
|
+
"react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
|
|
3078
|
+
"react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
|
|
3079
|
+
"react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
|
|
3080
|
+
"react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
|
|
3081
|
+
"react/jsx-fragments": "react-doctor/jsx-fragments",
|
|
3082
|
+
"react/jsx-handler-names": "react-doctor/jsx-handler-names",
|
|
3083
|
+
"react/jsx-key": "react-doctor/jsx-key",
|
|
3084
|
+
"react/jsx-max-depth": "react-doctor/jsx-max-depth",
|
|
3085
|
+
"react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
|
|
3086
|
+
"react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
|
|
3087
|
+
"react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
|
|
3088
|
+
"react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
|
|
3089
|
+
"react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
|
|
3090
|
+
"react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
|
|
3091
|
+
"react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
|
|
3092
|
+
"react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
|
|
3093
|
+
"react/jsx-no-target-blank": "react-doctor/jsx-no-target-blank",
|
|
3094
|
+
"react/jsx-no-undef": "react-doctor/jsx-no-undef",
|
|
3095
|
+
"react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
|
|
3096
|
+
"react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
|
|
3097
|
+
"react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
|
|
3098
|
+
"react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
|
|
3099
|
+
"react/no-array-index-key": "react-doctor/no-array-index-key",
|
|
3100
|
+
"react/no-children-prop": "react-doctor/no-children-prop",
|
|
3101
|
+
"react/no-clone-element": "react-doctor/no-clone-element",
|
|
3102
|
+
"react/no-danger": "react-doctor/no-danger",
|
|
3103
|
+
"react/no-danger-with-children": "react-doctor/no-danger-with-children",
|
|
3104
|
+
"react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
|
|
3105
|
+
"react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
|
|
3106
|
+
"react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
|
|
3107
|
+
"react/no-find-dom-node": "react-doctor/no-find-dom-node",
|
|
3108
|
+
"react/no-is-mounted": "react-doctor/no-is-mounted",
|
|
3109
|
+
"react/no-multi-comp": "react-doctor/no-multi-comp",
|
|
3110
|
+
"react/no-namespace": "react-doctor/no-namespace",
|
|
3111
|
+
"react/no-react-children": "react-doctor/no-react-children",
|
|
3112
|
+
"react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
|
|
3113
|
+
"react/no-render-return-value": "react-doctor/no-render-return-value",
|
|
3114
|
+
"react/no-set-state": "react-doctor/no-set-state",
|
|
3115
|
+
"react/no-string-refs": "react-doctor/no-string-refs",
|
|
3116
|
+
"react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
|
|
3117
|
+
"react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
|
|
3118
|
+
"react/no-unknown-property": "react-doctor/no-unknown-property",
|
|
3119
|
+
"react/no-unsafe": "react-doctor/no-unsafe",
|
|
3120
|
+
"react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
|
|
3121
|
+
"react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
|
|
3122
|
+
"react/only-export-components": "react-doctor/only-export-components",
|
|
3123
|
+
"react/prefer-es6-class": "react-doctor/prefer-es6-class",
|
|
3124
|
+
"react/prefer-function-component": "react-doctor/prefer-function-component",
|
|
3125
|
+
"react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
|
|
3126
|
+
"react/require-render-return": "react-doctor/require-render-return",
|
|
3127
|
+
"react/rules-of-hooks": "react-doctor/rules-of-hooks",
|
|
3128
|
+
"react/self-closing-comp": "react-doctor/self-closing-comp",
|
|
3129
|
+
"react/state-in-constructor": "react-doctor/state-in-constructor",
|
|
3130
|
+
"react/style-prop-object": "react-doctor/style-prop-object",
|
|
3131
|
+
"react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
|
|
3132
|
+
};
|
|
3133
|
+
const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
|
|
3134
|
+
for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
|
|
3135
|
+
const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
|
|
3136
|
+
aliases.push(legacyRuleKey);
|
|
3137
|
+
NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
|
|
3138
|
+
}
|
|
3139
|
+
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
3140
|
+
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
3141
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
|
|
3142
|
+
const getEquivalentRuleKeys = (ruleKey) => {
|
|
3143
|
+
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
3144
|
+
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
3145
|
+
};
|
|
3015
3146
|
/**
|
|
3016
3147
|
* Resolves the user-configured severity override for a rule.
|
|
3017
3148
|
* Per-rule overrides win over per-category overrides. Returns
|
|
@@ -3020,7 +3151,14 @@ const getDiagnosticRuleIdentity = (diagnostic) => ({
|
|
|
3020
3151
|
*/
|
|
3021
3152
|
const resolveRuleSeverityOverride = (input, controls) => {
|
|
3022
3153
|
if (!controls) return void 0;
|
|
3023
|
-
|
|
3154
|
+
const exactRuleOverride = controls.rules?.[input.ruleKey];
|
|
3155
|
+
if (exactRuleOverride !== void 0) return exactRuleOverride;
|
|
3156
|
+
for (const equivalentRuleKey of getEquivalentRuleKeys(input.ruleKey)) {
|
|
3157
|
+
if (equivalentRuleKey === input.ruleKey) continue;
|
|
3158
|
+
const equivalentRuleOverride = controls.rules?.[equivalentRuleKey];
|
|
3159
|
+
if (equivalentRuleOverride !== void 0) return equivalentRuleOverride;
|
|
3160
|
+
}
|
|
3161
|
+
return input.category !== void 0 ? controls.categories?.[input.category] : void 0;
|
|
3024
3162
|
};
|
|
3025
3163
|
const SEVERITY_FOR_OVERRIDE = {
|
|
3026
3164
|
error: "error",
|
|
@@ -3229,14 +3367,19 @@ const describeFailure = (error) => {
|
|
|
3229
3367
|
if (error instanceof Error && error.message) return error.message;
|
|
3230
3368
|
return String(error);
|
|
3231
3369
|
};
|
|
3232
|
-
const calculateScore = async (diagnostics) => {
|
|
3370
|
+
const calculateScore = async (diagnostics, options = {}) => {
|
|
3233
3371
|
const controller = new AbortController();
|
|
3234
3372
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3373
|
+
const requestUrl = options.isCi ? `${SCORE_API_URL}?ci=1` : SCORE_API_URL;
|
|
3235
3374
|
try {
|
|
3236
|
-
const
|
|
3375
|
+
const compressedBody = gzipSync(JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }));
|
|
3376
|
+
const response = await fetch(requestUrl, {
|
|
3237
3377
|
method: "POST",
|
|
3238
|
-
headers: {
|
|
3239
|
-
|
|
3378
|
+
headers: {
|
|
3379
|
+
"Content-Type": "application/json",
|
|
3380
|
+
"Content-Encoding": "gzip"
|
|
3381
|
+
},
|
|
3382
|
+
body: compressedBody,
|
|
3240
3383
|
signal: controller.signal
|
|
3241
3384
|
});
|
|
3242
3385
|
if (!response.ok) {
|
|
@@ -3323,60 +3466,6 @@ const canOxlintExtendConfig = (configPath) => {
|
|
|
3323
3466
|
if (extendsEntries.length === 0) return true;
|
|
3324
3467
|
return extendsEntries.some((entry) => typeof entry === "string" && isLocalPathExtend(entry));
|
|
3325
3468
|
};
|
|
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
3469
|
const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
|
|
3381
3470
|
const FALSY_VALUES = new Set([
|
|
3382
3471
|
"false",
|
|
@@ -3556,6 +3645,148 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
3556
3645
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
3557
3646
|
return patterns;
|
|
3558
3647
|
};
|
|
3648
|
+
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
3649
|
+
const resolveTsConfigPath = (rootDirectory) => {
|
|
3650
|
+
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
3651
|
+
const candidate = path.join(rootDirectory, filename);
|
|
3652
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
|
|
3656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3657
|
+
const sources = [
|
|
3658
|
+
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
3659
|
+
collectIgnorePatterns(rootDirectory),
|
|
3660
|
+
userConfig?.ignore?.files ?? []
|
|
3661
|
+
];
|
|
3662
|
+
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
3663
|
+
return [...seen].filter((pattern) => pattern.length > 0);
|
|
3664
|
+
};
|
|
3665
|
+
const toRelativeFilePath = (rootDirectory, filePath) => {
|
|
3666
|
+
const relative = toRelativePath(filePath, rootDirectory);
|
|
3667
|
+
return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
|
|
3668
|
+
};
|
|
3669
|
+
const checkDeadCode = async (options) => {
|
|
3670
|
+
const { rootDirectory, userConfig } = options;
|
|
3671
|
+
if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
|
|
3672
|
+
const { analyze, defineConfig } = await import("deslop-js");
|
|
3673
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
|
|
3674
|
+
const result = await analyze(defineConfig({
|
|
3675
|
+
rootDir: rootDirectory,
|
|
3676
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
3677
|
+
...ignorePatterns.length > 0 ? { ignorePatterns } : {}
|
|
3678
|
+
}));
|
|
3679
|
+
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
3680
|
+
const diagnostics = [];
|
|
3681
|
+
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
3682
|
+
filePath: toRelative(unusedFile.path),
|
|
3683
|
+
plugin: "deslop",
|
|
3684
|
+
rule: "unused-file",
|
|
3685
|
+
severity: "warning",
|
|
3686
|
+
message: "Unused file — not reachable from any entry point",
|
|
3687
|
+
help: "Delete the file if it is truly unreachable, or import it from an entry point.",
|
|
3688
|
+
line: 0,
|
|
3689
|
+
column: 0,
|
|
3690
|
+
category: "Dead Code"
|
|
3691
|
+
});
|
|
3692
|
+
for (const unusedExport of result.unusedExports) {
|
|
3693
|
+
const label = unusedExport.isTypeOnly ? "type export" : "export";
|
|
3694
|
+
diagnostics.push({
|
|
3695
|
+
filePath: toRelative(unusedExport.path),
|
|
3696
|
+
plugin: "deslop",
|
|
3697
|
+
rule: unusedExport.isTypeOnly ? "unused-type" : "unused-export",
|
|
3698
|
+
severity: "warning",
|
|
3699
|
+
message: `Unused ${label}: \`${unusedExport.name}\``,
|
|
3700
|
+
help: "Drop the `export` keyword (or remove the declaration) if no other module uses this symbol.",
|
|
3701
|
+
line: unusedExport.line,
|
|
3702
|
+
column: unusedExport.column,
|
|
3703
|
+
category: "Dead Code"
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
for (const unusedDependency of result.unusedDependencies) {
|
|
3707
|
+
const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
|
|
3708
|
+
diagnostics.push({
|
|
3709
|
+
filePath: "package.json",
|
|
3710
|
+
plugin: "deslop",
|
|
3711
|
+
rule: unusedDependency.isDevDependency ? "unused-dev-dependency" : "unused-dependency",
|
|
3712
|
+
severity: "warning",
|
|
3713
|
+
message: `Unused ${label}: \`${unusedDependency.name}\``,
|
|
3714
|
+
help: "Remove the dependency from package.json if it is genuinely unused.",
|
|
3715
|
+
line: 0,
|
|
3716
|
+
column: 0,
|
|
3717
|
+
category: "Dead Code"
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
for (const cycle of result.circularDependencies) {
|
|
3721
|
+
if (cycle.files.length === 0) continue;
|
|
3722
|
+
diagnostics.push({
|
|
3723
|
+
filePath: toRelative(cycle.files[0]),
|
|
3724
|
+
plugin: "deslop",
|
|
3725
|
+
rule: "circular-dependency",
|
|
3726
|
+
severity: "warning",
|
|
3727
|
+
message: `Circular import cycle: ${cycle.files.map(toRelative).join(" → ")}`,
|
|
3728
|
+
help: "Break the cycle by extracting the shared code into a third module that both files import.",
|
|
3729
|
+
line: 0,
|
|
3730
|
+
column: 0,
|
|
3731
|
+
category: "Dead Code"
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
return diagnostics;
|
|
3735
|
+
};
|
|
3736
|
+
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
3737
|
+
const REDUCED_MOTION_FILE_GLOBS = [
|
|
3738
|
+
"*.ts",
|
|
3739
|
+
"*.tsx",
|
|
3740
|
+
"*.js",
|
|
3741
|
+
"*.jsx",
|
|
3742
|
+
"*.css",
|
|
3743
|
+
"*.scss"
|
|
3744
|
+
];
|
|
3745
|
+
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
3746
|
+
filePath: "package.json",
|
|
3747
|
+
plugin: "react-doctor",
|
|
3748
|
+
rule: "require-reduced-motion",
|
|
3749
|
+
severity: "error",
|
|
3750
|
+
message: "Project uses a motion library but has no prefers-reduced-motion handling — required for accessibility (WCAG 2.3.3)",
|
|
3751
|
+
help: "Add `useReducedMotion()` from your animation library, or a `@media (prefers-reduced-motion: reduce)` CSS query",
|
|
3752
|
+
line: 0,
|
|
3753
|
+
column: 0,
|
|
3754
|
+
category: "Accessibility"
|
|
3755
|
+
};
|
|
3756
|
+
const checkReducedMotion = (rootDirectory) => {
|
|
3757
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
3758
|
+
if (!isFile(packageJsonPath)) return [];
|
|
3759
|
+
let hasMotionLibrary = false;
|
|
3760
|
+
try {
|
|
3761
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
3762
|
+
const allDependencies = {
|
|
3763
|
+
...packageJson.dependencies,
|
|
3764
|
+
...packageJson.devDependencies
|
|
3765
|
+
};
|
|
3766
|
+
hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
|
|
3767
|
+
} catch {
|
|
3768
|
+
return [];
|
|
3769
|
+
}
|
|
3770
|
+
if (!hasMotionLibrary) return [];
|
|
3771
|
+
const result = spawnSync("git", [
|
|
3772
|
+
"grep",
|
|
3773
|
+
"-ql",
|
|
3774
|
+
"-E",
|
|
3775
|
+
REDUCED_MOTION_GREP_PATTERN,
|
|
3776
|
+
"--",
|
|
3777
|
+
...REDUCED_MOTION_FILE_GLOBS
|
|
3778
|
+
], {
|
|
3779
|
+
cwd: rootDirectory,
|
|
3780
|
+
stdio: [
|
|
3781
|
+
"ignore",
|
|
3782
|
+
"pipe",
|
|
3783
|
+
"pipe"
|
|
3784
|
+
]
|
|
3785
|
+
});
|
|
3786
|
+
if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
3787
|
+
if (result.status === 0) return [];
|
|
3788
|
+
return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
3789
|
+
};
|
|
3559
3790
|
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
3560
3791
|
return (filePath) => {
|
|
3561
3792
|
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
@@ -3685,7 +3916,7 @@ const isRuleListedInComment = (ruleList, ruleId) => {
|
|
|
3685
3916
|
if (!trimmed) return true;
|
|
3686
3917
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
3687
3918
|
if (!ruleSection) return true;
|
|
3688
|
-
return ruleSection.split(/[,\s]+/).some((token) => token.trim()
|
|
3919
|
+
return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
|
|
3689
3920
|
};
|
|
3690
3921
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
3691
3922
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -3837,6 +4068,10 @@ const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrap
|
|
|
3837
4068
|
}
|
|
3838
4069
|
return false;
|
|
3839
4070
|
};
|
|
4071
|
+
const isIgnoredRule = (ignoredRules, ruleIdentifier) => {
|
|
4072
|
+
for (const ignoredRule of ignoredRules) if (isSameRuleKey(ignoredRule, ruleIdentifier)) return true;
|
|
4073
|
+
return false;
|
|
4074
|
+
};
|
|
3840
4075
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
3841
4076
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
3842
4077
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
@@ -3847,8 +4082,7 @@ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLi
|
|
|
3847
4082
|
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
3848
4083
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
3849
4084
|
return diagnostics.filter((diagnostic) => {
|
|
3850
|
-
|
|
3851
|
-
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
4085
|
+
if (isIgnoredRule(ignoredRules, `${diagnostic.plugin}/${diagnostic.rule}`)) return false;
|
|
3852
4086
|
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
3853
4087
|
if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
|
|
3854
4088
|
if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
@@ -3876,11 +4110,24 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
|
|
|
3876
4110
|
}] : [diagnostic];
|
|
3877
4111
|
});
|
|
3878
4112
|
};
|
|
3879
|
-
const
|
|
4113
|
+
const TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\//;
|
|
4114
|
+
const TEST_FILE_SUFFIX_PATTERN = /\.(?:test|spec|stories|story|fixture|fixtures)\.(?:[cm]?[jt]sx?)$/;
|
|
4115
|
+
const FIXTURE_PROJECT_PATTERN = /\/(?:fixtures|__fixtures__)\//;
|
|
4116
|
+
const SOURCE_ROOT_PATTERN = /\/(?:src|app|lib|components|pages|features|modules|packages|apps|frontend|client)\//g;
|
|
4117
|
+
const stripAboveSourceRoot = (relativePath) => {
|
|
4118
|
+
const fixtureMatch = FIXTURE_PROJECT_PATTERN.exec(relativePath);
|
|
4119
|
+
if (fixtureMatch === null) return relativePath;
|
|
4120
|
+
let lastIdx = -1;
|
|
4121
|
+
for (const match of relativePath.matchAll(SOURCE_ROOT_PATTERN)) if (match.index !== void 0 && match.index > lastIdx) lastIdx = match.index;
|
|
4122
|
+
if (lastIdx >= 0) return relativePath.slice(lastIdx);
|
|
4123
|
+
return relativePath.slice(fixtureMatch.index + fixtureMatch[0].length - 1);
|
|
4124
|
+
};
|
|
3880
4125
|
const isTestFilePath = (relativePath) => {
|
|
3881
4126
|
if (relativePath.length === 0) return false;
|
|
3882
4127
|
const forwardSlashed = relativePath.replaceAll("\\", "/");
|
|
3883
|
-
|
|
4128
|
+
if (TEST_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return true;
|
|
4129
|
+
const scoped = stripAboveSourceRoot(forwardSlashed);
|
|
4130
|
+
return TEST_FILE_DIRECTORY_PATTERN.test(scoped);
|
|
3884
4131
|
};
|
|
3885
4132
|
const testFileResultCache = /* @__PURE__ */ new Map();
|
|
3886
4133
|
const clearAutoSuppressionCaches = () => {
|
|
@@ -3888,7 +4135,9 @@ const clearAutoSuppressionCaches = () => {
|
|
|
3888
4135
|
};
|
|
3889
4136
|
const shouldAutoSuppress = (diagnostic) => {
|
|
3890
4137
|
const filePath = diagnostic.filePath;
|
|
3891
|
-
|
|
4138
|
+
const rule = diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule] : null;
|
|
4139
|
+
if (rule?.tags?.includes("test-noise")) {
|
|
4140
|
+
if (rule.tags.includes("migration-hint")) return false;
|
|
3892
4141
|
let isTest = testFileResultCache.get(filePath);
|
|
3893
4142
|
if (isTest === void 0) {
|
|
3894
4143
|
isTest = isTestFilePath(filePath);
|
|
@@ -3905,9 +4154,13 @@ const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, rea
|
|
|
3905
4154
|
return filterInlineSuppressions(filtered, directory, readFileLinesSync);
|
|
3906
4155
|
};
|
|
3907
4156
|
const combineDiagnostics = (input) => {
|
|
3908
|
-
const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables } = input;
|
|
3909
|
-
const
|
|
3910
|
-
return mergeAndFilterDiagnostics([
|
|
4157
|
+
const { lintDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true, respectInlineDisables, extraDiagnostics = [] } = input;
|
|
4158
|
+
const environmentDiagnostics = isDiffMode || !includeEnvironmentChecks ? [] : checkReducedMotion(directory);
|
|
4159
|
+
return mergeAndFilterDiagnostics([
|
|
4160
|
+
...lintDiagnostics,
|
|
4161
|
+
...environmentDiagnostics,
|
|
4162
|
+
...extraDiagnostics
|
|
4163
|
+
], directory, userConfig, readFileLinesSync, { respectInlineDisables });
|
|
3911
4164
|
};
|
|
3912
4165
|
const findFirstLintConfigInDirectory = (directory) => {
|
|
3913
4166
|
for (const filename of ADOPTABLE_LINT_CONFIG_FILENAMES) {
|
|
@@ -4030,11 +4283,11 @@ const getUncommittedChangedFiles = (directory) => {
|
|
|
4030
4283
|
const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
4031
4284
|
if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) throw new Error("Diff base branch cannot be empty.");
|
|
4032
4285
|
const currentBranch = getCurrentBranch(directory);
|
|
4033
|
-
if (!currentBranch) return null;
|
|
4286
|
+
if (!currentBranch && !explicitBaseBranch) return null;
|
|
4034
4287
|
const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
|
|
4035
4288
|
if (!baseBranch) return null;
|
|
4036
4289
|
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) {
|
|
4290
|
+
if (currentBranch !== null && currentBranch === baseBranch) {
|
|
4038
4291
|
const uncommittedFiles = getUncommittedChangedFiles(directory);
|
|
4039
4292
|
if (uncommittedFiles.length === 0) return null;
|
|
4040
4293
|
return {
|
|
@@ -4061,6 +4314,7 @@ const VALID_RULE_SEVERITIES = [
|
|
|
4061
4314
|
];
|
|
4062
4315
|
const BOOLEAN_FIELD_NAMES = [
|
|
4063
4316
|
"lint",
|
|
4317
|
+
"deadCode",
|
|
4064
4318
|
"verbose",
|
|
4065
4319
|
"customRulesOnly",
|
|
4066
4320
|
"share",
|
|
@@ -4416,10 +4670,13 @@ const buildCapabilities = (project) => {
|
|
|
4416
4670
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
4417
4671
|
return capabilities;
|
|
4418
4672
|
};
|
|
4419
|
-
const shouldEnableRule = (requires, tags, capabilities, ignoredTags) => {
|
|
4673
|
+
const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
|
|
4420
4674
|
if (requires) {
|
|
4421
4675
|
for (const capability of requires) if (!capabilities.has(capability)) return false;
|
|
4422
4676
|
}
|
|
4677
|
+
if (disabledBy) {
|
|
4678
|
+
for (const capability of disabledBy) if (capabilities.has(capability)) return false;
|
|
4679
|
+
}
|
|
4423
4680
|
if (tags) {
|
|
4424
4681
|
for (const tag of tags) if (ignoredTags.has(tag)) return false;
|
|
4425
4682
|
}
|
|
@@ -4452,23 +4709,6 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
|
4452
4709
|
availableRuleNames: readPluginRuleNames(pluginSpecifier)
|
|
4453
4710
|
};
|
|
4454
4711
|
};
|
|
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
4712
|
const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
|
|
4473
4713
|
if (availableRuleNames.size === 0) return rules;
|
|
4474
4714
|
const ruleKeyPrefix = `${pluginNamespace}/`;
|
|
@@ -4487,26 +4727,36 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
|
|
|
4487
4727
|
if (!fs.existsSync(rootDirectory)) return rootDirectory;
|
|
4488
4728
|
return fs.realpathSync(rootDirectory);
|
|
4489
4729
|
};
|
|
4730
|
+
const applyRuleSeverityControls = (rules, severityControls) => {
|
|
4731
|
+
const enabledRules = {};
|
|
4732
|
+
for (const [ruleKey, defaultSeverity] of Object.entries(rules)) {
|
|
4733
|
+
const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? defaultSeverity;
|
|
4734
|
+
if (severity === "off") continue;
|
|
4735
|
+
enabledRules[ruleKey] = severity;
|
|
4736
|
+
}
|
|
4737
|
+
return enabledRules;
|
|
4738
|
+
};
|
|
4490
4739
|
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls }) => {
|
|
4491
4740
|
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) : {};
|
|
4741
|
+
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
4495
4742
|
const jsPlugins = [];
|
|
4496
4743
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
4497
|
-
if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
|
|
4498
4744
|
const capabilities = buildCapabilities(project);
|
|
4499
4745
|
const enabledReactDoctorRules = {};
|
|
4500
|
-
for (const
|
|
4501
|
-
const
|
|
4746
|
+
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
4747
|
+
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
4748
|
+
if (!rule) continue;
|
|
4749
|
+
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
4502
4750
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
4503
|
-
if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags)) continue;
|
|
4504
|
-
const
|
|
4505
|
-
ruleKey:
|
|
4751
|
+
if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
|
|
4752
|
+
const explicitSeverity = resolveRuleSeverityOverride({
|
|
4753
|
+
ruleKey: registryEntry.key,
|
|
4506
4754
|
category: rule.category
|
|
4507
|
-
}, severityControls)
|
|
4755
|
+
}, severityControls);
|
|
4756
|
+
if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
|
|
4757
|
+
const severity = explicitSeverity ?? rule.severity;
|
|
4508
4758
|
if (severity === "off") continue;
|
|
4509
|
-
enabledReactDoctorRules[
|
|
4759
|
+
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
4510
4760
|
}
|
|
4511
4761
|
return {
|
|
4512
4762
|
...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
|
|
@@ -4519,7 +4769,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
4519
4769
|
style: "off",
|
|
4520
4770
|
nursery: "off"
|
|
4521
4771
|
},
|
|
4522
|
-
plugins:
|
|
4772
|
+
plugins: [],
|
|
4523
4773
|
jsPlugins: [...jsPlugins, pluginPath],
|
|
4524
4774
|
settings: { "react-doctor": {
|
|
4525
4775
|
framework: project.framework,
|
|
@@ -4527,10 +4777,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
4527
4777
|
...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
|
|
4528
4778
|
} },
|
|
4529
4779
|
rules: {
|
|
4530
|
-
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
4531
|
-
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
4532
4780
|
...reactCompilerRules,
|
|
4533
|
-
...youMightNotNeedEffectRules,
|
|
4534
4781
|
...enabledReactDoctorRules
|
|
4535
4782
|
}
|
|
4536
4783
|
};
|
|
@@ -4908,7 +5155,13 @@ const SANITIZED_ENV = (() => {
|
|
|
4908
5155
|
}
|
|
4909
5156
|
return sanitized;
|
|
4910
5157
|
})();
|
|
4911
|
-
const OXLINT_SPAWN_TIMEOUT_MS =
|
|
5158
|
+
const OXLINT_SPAWN_TIMEOUT_MS = (() => {
|
|
5159
|
+
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
5160
|
+
if (raw === void 0) return 6e4;
|
|
5161
|
+
const parsed = Number(raw);
|
|
5162
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 6e4;
|
|
5163
|
+
return parsed;
|
|
5164
|
+
})();
|
|
4912
5165
|
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
4913
5166
|
const child = spawn(nodeBinaryPath, args, {
|
|
4914
5167
|
cwd: rootDirectory,
|
|
@@ -5088,6 +5341,7 @@ const runOxlint = async (options) => {
|
|
|
5088
5341
|
const spawnLintBatches = async () => {
|
|
5089
5342
|
const allDiagnostics = [];
|
|
5090
5343
|
const droppedFiles = [];
|
|
5344
|
+
let firstDropReason = null;
|
|
5091
5345
|
const spawnLintBatch = async (batch) => {
|
|
5092
5346
|
const batchArgs = [...baseArgs, ...batch];
|
|
5093
5347
|
try {
|
|
@@ -5096,6 +5350,7 @@ const runOxlint = async (options) => {
|
|
|
5096
5350
|
if (!isSplittableOxlintBatchError(error)) throw error;
|
|
5097
5351
|
if (batch.length <= 1) {
|
|
5098
5352
|
droppedFiles.push(...batch);
|
|
5353
|
+
if (firstDropReason === null && error instanceof Error) firstDropReason = error.message;
|
|
5099
5354
|
return [];
|
|
5100
5355
|
}
|
|
5101
5356
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
@@ -5107,7 +5362,8 @@ const runOxlint = async (options) => {
|
|
|
5107
5362
|
const previewCount = 3;
|
|
5108
5363
|
const previewFiles = droppedFiles.slice(0, previewCount).join(", ");
|
|
5109
5364
|
const remainderHint = droppedFiles.length > previewCount ? `, +${droppedFiles.length - previewCount} more` : "";
|
|
5110
|
-
|
|
5365
|
+
const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
|
|
5366
|
+
onPartialFailure(`${droppedFiles.length} file(s) failed to lint and were skipped (${previewFiles}${remainderHint})${reasonHint}`);
|
|
5111
5367
|
}
|
|
5112
5368
|
return dedupeDiagnostics(allDiagnostics);
|
|
5113
5369
|
};
|
|
@@ -5154,7 +5410,8 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
5154
5410
|
result: {
|
|
5155
5411
|
diagnostics: result.diagnostics,
|
|
5156
5412
|
score: result.score,
|
|
5157
|
-
skippedChecks:
|
|
5413
|
+
skippedChecks: result.skippedChecks,
|
|
5414
|
+
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
5158
5415
|
project: result.project,
|
|
5159
5416
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
5160
5417
|
}
|
|
@@ -5177,32 +5434,54 @@ const diagnose = async (directory, options = {}) => {
|
|
|
5177
5434
|
const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
|
|
5178
5435
|
const readFileLinesSync = createNodeReadFileLinesSync(resolvedDirectory);
|
|
5179
5436
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
5437
|
+
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
5180
5438
|
const effectiveRespectInlineDisables = options.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true;
|
|
5181
5439
|
const ignoredTags = new Set(userConfig?.ignore?.tags ?? []);
|
|
5440
|
+
const lintPromise = effectiveLint ? runOxlint({
|
|
5441
|
+
rootDirectory: resolvedDirectory,
|
|
5442
|
+
project: projectInfo,
|
|
5443
|
+
includePaths: lintIncludePaths,
|
|
5444
|
+
customRulesOnly: userConfig?.customRulesOnly ?? false,
|
|
5445
|
+
respectInlineDisables: effectiveRespectInlineDisables,
|
|
5446
|
+
adoptExistingLintConfig: userConfig?.adoptExistingLintConfig ?? true,
|
|
5447
|
+
ignoredTags,
|
|
5448
|
+
userConfig
|
|
5449
|
+
}).catch((error) => {
|
|
5450
|
+
console.error("Lint failed:", error);
|
|
5451
|
+
return EMPTY_DIAGNOSTICS;
|
|
5452
|
+
}) : Promise.resolve(EMPTY_DIAGNOSTICS);
|
|
5453
|
+
const shouldRunDeadCode = effectiveDeadCode && !isDiffMode;
|
|
5454
|
+
let deadCodeFailureReason = null;
|
|
5455
|
+
const deadCodePromise = shouldRunDeadCode ? checkDeadCode({
|
|
5456
|
+
rootDirectory: resolvedDirectory,
|
|
5457
|
+
userConfig
|
|
5458
|
+
}).catch((error) => {
|
|
5459
|
+
deadCodeFailureReason = error instanceof Error ? error.message : String(error);
|
|
5460
|
+
return EMPTY_DIAGNOSTICS;
|
|
5461
|
+
}) : Promise.resolve(EMPTY_DIAGNOSTICS);
|
|
5462
|
+
const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
|
|
5182
5463
|
const diagnostics = combineDiagnostics({
|
|
5183
|
-
lintDiagnostics
|
|
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,
|
|
5464
|
+
lintDiagnostics,
|
|
5196
5465
|
directory: resolvedDirectory,
|
|
5197
5466
|
isDiffMode,
|
|
5198
5467
|
userConfig,
|
|
5199
5468
|
readFileLinesSync,
|
|
5200
|
-
respectInlineDisables: effectiveRespectInlineDisables
|
|
5469
|
+
respectInlineDisables: effectiveRespectInlineDisables,
|
|
5470
|
+
extraDiagnostics: deadCodeDiagnostics
|
|
5201
5471
|
});
|
|
5202
5472
|
const elapsedMilliseconds = globalThis.performance.now() - startTime;
|
|
5473
|
+
const score = await calculateScore(diagnostics);
|
|
5474
|
+
const skippedChecks = [];
|
|
5475
|
+
const skippedCheckReasons = {};
|
|
5476
|
+
if (deadCodeFailureReason !== null) {
|
|
5477
|
+
skippedChecks.push("dead-code");
|
|
5478
|
+
skippedCheckReasons["dead-code"] = deadCodeFailureReason;
|
|
5479
|
+
}
|
|
5203
5480
|
return {
|
|
5204
5481
|
diagnostics,
|
|
5205
|
-
score
|
|
5482
|
+
score,
|
|
5483
|
+
skippedChecks,
|
|
5484
|
+
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
5206
5485
|
project: projectInfo,
|
|
5207
5486
|
elapsedMilliseconds
|
|
5208
5487
|
};
|