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