oxlint-plugin-react-doctor 0.2.10 → 0.2.11-dev.d917f62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +43 -36
- package/dist/index.js +215 -248
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -82,10 +82,17 @@ interface ScopeAnalysis {
|
|
|
82
82
|
//#region src/plugin/utils/rule-context.d.ts
|
|
83
83
|
interface BaseRuleContext {
|
|
84
84
|
report: (descriptor: ReportDescriptor) => void;
|
|
85
|
-
|
|
85
|
+
readonly filename?: string;
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Rules use `context.filename`. Read only as a fallback by
|
|
88
|
+
* `wrapWithSemanticContext`; ESLint implements it as a `this`-bound class
|
|
89
|
+
* method, so it must be called on the host context, never a detached
|
|
90
|
+
* reference.
|
|
91
|
+
*/
|
|
92
|
+
getFilename?: () => string | undefined;
|
|
86
93
|
readonly settings?: Readonly<Record<string, unknown>>;
|
|
87
94
|
}
|
|
88
|
-
interface RuleContext extends BaseRuleContext {
|
|
95
|
+
interface RuleContext extends Omit<BaseRuleContext, "getFilename"> {
|
|
89
96
|
readonly scopes: ScopeAnalysis;
|
|
90
97
|
readonly cfg: ControlFlowAnalysis;
|
|
91
98
|
}
|
|
@@ -3104,6 +3111,23 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
3104
3111
|
readonly recommendation?: string;
|
|
3105
3112
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3106
3113
|
};
|
|
3114
|
+
}, {
|
|
3115
|
+
readonly key: "react-doctor/no-prop-types";
|
|
3116
|
+
readonly id: "no-prop-types";
|
|
3117
|
+
readonly source: "react-doctor";
|
|
3118
|
+
readonly originallyExternal: false;
|
|
3119
|
+
readonly rule: {
|
|
3120
|
+
readonly framework: "global";
|
|
3121
|
+
readonly category: "Architecture";
|
|
3122
|
+
readonly id: string;
|
|
3123
|
+
readonly severity: RuleSeverity;
|
|
3124
|
+
readonly requires?: ReadonlyArray<string>;
|
|
3125
|
+
readonly disabledBy?: ReadonlyArray<string>;
|
|
3126
|
+
readonly tags?: ReadonlyArray<string>;
|
|
3127
|
+
readonly defaultEnabled?: boolean;
|
|
3128
|
+
readonly recommendation?: string;
|
|
3129
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3130
|
+
};
|
|
3107
3131
|
}, {
|
|
3108
3132
|
readonly key: "react-doctor/no-pure-black-background";
|
|
3109
3133
|
readonly id: "no-pure-black-background";
|
|
@@ -3954,23 +3978,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
3954
3978
|
readonly recommendation?: string;
|
|
3955
3979
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3956
3980
|
};
|
|
3957
|
-
}, {
|
|
3958
|
-
readonly key: "react-doctor/react-compiler-destructure-method";
|
|
3959
|
-
readonly id: "react-compiler-destructure-method";
|
|
3960
|
-
readonly source: "react-doctor";
|
|
3961
|
-
readonly originallyExternal: false;
|
|
3962
|
-
readonly rule: {
|
|
3963
|
-
readonly framework: "global";
|
|
3964
|
-
readonly category: "Architecture";
|
|
3965
|
-
readonly id: string;
|
|
3966
|
-
readonly severity: RuleSeverity;
|
|
3967
|
-
readonly requires?: ReadonlyArray<string>;
|
|
3968
|
-
readonly disabledBy?: ReadonlyArray<string>;
|
|
3969
|
-
readonly tags?: ReadonlyArray<string>;
|
|
3970
|
-
readonly defaultEnabled?: boolean;
|
|
3971
|
-
readonly recommendation?: string;
|
|
3972
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3973
|
-
};
|
|
3974
3981
|
}, {
|
|
3975
3982
|
readonly key: "react-doctor/react-compiler-no-manual-memoization";
|
|
3976
3983
|
readonly id: "react-compiler-no-manual-memoization";
|
|
@@ -8389,6 +8396,23 @@ declare const RULES: readonly [{
|
|
|
8389
8396
|
readonly recommendation?: string;
|
|
8390
8397
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
8391
8398
|
};
|
|
8399
|
+
}, {
|
|
8400
|
+
readonly key: "react-doctor/no-prop-types";
|
|
8401
|
+
readonly id: "no-prop-types";
|
|
8402
|
+
readonly source: "react-doctor";
|
|
8403
|
+
readonly originallyExternal: false;
|
|
8404
|
+
readonly rule: {
|
|
8405
|
+
readonly framework: "global";
|
|
8406
|
+
readonly category: "Architecture";
|
|
8407
|
+
readonly id: string;
|
|
8408
|
+
readonly severity: RuleSeverity;
|
|
8409
|
+
readonly requires?: ReadonlyArray<string>;
|
|
8410
|
+
readonly disabledBy?: ReadonlyArray<string>;
|
|
8411
|
+
readonly tags?: ReadonlyArray<string>;
|
|
8412
|
+
readonly defaultEnabled?: boolean;
|
|
8413
|
+
readonly recommendation?: string;
|
|
8414
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
8415
|
+
};
|
|
8392
8416
|
}, {
|
|
8393
8417
|
readonly key: "react-doctor/no-pure-black-background";
|
|
8394
8418
|
readonly id: "no-pure-black-background";
|
|
@@ -9239,23 +9263,6 @@ declare const RULES: readonly [{
|
|
|
9239
9263
|
readonly recommendation?: string;
|
|
9240
9264
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
9241
9265
|
};
|
|
9242
|
-
}, {
|
|
9243
|
-
readonly key: "react-doctor/react-compiler-destructure-method";
|
|
9244
|
-
readonly id: "react-compiler-destructure-method";
|
|
9245
|
-
readonly source: "react-doctor";
|
|
9246
|
-
readonly originallyExternal: false;
|
|
9247
|
-
readonly rule: {
|
|
9248
|
-
readonly framework: "global";
|
|
9249
|
-
readonly category: "Architecture";
|
|
9250
|
-
readonly id: string;
|
|
9251
|
-
readonly severity: RuleSeverity;
|
|
9252
|
-
readonly requires?: ReadonlyArray<string>;
|
|
9253
|
-
readonly disabledBy?: ReadonlyArray<string>;
|
|
9254
|
-
readonly tags?: ReadonlyArray<string>;
|
|
9255
|
-
readonly defaultEnabled?: boolean;
|
|
9256
|
-
readonly recommendation?: string;
|
|
9257
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
9258
|
-
};
|
|
9259
9266
|
}, {
|
|
9260
9267
|
readonly key: "react-doctor/react-compiler-no-manual-memoization";
|
|
9261
9268
|
readonly id: "react-compiler-no-manual-memoization";
|
package/dist/index.js
CHANGED
|
@@ -223,7 +223,7 @@ const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
|
223
223
|
//#endregion
|
|
224
224
|
//#region src/plugin/utils/define-rule.ts
|
|
225
225
|
const wrapCreateForTestNoise = (create) => ((context) => {
|
|
226
|
-
if (isTestlikeFilename(context.
|
|
226
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
227
227
|
return create(context);
|
|
228
228
|
});
|
|
229
229
|
const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -813,6 +813,12 @@ const getElementType = (openingElement, settings) => {
|
|
|
813
813
|
return baseName;
|
|
814
814
|
};
|
|
815
815
|
//#endregion
|
|
816
|
+
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
817
|
+
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
818
|
+
if (!rawFilename) return false;
|
|
819
|
+
return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
|
|
820
|
+
};
|
|
821
|
+
//#endregion
|
|
816
822
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
817
823
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
818
824
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -983,6 +989,7 @@ const altText = defineRule({
|
|
|
983
989
|
recommendation: "Provide `alt` (or aria-label / aria-labelledby) for non-decorative images.",
|
|
984
990
|
category: "Accessibility",
|
|
985
991
|
create: (context) => {
|
|
992
|
+
if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
|
|
986
993
|
const settings = resolveSettings$53(context.settings);
|
|
987
994
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
988
995
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
@@ -1019,7 +1026,7 @@ const altText = defineRule({
|
|
|
1019
1026
|
});
|
|
1020
1027
|
//#endregion
|
|
1021
1028
|
//#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
|
|
1022
|
-
const buildMessage$
|
|
1029
|
+
const buildMessage$29 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
|
|
1023
1030
|
const DEFAULT_AMBIGUOUS = [
|
|
1024
1031
|
"click here",
|
|
1025
1032
|
"here",
|
|
@@ -1076,7 +1083,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1076
1083
|
const normalized = normalizeText(accessibleText);
|
|
1077
1084
|
if (ambiguousSet.has(normalized)) context.report({
|
|
1078
1085
|
node: node.openingElement.name,
|
|
1079
|
-
message: buildMessage$
|
|
1086
|
+
message: buildMessage$29(normalized)
|
|
1080
1087
|
});
|
|
1081
1088
|
} };
|
|
1082
1089
|
}
|
|
@@ -1655,7 +1662,7 @@ const ARIA_PROPERTIES = new Map([
|
|
|
1655
1662
|
const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
|
|
1656
1663
|
//#endregion
|
|
1657
1664
|
//#region src/plugin/rules/a11y/aria-props.ts
|
|
1658
|
-
const buildMessage$
|
|
1665
|
+
const buildMessage$28 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
|
|
1659
1666
|
const ariaProps = defineRule({
|
|
1660
1667
|
id: "aria-props",
|
|
1661
1668
|
tags: ["react-jsx-only"],
|
|
@@ -1668,7 +1675,7 @@ const ariaProps = defineRule({
|
|
|
1668
1675
|
if (!name || !name.startsWith("aria-")) return;
|
|
1669
1676
|
if (!isValidAriaProperty(name)) context.report({
|
|
1670
1677
|
node: node.name,
|
|
1671
|
-
message: buildMessage$
|
|
1678
|
+
message: buildMessage$28(name)
|
|
1672
1679
|
});
|
|
1673
1680
|
} })
|
|
1674
1681
|
});
|
|
@@ -1819,7 +1826,7 @@ const buildExpectedDescription = (propType) => {
|
|
|
1819
1826
|
case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
|
|
1820
1827
|
}
|
|
1821
1828
|
};
|
|
1822
|
-
const buildMessage$
|
|
1829
|
+
const buildMessage$27 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
|
|
1823
1830
|
const allowNoneValue = (propType) => {
|
|
1824
1831
|
switch (propType.kind) {
|
|
1825
1832
|
case "boolean":
|
|
@@ -1952,13 +1959,13 @@ const ariaProptypes = defineRule({
|
|
|
1952
1959
|
if (!node.value) {
|
|
1953
1960
|
if (!allowNoneValue(propType)) context.report({
|
|
1954
1961
|
node,
|
|
1955
|
-
message: buildMessage$
|
|
1962
|
+
message: buildMessage$27(propName, propType)
|
|
1956
1963
|
});
|
|
1957
1964
|
return;
|
|
1958
1965
|
}
|
|
1959
1966
|
if (!isValidValueForType(propType, node.value)) context.report({
|
|
1960
1967
|
node,
|
|
1961
|
-
message: buildMessage$
|
|
1968
|
+
message: buildMessage$27(propName, propType)
|
|
1962
1969
|
});
|
|
1963
1970
|
} })
|
|
1964
1971
|
});
|
|
@@ -2270,7 +2277,7 @@ const ariaRole = defineRule({
|
|
|
2270
2277
|
});
|
|
2271
2278
|
//#endregion
|
|
2272
2279
|
//#region src/plugin/rules/a11y/aria-unsupported-elements.ts
|
|
2273
|
-
const buildMessage$
|
|
2280
|
+
const buildMessage$26 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
|
|
2274
2281
|
const ariaUnsupportedElements = defineRule({
|
|
2275
2282
|
id: "aria-unsupported-elements",
|
|
2276
2283
|
tags: ["react-jsx-only"],
|
|
@@ -2287,7 +2294,7 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2287
2294
|
if (!attrName) continue;
|
|
2288
2295
|
if (attrName.startsWith("aria-") || attrName === "role") context.report({
|
|
2289
2296
|
node: attribute,
|
|
2290
|
-
message: buildMessage$
|
|
2297
|
+
message: buildMessage$26(tag, attrName)
|
|
2291
2298
|
});
|
|
2292
2299
|
}
|
|
2293
2300
|
} })
|
|
@@ -2712,6 +2719,17 @@ const asyncAwaitInLoop = defineRule({
|
|
|
2712
2719
|
}
|
|
2713
2720
|
});
|
|
2714
2721
|
//#endregion
|
|
2722
|
+
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
2723
|
+
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
2724
|
+
"implements",
|
|
2725
|
+
"returnType",
|
|
2726
|
+
"superTypeArguments",
|
|
2727
|
+
"superTypeParameters",
|
|
2728
|
+
"typeAnnotation",
|
|
2729
|
+
"typeArguments",
|
|
2730
|
+
"typeParameters"
|
|
2731
|
+
]);
|
|
2732
|
+
//#endregion
|
|
2715
2733
|
//#region src/plugin/utils/collect-pattern-names.ts
|
|
2716
2734
|
const collectPatternNames = (pattern, into) => {
|
|
2717
2735
|
if (!pattern) return;
|
|
@@ -2741,14 +2759,6 @@ const collectPatternNames = (pattern, into) => {
|
|
|
2741
2759
|
};
|
|
2742
2760
|
//#endregion
|
|
2743
2761
|
//#region src/plugin/utils/collect-reference-identifier-names.ts
|
|
2744
|
-
const TYPE_POSITION_KEYS = new Set([
|
|
2745
|
-
"typeAnnotation",
|
|
2746
|
-
"typeParameters",
|
|
2747
|
-
"typeArguments",
|
|
2748
|
-
"returnType",
|
|
2749
|
-
"superTypeArguments",
|
|
2750
|
-
"superTypeParameters"
|
|
2751
|
-
]);
|
|
2752
2762
|
const collectScopedReferencesInPattern = (pattern, into, shadowed) => {
|
|
2753
2763
|
if (!pattern) return;
|
|
2754
2764
|
if (isNodeOfType(pattern, "Identifier")) return;
|
|
@@ -2809,7 +2819,7 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
2809
2819
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
2810
2820
|
for (const [key, child] of Object.entries(node)) {
|
|
2811
2821
|
if (key === "parent") continue;
|
|
2812
|
-
if (
|
|
2822
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
2813
2823
|
if (Array.isArray(child)) {
|
|
2814
2824
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
2815
2825
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -3127,7 +3137,7 @@ const asyncParallel = defineRule({
|
|
|
3127
3137
|
severity: "warn",
|
|
3128
3138
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
3129
3139
|
create: (context) => {
|
|
3130
|
-
const filename = normalizeFilename$1(context.
|
|
3140
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
3131
3141
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
3132
3142
|
let hasTestLibraryImport = false;
|
|
3133
3143
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -3154,7 +3164,7 @@ const asyncParallel = defineRule({
|
|
|
3154
3164
|
});
|
|
3155
3165
|
//#endregion
|
|
3156
3166
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
3157
|
-
const buildMessage$
|
|
3167
|
+
const buildMessage$25 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
|
|
3158
3168
|
const AUTOFILL_TOKENS = new Set([
|
|
3159
3169
|
"off",
|
|
3160
3170
|
"on",
|
|
@@ -3242,7 +3252,7 @@ const autocompleteValid = defineRule({
|
|
|
3242
3252
|
if (!AUTOFILL_TOKENS.has(token)) {
|
|
3243
3253
|
context.report({
|
|
3244
3254
|
node: attribute,
|
|
3245
|
-
message: buildMessage$
|
|
3255
|
+
message: buildMessage$25(value)
|
|
3246
3256
|
});
|
|
3247
3257
|
return;
|
|
3248
3258
|
}
|
|
@@ -3330,7 +3340,7 @@ const buttonHasType = defineRule({
|
|
|
3330
3340
|
recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
|
|
3331
3341
|
create: (context) => {
|
|
3332
3342
|
const settings = resolveSettings$48(context.settings);
|
|
3333
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3343
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3334
3344
|
return {
|
|
3335
3345
|
JSXOpeningElement(node) {
|
|
3336
3346
|
if (isTestlikeFile) return;
|
|
@@ -3530,7 +3540,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3530
3540
|
recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
|
|
3531
3541
|
category: "Accessibility",
|
|
3532
3542
|
create: (context) => {
|
|
3533
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3543
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3534
3544
|
return { JSXOpeningElement(node) {
|
|
3535
3545
|
if (isTestlikeFile) return;
|
|
3536
3546
|
const tag = getElementType(node, context.settings);
|
|
@@ -3791,7 +3801,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3791
3801
|
category: "Accessibility",
|
|
3792
3802
|
create: (context) => {
|
|
3793
3803
|
const settings = resolveSettings$46(context.settings);
|
|
3794
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3804
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3795
3805
|
return { JSXElement(node) {
|
|
3796
3806
|
if (isTestlikeFile) return;
|
|
3797
3807
|
const opening = node.openingElement;
|
|
@@ -4928,6 +4938,33 @@ const inferReferenceFlag = (identifier) => {
|
|
|
4928
4938
|
const setNodeScope = (node, state) => {
|
|
4929
4939
|
state.nodeScope.set(node, state.currentScope);
|
|
4930
4940
|
};
|
|
4941
|
+
const walkParameterReferences = (pattern, state) => {
|
|
4942
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
4943
|
+
walkParameterReferences(pattern.left, state);
|
|
4944
|
+
const defaultValue = pattern.right ?? null;
|
|
4945
|
+
if (defaultValue) walk(defaultValue, state);
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
4949
|
+
for (const property of pattern.properties) {
|
|
4950
|
+
const propertyNode = property;
|
|
4951
|
+
if (isNodeOfType(propertyNode, "RestElement")) {
|
|
4952
|
+
walkParameterReferences(propertyNode.argument, state);
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (!isNodeOfType(propertyNode, "Property")) continue;
|
|
4956
|
+
const propertyDetail = propertyNode;
|
|
4957
|
+
if (propertyDetail.computed) walk(propertyDetail.key, state);
|
|
4958
|
+
walkParameterReferences(propertyDetail.value, state);
|
|
4959
|
+
}
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4962
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
4963
|
+
for (const element of pattern.elements) if (element) walkParameterReferences(element, state);
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
4967
|
+
};
|
|
4931
4968
|
const walk = (node, state) => {
|
|
4932
4969
|
if (isFunctionLike$1(node)) {
|
|
4933
4970
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
@@ -4943,7 +4980,9 @@ const walk = (node, state) => {
|
|
|
4943
4980
|
});
|
|
4944
4981
|
tagAsBinding(state, node.id);
|
|
4945
4982
|
}
|
|
4946
|
-
|
|
4983
|
+
const functionParams = node.params ?? [];
|
|
4984
|
+
handleFunctionParameters(functionParams, fnScope, state);
|
|
4985
|
+
for (const param of functionParams) walkParameterReferences(param, state);
|
|
4947
4986
|
const body = node.body;
|
|
4948
4987
|
if (body) walk(body, state);
|
|
4949
4988
|
popScope(state);
|
|
@@ -4985,6 +5024,7 @@ const walk = (node, state) => {
|
|
|
4985
5024
|
const nodeRecord = node;
|
|
4986
5025
|
for (const key of Object.keys(nodeRecord)) {
|
|
4987
5026
|
if (key === "parent") continue;
|
|
5027
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4988
5028
|
const child = nodeRecord[key];
|
|
4989
5029
|
if (Array.isArray(child)) {
|
|
4990
5030
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5047,6 +5087,7 @@ const walk = (node, state) => {
|
|
|
5047
5087
|
const nodeRecord = node;
|
|
5048
5088
|
for (const key of Object.keys(nodeRecord)) {
|
|
5049
5089
|
if (key === "parent") continue;
|
|
5090
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5050
5091
|
const child = nodeRecord[key];
|
|
5051
5092
|
if (Array.isArray(child)) {
|
|
5052
5093
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5166,14 +5207,6 @@ const isAstDescendant = (inner, outer) => {
|
|
|
5166
5207
|
};
|
|
5167
5208
|
//#endregion
|
|
5168
5209
|
//#region src/plugin/semantic/closure-captures.ts
|
|
5169
|
-
const TYPE_ONLY_CHILD_KEYS = new Set([
|
|
5170
|
-
"implements",
|
|
5171
|
-
"returnType",
|
|
5172
|
-
"superTypeArguments",
|
|
5173
|
-
"typeAnnotation",
|
|
5174
|
-
"typeArguments",
|
|
5175
|
-
"typeParameters"
|
|
5176
|
-
]);
|
|
5177
5210
|
const closureCaptures = (functionNode, scopes) => {
|
|
5178
5211
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
5179
5212
|
const out = [];
|
|
@@ -5201,7 +5234,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5201
5234
|
const record = node;
|
|
5202
5235
|
for (const key of Object.keys(record)) {
|
|
5203
5236
|
if (key === "parent") continue;
|
|
5204
|
-
if (
|
|
5237
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5205
5238
|
const child = record[key];
|
|
5206
5239
|
if (Array.isArray(child)) {
|
|
5207
5240
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
@@ -5564,33 +5597,6 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
5564
5597
|
if (!depKey) continue;
|
|
5565
5598
|
keys.add(depKey);
|
|
5566
5599
|
}
|
|
5567
|
-
const functionParams = callback.params ?? [];
|
|
5568
|
-
for (const param of functionParams) {
|
|
5569
|
-
if (!isNodeOfType(param, "AssignmentPattern")) continue;
|
|
5570
|
-
const visitDefaultValue = (node) => {
|
|
5571
|
-
if (isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression")) {
|
|
5572
|
-
const depKey = stringifyMemberChain(node);
|
|
5573
|
-
if (depKey) keys.add(depKey);
|
|
5574
|
-
}
|
|
5575
|
-
const reference = scopes.referenceFor(node);
|
|
5576
|
-
if (reference?.resolvedSymbol) {
|
|
5577
|
-
const symbol = reference.resolvedSymbol;
|
|
5578
|
-
if (!isOutsideAllFunctions(symbol)) {
|
|
5579
|
-
const depKey = computeDepKey(reference);
|
|
5580
|
-
if (depKey) keys.add(depKey);
|
|
5581
|
-
}
|
|
5582
|
-
}
|
|
5583
|
-
const record = node;
|
|
5584
|
-
for (const key of Object.keys(record)) {
|
|
5585
|
-
if (key === "parent") continue;
|
|
5586
|
-
const child = record[key];
|
|
5587
|
-
if (Array.isArray(child)) {
|
|
5588
|
-
for (const item of child) if (isAstNode(item)) visitDefaultValue(item);
|
|
5589
|
-
} else if (isAstNode(child)) visitDefaultValue(child);
|
|
5590
|
-
}
|
|
5591
|
-
};
|
|
5592
|
-
visitDefaultValue(param.right);
|
|
5593
|
-
}
|
|
5594
5600
|
return {
|
|
5595
5601
|
keys,
|
|
5596
5602
|
stableCapturedNames
|
|
@@ -6200,7 +6206,7 @@ const flattenJsxName$1 = (name) => {
|
|
|
6200
6206
|
return "";
|
|
6201
6207
|
};
|
|
6202
6208
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
6203
|
-
const buildMessage$
|
|
6209
|
+
const buildMessage$24 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
|
|
6204
6210
|
const forbidComponentProps = defineRule({
|
|
6205
6211
|
id: "forbid-component-props",
|
|
6206
6212
|
severity: "warn",
|
|
@@ -6226,7 +6232,7 @@ const forbidComponentProps = defineRule({
|
|
|
6226
6232
|
if (!isForbiddenForTag(entry, tag)) continue;
|
|
6227
6233
|
context.report({
|
|
6228
6234
|
node: attribute,
|
|
6229
|
-
message: buildMessage$
|
|
6235
|
+
message: buildMessage$24(propName, entry.message)
|
|
6230
6236
|
});
|
|
6231
6237
|
break;
|
|
6232
6238
|
}
|
|
@@ -6236,7 +6242,7 @@ const forbidComponentProps = defineRule({
|
|
|
6236
6242
|
});
|
|
6237
6243
|
//#endregion
|
|
6238
6244
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6239
|
-
const buildMessage$
|
|
6245
|
+
const buildMessage$23 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
|
|
6240
6246
|
const resolveSettings$43 = (settings) => {
|
|
6241
6247
|
const reactDoctor = settings?.["react-doctor"];
|
|
6242
6248
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
@@ -6274,7 +6280,7 @@ const forbidDomProps = defineRule({
|
|
|
6274
6280
|
if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
|
|
6275
6281
|
context.report({
|
|
6276
6282
|
node: attribute.name,
|
|
6277
|
-
message: buildMessage$
|
|
6283
|
+
message: buildMessage$23(propName, descriptor.message)
|
|
6278
6284
|
});
|
|
6279
6285
|
}
|
|
6280
6286
|
} };
|
|
@@ -6344,7 +6350,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6344
6350
|
};
|
|
6345
6351
|
//#endregion
|
|
6346
6352
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6347
|
-
const buildMessage$
|
|
6353
|
+
const buildMessage$22 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
|
|
6348
6354
|
const resolveSettings$42 = (settings) => {
|
|
6349
6355
|
const reactDoctor = settings?.["react-doctor"];
|
|
6350
6356
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
@@ -6368,7 +6374,7 @@ const forbidElements = defineRule({
|
|
|
6368
6374
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6369
6375
|
context.report({
|
|
6370
6376
|
node: node.name,
|
|
6371
|
-
message: buildMessage$
|
|
6377
|
+
message: buildMessage$22(fullName, forbidMap.get(fullName))
|
|
6372
6378
|
});
|
|
6373
6379
|
},
|
|
6374
6380
|
CallExpression(node) {
|
|
@@ -6388,7 +6394,7 @@ const forbidElements = defineRule({
|
|
|
6388
6394
|
if (!elementName || !forbidMap.has(elementName)) return;
|
|
6389
6395
|
context.report({
|
|
6390
6396
|
node: firstArgument,
|
|
6391
|
-
message: buildMessage$
|
|
6397
|
+
message: buildMessage$22(elementName, forbidMap.get(elementName))
|
|
6392
6398
|
});
|
|
6393
6399
|
}
|
|
6394
6400
|
};
|
|
@@ -6690,7 +6696,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
|
|
|
6690
6696
|
"table",
|
|
6691
6697
|
"ul"
|
|
6692
6698
|
]);
|
|
6693
|
-
const buildMessage$
|
|
6699
|
+
const buildMessage$21 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
|
|
6694
6700
|
const isParagraphElement = (candidate) => {
|
|
6695
6701
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6696
6702
|
const opening = candidate.openingElement;
|
|
@@ -6718,7 +6724,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
6718
6724
|
if (!findEnclosingParagraph(node)) return;
|
|
6719
6725
|
context.report({
|
|
6720
6726
|
node: node.name,
|
|
6721
|
-
message: buildMessage$
|
|
6727
|
+
message: buildMessage$21(childTagName)
|
|
6722
6728
|
});
|
|
6723
6729
|
} })
|
|
6724
6730
|
});
|
|
@@ -6738,7 +6744,7 @@ const ROW_GROUPS = new Set([
|
|
|
6738
6744
|
"tbody",
|
|
6739
6745
|
"tfoot"
|
|
6740
6746
|
]);
|
|
6741
|
-
const buildMessage$
|
|
6747
|
+
const buildMessage$20 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
|
|
6742
6748
|
const buildNestedTableMessage = () => "Improper table nesting — `<table>` cannot be a direct descendant of another table element. Tables can only nest inside a `<td>` or `<th>` cell of an outer table.";
|
|
6743
6749
|
const getHostTagName = (jsxElement) => {
|
|
6744
6750
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
@@ -6806,28 +6812,28 @@ const htmlNoInvalidTableNesting = defineRule({
|
|
|
6806
6812
|
if (ROW_GROUPS.has(tagName)) {
|
|
6807
6813
|
if (actualParent !== "table") context.report({
|
|
6808
6814
|
node: node.openingElement.name,
|
|
6809
|
-
message: buildMessage$
|
|
6815
|
+
message: buildMessage$20(tagName, "`<table>`", actualParent)
|
|
6810
6816
|
});
|
|
6811
6817
|
return;
|
|
6812
6818
|
}
|
|
6813
6819
|
if (tagName === "tr") {
|
|
6814
6820
|
if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
|
|
6815
6821
|
node: node.openingElement.name,
|
|
6816
|
-
message: buildMessage$
|
|
6822
|
+
message: buildMessage$20(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
|
|
6817
6823
|
});
|
|
6818
6824
|
return;
|
|
6819
6825
|
}
|
|
6820
6826
|
if (tagName === "td" || tagName === "th") {
|
|
6821
6827
|
if (actualParent !== "tr") context.report({
|
|
6822
6828
|
node: node.openingElement.name,
|
|
6823
|
-
message: buildMessage$
|
|
6829
|
+
message: buildMessage$20(tagName, "`<tr>`", actualParent)
|
|
6824
6830
|
});
|
|
6825
6831
|
}
|
|
6826
6832
|
} })
|
|
6827
6833
|
});
|
|
6828
6834
|
//#endregion
|
|
6829
6835
|
//#region src/plugin/rules/correctness/html-no-nested-interactive.ts
|
|
6830
|
-
const buildMessage$
|
|
6836
|
+
const buildMessage$19 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
|
|
6831
6837
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
6832
6838
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6833
6839
|
const opening = candidate.openingElement;
|
|
@@ -6855,7 +6861,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
6855
6861
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
6856
6862
|
context.report({
|
|
6857
6863
|
node: node.name,
|
|
6858
|
-
message: buildMessage$
|
|
6864
|
+
message: buildMessage$19(tagName)
|
|
6859
6865
|
});
|
|
6860
6866
|
} })
|
|
6861
6867
|
});
|
|
@@ -8423,6 +8429,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8423
8429
|
id: "js-tosorted-immutable",
|
|
8424
8430
|
tags: ["test-noise"],
|
|
8425
8431
|
severity: "warn",
|
|
8432
|
+
disabledBy: ["react-native"],
|
|
8426
8433
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
8427
8434
|
create: (context) => ({ CallExpression(node) {
|
|
8428
8435
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -8711,7 +8718,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
8711
8718
|
const settings = resolveSettings$34(context.settings);
|
|
8712
8719
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8713
8720
|
const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
|
|
8714
|
-
const filename =
|
|
8721
|
+
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8715
8722
|
const extensionOnly = path.extname(filename).slice(1);
|
|
8716
8723
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
8717
8724
|
let didReportMismatch = false;
|
|
@@ -9337,7 +9344,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
9337
9344
|
};
|
|
9338
9345
|
//#endregion
|
|
9339
9346
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9340
|
-
const buildMessage$
|
|
9347
|
+
const buildMessage$18 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
9341
9348
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9342
9349
|
const resolveSettings$30 = (settings) => {
|
|
9343
9350
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -9404,7 +9411,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9404
9411
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
9405
9412
|
if (total > max) context.report({
|
|
9406
9413
|
node,
|
|
9407
|
-
message: buildMessage$
|
|
9414
|
+
message: buildMessage$18(total, max)
|
|
9408
9415
|
});
|
|
9409
9416
|
};
|
|
9410
9417
|
return {
|
|
@@ -9495,7 +9502,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9495
9502
|
recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
|
|
9496
9503
|
category: "Performance",
|
|
9497
9504
|
create: (context) => {
|
|
9498
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9505
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9499
9506
|
return { JSXOpeningElement(node) {
|
|
9500
9507
|
if (isTestlikeFile) return;
|
|
9501
9508
|
if (!isProviderName(node.name)) return;
|
|
@@ -9841,7 +9848,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
9841
9848
|
recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
|
|
9842
9849
|
category: "Performance",
|
|
9843
9850
|
create: (context) => {
|
|
9844
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9851
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9845
9852
|
let memoRegistry = null;
|
|
9846
9853
|
return {
|
|
9847
9854
|
Program(node) {
|
|
@@ -10212,7 +10219,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10212
10219
|
recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
|
|
10213
10220
|
category: "Performance",
|
|
10214
10221
|
create: (context) => {
|
|
10215
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10222
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10216
10223
|
let memoRegistry = null;
|
|
10217
10224
|
return {
|
|
10218
10225
|
Program(node) {
|
|
@@ -10674,7 +10681,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10674
10681
|
recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
|
|
10675
10682
|
category: "Performance",
|
|
10676
10683
|
create: (context) => {
|
|
10677
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10684
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10678
10685
|
let memoRegistry = null;
|
|
10679
10686
|
return {
|
|
10680
10687
|
Program(node) {
|
|
@@ -10980,7 +10987,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10980
10987
|
recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
|
|
10981
10988
|
category: "Performance",
|
|
10982
10989
|
create: (context) => {
|
|
10983
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10990
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10984
10991
|
let memoRegistry = null;
|
|
10985
10992
|
return {
|
|
10986
10993
|
Program(node) {
|
|
@@ -11340,7 +11347,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
11340
11347
|
});
|
|
11341
11348
|
//#endregion
|
|
11342
11349
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11343
|
-
const buildMessage$
|
|
11350
|
+
const buildMessage$17 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
11344
11351
|
const KNOWN_GLOBALS = new Set([
|
|
11345
11352
|
"globalThis",
|
|
11346
11353
|
"window",
|
|
@@ -11375,7 +11382,7 @@ const jsxNoUndef = defineRule({
|
|
|
11375
11382
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
11376
11383
|
context.report({
|
|
11377
11384
|
node: node.name,
|
|
11378
|
-
message: buildMessage$
|
|
11385
|
+
message: buildMessage$17(rootIdentifier)
|
|
11379
11386
|
});
|
|
11380
11387
|
} })
|
|
11381
11388
|
});
|
|
@@ -11474,7 +11481,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
11474
11481
|
});
|
|
11475
11482
|
//#endregion
|
|
11476
11483
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
11477
|
-
const buildMessage$
|
|
11484
|
+
const buildMessage$16 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
|
|
11478
11485
|
const resolveSettings$26 = (settings) => {
|
|
11479
11486
|
const reactDoctor = settings?.["react-doctor"];
|
|
11480
11487
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -11590,7 +11597,7 @@ const jsxPascalCase = defineRule({
|
|
|
11590
11597
|
if (!isPascal && !isAllCaps) {
|
|
11591
11598
|
context.report({
|
|
11592
11599
|
node,
|
|
11593
|
-
message: buildMessage$
|
|
11600
|
+
message: buildMessage$16(segment, settings.allowAllCaps)
|
|
11594
11601
|
});
|
|
11595
11602
|
return;
|
|
11596
11603
|
}
|
|
@@ -11785,7 +11792,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
11785
11792
|
category: "Accessibility",
|
|
11786
11793
|
create: (context) => {
|
|
11787
11794
|
const settings = resolveSettings$24(context.settings);
|
|
11788
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11795
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
11789
11796
|
return { JSXElement(node) {
|
|
11790
11797
|
if (isTestlikeFile) return;
|
|
11791
11798
|
const opening = node.openingElement;
|
|
@@ -12323,7 +12330,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12323
12330
|
severity: "warn",
|
|
12324
12331
|
recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
12325
12332
|
create: (context) => ({ Program(programNode) {
|
|
12326
|
-
const filename = normalizeFilename$1(context.
|
|
12333
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12327
12334
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12328
12335
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12329
12336
|
if (!programNode.body?.some((statement) => {
|
|
@@ -12388,7 +12395,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
12388
12395
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
12389
12396
|
const callback = getEffectCallback(node);
|
|
12390
12397
|
if (!callback || !containsFetchCall(callback)) return;
|
|
12391
|
-
const filename = normalizeFilename$1(context.
|
|
12398
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12392
12399
|
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
|
|
12393
12400
|
node,
|
|
12394
12401
|
message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
|
|
@@ -12421,7 +12428,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
12421
12428
|
severity: "warn",
|
|
12422
12429
|
recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
12423
12430
|
create: (context) => {
|
|
12424
|
-
const filename = normalizeFilename$1(context.
|
|
12431
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12425
12432
|
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
12426
12433
|
return { CallExpression(node) {
|
|
12427
12434
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -12490,7 +12497,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
12490
12497
|
recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
12491
12498
|
create: (context) => ({ ImportDeclaration(node) {
|
|
12492
12499
|
if (node.source?.value !== "next/head") return;
|
|
12493
|
-
const filename = normalizeFilename$1(context.
|
|
12500
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12494
12501
|
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
12495
12502
|
context.report({
|
|
12496
12503
|
node,
|
|
@@ -12507,7 +12514,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
12507
12514
|
severity: "warn",
|
|
12508
12515
|
recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
12509
12516
|
create: (context) => {
|
|
12510
|
-
const filename = normalizeFilename$1(context.
|
|
12517
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12511
12518
|
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12512
12519
|
return { JSXOpeningElement(node) {
|
|
12513
12520
|
if (isOgRoute) return;
|
|
@@ -12861,7 +12868,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12861
12868
|
resolveBinding = buildProgramBindingLookup(node);
|
|
12862
12869
|
},
|
|
12863
12870
|
ExportNamedDeclaration(node) {
|
|
12864
|
-
const filename = normalizeFilename$1(context.
|
|
12871
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12865
12872
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
12866
12873
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
12867
12874
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -13405,9 +13412,9 @@ const findContainingNode = (analysis, node) => {
|
|
|
13405
13412
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
13406
13413
|
const noAdjustStateOnPropChange = defineRule({
|
|
13407
13414
|
id: "no-adjust-state-on-prop-change",
|
|
13408
|
-
severity: "
|
|
13415
|
+
severity: "error",
|
|
13409
13416
|
tags: ["test-noise"],
|
|
13410
|
-
recommendation: "Adjust the state inline during render
|
|
13417
|
+
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
13411
13418
|
create: (context) => ({ CallExpression(node) {
|
|
13412
13419
|
if (!isUseEffect(node)) return;
|
|
13413
13420
|
const analysis = getProgramAnalysis(node);
|
|
@@ -13426,7 +13433,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13426
13433
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
13427
13434
|
context.report({
|
|
13428
13435
|
node: callExpr,
|
|
13429
|
-
message: "
|
|
13436
|
+
message: "State adjusted in a useEffect when a prop changes — forces an extra render with a stale UI between the two commits. Adjust the state during render with a `prev`-prop comparison instead, or refactor to remove the duplicated state."
|
|
13430
13437
|
});
|
|
13431
13438
|
}
|
|
13432
13439
|
} })
|
|
@@ -14093,7 +14100,7 @@ const noAutofocus = defineRule({
|
|
|
14093
14100
|
category: "Accessibility",
|
|
14094
14101
|
create: (context) => {
|
|
14095
14102
|
const settings = resolveSettings$21(context.settings);
|
|
14096
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
14103
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
14097
14104
|
return { JSXOpeningElement(node) {
|
|
14098
14105
|
if (isTestlikeFile) return;
|
|
14099
14106
|
const autoFocusAttribute = node.attributes.find((attribute) => {
|
|
@@ -14467,7 +14474,7 @@ const noBarrelImport = defineRule({
|
|
|
14467
14474
|
if (didReportForFile) return;
|
|
14468
14475
|
const source = node.source?.value;
|
|
14469
14476
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
14470
|
-
const filename = normalizeFilename$1(context.
|
|
14477
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
14471
14478
|
if (!filename) return;
|
|
14472
14479
|
const importRequests = getRuntimeImportRequests(node);
|
|
14473
14480
|
if (importRequests.length === 0) return;
|
|
@@ -15597,7 +15604,7 @@ const noDisabledZoom = defineRule({
|
|
|
15597
15604
|
});
|
|
15598
15605
|
//#endregion
|
|
15599
15606
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
15600
|
-
const buildMessage$
|
|
15607
|
+
const buildMessage$15 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
15601
15608
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
15602
15609
|
const resolveSettings$18 = (settings) => {
|
|
15603
15610
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -15617,7 +15624,7 @@ const noDistractingElements = defineRule({
|
|
|
15617
15624
|
const tag = getElementType(node, context.settings);
|
|
15618
15625
|
if (distractingTags.has(tag)) context.report({
|
|
15619
15626
|
node: node.name,
|
|
15620
|
-
message: buildMessage$
|
|
15627
|
+
message: buildMessage$15(tag)
|
|
15621
15628
|
});
|
|
15622
15629
|
} };
|
|
15623
15630
|
}
|
|
@@ -17458,7 +17465,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
17458
17465
|
});
|
|
17459
17466
|
//#endregion
|
|
17460
17467
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
17461
|
-
const buildMessage$
|
|
17468
|
+
const buildMessage$14 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
17462
17469
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
17463
17470
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
17464
17471
|
tr: ["none", "presentation"],
|
|
@@ -17502,7 +17509,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
17502
17509
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
17503
17510
|
context.report({
|
|
17504
17511
|
node: roleAttribute,
|
|
17505
|
-
message: buildMessage$
|
|
17512
|
+
message: buildMessage$14(elementType, firstRole)
|
|
17506
17513
|
});
|
|
17507
17514
|
} };
|
|
17508
17515
|
}
|
|
@@ -17946,7 +17953,7 @@ const noMoment = defineRule({
|
|
|
17946
17953
|
});
|
|
17947
17954
|
//#endregion
|
|
17948
17955
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17949
|
-
const buildMessage$
|
|
17956
|
+
const buildMessage$13 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17950
17957
|
const resolveSettings$16 = (settings) => {
|
|
17951
17958
|
const reactDoctor = settings?.["react-doctor"];
|
|
17952
17959
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -18246,7 +18253,7 @@ const noMultiComp = defineRule({
|
|
|
18246
18253
|
category: "Architecture",
|
|
18247
18254
|
create: (context) => {
|
|
18248
18255
|
const settings = resolveSettings$16(context.settings);
|
|
18249
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
18256
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
18250
18257
|
return { Program(node) {
|
|
18251
18258
|
if (isTestlikeFile) return;
|
|
18252
18259
|
const visitContext = {
|
|
@@ -18267,7 +18274,7 @@ const noMultiComp = defineRule({
|
|
|
18267
18274
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
18268
18275
|
for (const component of flagged.slice(1)) context.report({
|
|
18269
18276
|
node: component.reportNode,
|
|
18270
|
-
message: buildMessage$
|
|
18277
|
+
message: buildMessage$13(component.name)
|
|
18271
18278
|
});
|
|
18272
18279
|
} };
|
|
18273
18280
|
}
|
|
@@ -18628,7 +18635,7 @@ const noMutatingReducerState = defineRule({
|
|
|
18628
18635
|
});
|
|
18629
18636
|
//#endregion
|
|
18630
18637
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
18631
|
-
const buildMessage$
|
|
18638
|
+
const buildMessage$12 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
18632
18639
|
const noNamespace = defineRule({
|
|
18633
18640
|
id: "no-namespace",
|
|
18634
18641
|
severity: "warn",
|
|
@@ -18640,7 +18647,7 @@ const noNamespace = defineRule({
|
|
|
18640
18647
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
18641
18648
|
context.report({
|
|
18642
18649
|
node: namespaced,
|
|
18643
|
-
message: buildMessage$
|
|
18650
|
+
message: buildMessage$12(fullName)
|
|
18644
18651
|
});
|
|
18645
18652
|
},
|
|
18646
18653
|
CallExpression(node) {
|
|
@@ -18651,7 +18658,7 @@ const noNamespace = defineRule({
|
|
|
18651
18658
|
if (!firstArgument.value.includes(":")) return;
|
|
18652
18659
|
context.report({
|
|
18653
18660
|
node: firstArgument,
|
|
18654
|
-
message: buildMessage$
|
|
18661
|
+
message: buildMessage$12(firstArgument.value)
|
|
18655
18662
|
});
|
|
18656
18663
|
}
|
|
18657
18664
|
})
|
|
@@ -18695,7 +18702,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
18695
18702
|
});
|
|
18696
18703
|
//#endregion
|
|
18697
18704
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
18698
|
-
const buildMessage$
|
|
18705
|
+
const buildMessage$11 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
|
|
18699
18706
|
const INTERACTIVE_HANDLERS = [
|
|
18700
18707
|
"onClick",
|
|
18701
18708
|
"onMouseDown",
|
|
@@ -18721,13 +18728,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
18721
18728
|
}
|
|
18722
18729
|
context.report({
|
|
18723
18730
|
node: node.name,
|
|
18724
|
-
message: buildMessage$
|
|
18731
|
+
message: buildMessage$11(tag)
|
|
18725
18732
|
});
|
|
18726
18733
|
} })
|
|
18727
18734
|
});
|
|
18728
18735
|
//#endregion
|
|
18729
18736
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
18730
|
-
const buildMessage$
|
|
18737
|
+
const buildMessage$10 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
18731
18738
|
const DEFAULT_ALLOWED_ROLES = {
|
|
18732
18739
|
ul: [
|
|
18733
18740
|
"menu",
|
|
@@ -18791,7 +18798,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
18791
18798
|
if (!isInteractiveRole(firstRole)) return;
|
|
18792
18799
|
context.report({
|
|
18793
18800
|
node: roleAttribute,
|
|
18794
|
-
message: buildMessage$
|
|
18801
|
+
message: buildMessage$10(elementType, firstRole)
|
|
18795
18802
|
});
|
|
18796
18803
|
} };
|
|
18797
18804
|
}
|
|
@@ -19381,6 +19388,63 @@ const noPropCallbackInEffect = defineRule({
|
|
|
19381
19388
|
}
|
|
19382
19389
|
});
|
|
19383
19390
|
//#endregion
|
|
19391
|
+
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
19392
|
+
const PROP_TYPES_PROPERTY = "propTypes";
|
|
19393
|
+
const isPropTypesKey = (key, computed) => {
|
|
19394
|
+
if (!key) return false;
|
|
19395
|
+
if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
|
|
19396
|
+
return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
|
|
19397
|
+
};
|
|
19398
|
+
const getComponentNameFromPropTypesAssignment = (left) => {
|
|
19399
|
+
if (!isNodeOfType(left, "MemberExpression")) return null;
|
|
19400
|
+
if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
|
|
19401
|
+
if (!isNodeOfType(left.object, "Identifier")) return null;
|
|
19402
|
+
if (!isUppercaseName(left.object.name)) return null;
|
|
19403
|
+
return left.object.name;
|
|
19404
|
+
};
|
|
19405
|
+
const getComponentNameFromClassProperty = (node) => {
|
|
19406
|
+
if (!node.static) return null;
|
|
19407
|
+
if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
|
|
19408
|
+
const classBody = node.parent;
|
|
19409
|
+
if (!isNodeOfType(classBody, "ClassBody")) return null;
|
|
19410
|
+
const classNode = classBody.parent;
|
|
19411
|
+
if (!classNode) return null;
|
|
19412
|
+
if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
|
|
19413
|
+
if (!isNodeOfType(classNode, "ClassExpression")) return null;
|
|
19414
|
+
const declarator = classNode.parent;
|
|
19415
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
19416
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
19417
|
+
if (!isUppercaseName(declarator.id.name)) return null;
|
|
19418
|
+
return declarator.id.name;
|
|
19419
|
+
};
|
|
19420
|
+
const buildMessage$9 = (componentName) => `${componentName}.propTypes — React 19 no longer runs \`propTypes\` checks, so invalid props pass silently. Move the prop contract to TypeScript types and add explicit runtime validation only where data can actually be invalid`;
|
|
19421
|
+
const noPropTypes = defineRule({
|
|
19422
|
+
id: "no-prop-types",
|
|
19423
|
+
requires: ["react:19"],
|
|
19424
|
+
tags: ["test-noise"],
|
|
19425
|
+
severity: "warn",
|
|
19426
|
+
recommendation: "React 19 removed runtime `propTypes` validation — React no longer reads `Component.propTypes`, so invalid props pass silently. Describe props with TypeScript types and move any required runtime validation to explicit checks (or schema parsing) in component code. Only enabled on projects detected as React 19+.",
|
|
19427
|
+
create: (context) => ({
|
|
19428
|
+
AssignmentExpression(node) {
|
|
19429
|
+
if (node.operator !== "=") return;
|
|
19430
|
+
const componentName = getComponentNameFromPropTypesAssignment(node.left);
|
|
19431
|
+
if (!componentName) return;
|
|
19432
|
+
context.report({
|
|
19433
|
+
node: node.left,
|
|
19434
|
+
message: buildMessage$9(componentName)
|
|
19435
|
+
});
|
|
19436
|
+
},
|
|
19437
|
+
PropertyDefinition(node) {
|
|
19438
|
+
const componentName = getComponentNameFromClassProperty(node);
|
|
19439
|
+
if (!componentName) return;
|
|
19440
|
+
context.report({
|
|
19441
|
+
node: node.key,
|
|
19442
|
+
message: buildMessage$9(componentName)
|
|
19443
|
+
});
|
|
19444
|
+
}
|
|
19445
|
+
})
|
|
19446
|
+
});
|
|
19447
|
+
//#endregion
|
|
19384
19448
|
//#region src/plugin/rules/design/no-pure-black-background.ts
|
|
19385
19449
|
const noPureBlackBackground = defineRule({
|
|
19386
19450
|
id: "no-pure-black-background",
|
|
@@ -20193,7 +20257,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
20193
20257
|
severity: "warn",
|
|
20194
20258
|
recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
|
|
20195
20259
|
create: (context) => {
|
|
20196
|
-
const filename = normalizeFilename$1(context.
|
|
20260
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
20197
20261
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
20198
20262
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
20199
20263
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -20448,7 +20512,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20448
20512
|
category: "Accessibility",
|
|
20449
20513
|
create: (context) => {
|
|
20450
20514
|
const settings = resolveSettings$12(context.settings);
|
|
20451
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20515
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20452
20516
|
return { JSXOpeningElement(node) {
|
|
20453
20517
|
if (isTestlikeFile) return;
|
|
20454
20518
|
let hasNonBlockerHandler = false;
|
|
@@ -20525,7 +20589,7 @@ const noStringRefs = defineRule({
|
|
|
20525
20589
|
recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
|
|
20526
20590
|
create: (context) => {
|
|
20527
20591
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
20528
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20592
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20529
20593
|
return {
|
|
20530
20594
|
JSXAttribute(node) {
|
|
20531
20595
|
if (isTestlikeFile) return;
|
|
@@ -22660,7 +22724,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
22660
22724
|
};
|
|
22661
22725
|
const onlyExportComponents = defineRule({
|
|
22662
22726
|
id: "only-export-components",
|
|
22663
|
-
severity: "
|
|
22727
|
+
severity: "warn",
|
|
22664
22728
|
recommendation: "Move non-component exports out of files that export components.",
|
|
22665
22729
|
category: "Architecture",
|
|
22666
22730
|
create: (context) => {
|
|
@@ -22671,7 +22735,7 @@ const onlyExportComponents = defineRule({
|
|
|
22671
22735
|
allowConstantExport: settings.allowConstantExport
|
|
22672
22736
|
};
|
|
22673
22737
|
return { Program(node) {
|
|
22674
|
-
if (!isFileNameAllowed(
|
|
22738
|
+
if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
|
|
22675
22739
|
const allNodes = collectAllNodes(node);
|
|
22676
22740
|
const exports = [];
|
|
22677
22741
|
let hasReactExport = false;
|
|
@@ -23717,106 +23781,6 @@ const queryStableQueryClient = defineRule({
|
|
|
23717
23781
|
}
|
|
23718
23782
|
});
|
|
23719
23783
|
//#endregion
|
|
23720
|
-
//#region src/plugin/rules/architecture/react-compiler-destructure-method.ts
|
|
23721
|
-
const HOOK_OBJECTS_WITH_METHODS = new Map([
|
|
23722
|
-
["useRouter", new Set([
|
|
23723
|
-
"push",
|
|
23724
|
-
"replace",
|
|
23725
|
-
"back",
|
|
23726
|
-
"forward",
|
|
23727
|
-
"refresh",
|
|
23728
|
-
"prefetch"
|
|
23729
|
-
])],
|
|
23730
|
-
["useNavigation", new Set([
|
|
23731
|
-
"navigate",
|
|
23732
|
-
"push",
|
|
23733
|
-
"goBack",
|
|
23734
|
-
"popToTop",
|
|
23735
|
-
"reset",
|
|
23736
|
-
"replace",
|
|
23737
|
-
"dispatch"
|
|
23738
|
-
])],
|
|
23739
|
-
["useSearchParams", new Set([
|
|
23740
|
-
"get",
|
|
23741
|
-
"getAll",
|
|
23742
|
-
"has",
|
|
23743
|
-
"set"
|
|
23744
|
-
])]
|
|
23745
|
-
]);
|
|
23746
|
-
const HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING = new Map([["useNavigation", new Set(["@react-navigation/native", "@react-navigation/core"])]]);
|
|
23747
|
-
const isUnsafeMethodDestructureHookImport = (node, hookSource) => {
|
|
23748
|
-
const moduleSources = HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING.get(hookSource);
|
|
23749
|
-
if (!moduleSources) return false;
|
|
23750
|
-
for (const moduleSource of moduleSources) if (isImportedFromModule(node, hookSource, moduleSource)) return true;
|
|
23751
|
-
return false;
|
|
23752
|
-
};
|
|
23753
|
-
const buildHookBindingMap = (componentBody) => {
|
|
23754
|
-
const result = /* @__PURE__ */ new Map();
|
|
23755
|
-
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return result;
|
|
23756
|
-
for (const statement of componentBody.body ?? []) {
|
|
23757
|
-
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
23758
|
-
for (const declarator of statement.declarations ?? []) {
|
|
23759
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
23760
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
23761
|
-
const callee = declarator.init.callee;
|
|
23762
|
-
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
23763
|
-
result.set(declarator.id.name, callee.name);
|
|
23764
|
-
}
|
|
23765
|
-
}
|
|
23766
|
-
return result;
|
|
23767
|
-
};
|
|
23768
|
-
const reactCompilerDestructureMethod = defineRule({
|
|
23769
|
-
id: "react-compiler-destructure-method",
|
|
23770
|
-
tags: ["test-noise"],
|
|
23771
|
-
severity: "warn",
|
|
23772
|
-
recommendation: "Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoize",
|
|
23773
|
-
create: (context) => {
|
|
23774
|
-
const hookBindingMapStack = [];
|
|
23775
|
-
const isComponent = (node) => {
|
|
23776
|
-
if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id?.name && isUppercaseName(node.id.name));
|
|
23777
|
-
if (isNodeOfType(node, "VariableDeclarator")) return isComponentAssignment(node);
|
|
23778
|
-
return false;
|
|
23779
|
-
};
|
|
23780
|
-
const enter = (node) => {
|
|
23781
|
-
if (!isComponent(node)) return;
|
|
23782
|
-
let body;
|
|
23783
|
-
if (isNodeOfType(node, "FunctionDeclaration")) body = node.body;
|
|
23784
|
-
else if (isNodeOfType(node, "VariableDeclarator")) {
|
|
23785
|
-
const initializer = node.init;
|
|
23786
|
-
body = isInlineFunctionExpression(initializer) ? initializer.body : null;
|
|
23787
|
-
}
|
|
23788
|
-
hookBindingMapStack.push(buildHookBindingMap(body));
|
|
23789
|
-
};
|
|
23790
|
-
const exit = (node) => {
|
|
23791
|
-
if (isComponent(node)) hookBindingMapStack.pop();
|
|
23792
|
-
};
|
|
23793
|
-
return {
|
|
23794
|
-
FunctionDeclaration: enter,
|
|
23795
|
-
"FunctionDeclaration:exit": exit,
|
|
23796
|
-
VariableDeclarator: enter,
|
|
23797
|
-
"VariableDeclarator:exit": exit,
|
|
23798
|
-
MemberExpression(node) {
|
|
23799
|
-
if (hookBindingMapStack.length === 0) return;
|
|
23800
|
-
if (node.computed) return;
|
|
23801
|
-
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
23802
|
-
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
23803
|
-
const bindingName = node.object.name;
|
|
23804
|
-
const methodName = node.property.name;
|
|
23805
|
-
const hookSource = hookBindingMapStack[hookBindingMapStack.length - 1].get(bindingName);
|
|
23806
|
-
if (!hookSource) return;
|
|
23807
|
-
const allowedMethods = HOOK_OBJECTS_WITH_METHODS.get(hookSource);
|
|
23808
|
-
if (!allowedMethods || !allowedMethods.has(methodName)) return;
|
|
23809
|
-
if (isUnsafeMethodDestructureHookImport(node, hookSource)) return;
|
|
23810
|
-
if (!isNodeOfType(node.parent, "CallExpression") || node.parent.callee !== node) return;
|
|
23811
|
-
context.report({
|
|
23812
|
-
node,
|
|
23813
|
-
message: `Destructure for clarity: \`const { ${methodName} } = ${hookSource}()\` then call \`${methodName}(...)\` directly — easier for React Compiler to memoize and clearer about which methods this component depends on`
|
|
23814
|
-
});
|
|
23815
|
-
}
|
|
23816
|
-
};
|
|
23817
|
-
}
|
|
23818
|
-
});
|
|
23819
|
-
//#endregion
|
|
23820
23784
|
//#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
|
|
23821
23785
|
const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
23822
23786
|
["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
|
|
@@ -24192,7 +24156,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
24192
24156
|
category: "Performance",
|
|
24193
24157
|
recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
24194
24158
|
create: (context) => {
|
|
24195
|
-
const filename = context.
|
|
24159
|
+
const filename = context.filename;
|
|
24196
24160
|
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
|
|
24197
24161
|
return { JSXAttribute(node) {
|
|
24198
24162
|
if (isAutoGenerated) return;
|
|
@@ -25524,7 +25488,8 @@ const classifyPackagePlatform = (filename) => {
|
|
|
25524
25488
|
//#endregion
|
|
25525
25489
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
25526
25490
|
const isExpoManagedFileActive = (context) => {
|
|
25527
|
-
const
|
|
25491
|
+
const rawFilename = context.filename;
|
|
25492
|
+
const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
|
|
25528
25493
|
if (filename) {
|
|
25529
25494
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
25530
25495
|
if (packagePlatform === "expo") return true;
|
|
@@ -30692,7 +30657,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
30692
30657
|
let isServerSideFile = false;
|
|
30693
30658
|
return {
|
|
30694
30659
|
Program(node) {
|
|
30695
|
-
const filename = normalizeFilename$1(context.
|
|
30660
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30696
30661
|
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
30697
30662
|
isServerSideFile = false;
|
|
30698
30663
|
return;
|
|
@@ -30805,7 +30770,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
30805
30770
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
|
|
30806
30771
|
},
|
|
30807
30772
|
ExportDefaultDeclaration(node) {
|
|
30808
|
-
const filename = normalizeFilename$1(context.
|
|
30773
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30809
30774
|
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
30810
30775
|
const declaration = node.declaration;
|
|
30811
30776
|
if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
|
|
@@ -31356,7 +31321,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31356
31321
|
};
|
|
31357
31322
|
return {
|
|
31358
31323
|
Program(node) {
|
|
31359
|
-
const filename = context.
|
|
31324
|
+
const filename = context.filename ?? "";
|
|
31360
31325
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31361
31326
|
const statements = node.body ?? [];
|
|
31362
31327
|
for (const statement of statements) collectImportBindings(statement);
|
|
@@ -31366,17 +31331,17 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31366
31331
|
}
|
|
31367
31332
|
},
|
|
31368
31333
|
ImportDeclaration(node) {
|
|
31369
|
-
const filename = context.
|
|
31334
|
+
const filename = context.filename ?? "";
|
|
31370
31335
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31371
31336
|
collectImportBindings(node);
|
|
31372
31337
|
},
|
|
31373
31338
|
VariableDeclarator(node) {
|
|
31374
|
-
const filename = context.
|
|
31339
|
+
const filename = context.filename ?? "";
|
|
31375
31340
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31376
31341
|
collectVariableAlias(node);
|
|
31377
31342
|
},
|
|
31378
31343
|
JSXOpeningElement(node) {
|
|
31379
|
-
const filename = normalizeFilename$1(context.
|
|
31344
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31380
31345
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31381
31346
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
31382
31347
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -31391,7 +31356,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31391
31356
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
31392
31357
|
},
|
|
31393
31358
|
"Program:exit"(programNode) {
|
|
31394
|
-
const filename = normalizeFilename$1(context.
|
|
31359
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31395
31360
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31396
31361
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
31397
31362
|
node: programNode,
|
|
@@ -31410,7 +31375,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
31410
31375
|
severity: "warn",
|
|
31411
31376
|
recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
31412
31377
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31413
|
-
const filename = normalizeFilename$1(context.
|
|
31378
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31414
31379
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31415
31380
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
31416
31381
|
const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
|
|
@@ -31484,7 +31449,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31484
31449
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && typeof node.name.name === "string" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2));
|
|
31485
31450
|
return {
|
|
31486
31451
|
CallExpression(node) {
|
|
31487
|
-
const filename = normalizeFilename$1(context.
|
|
31452
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31488
31453
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31489
31454
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
31490
31455
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
@@ -31494,17 +31459,17 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31494
31459
|
});
|
|
31495
31460
|
},
|
|
31496
31461
|
"CallExpression:exit"(node) {
|
|
31497
|
-
const filename = normalizeFilename$1(context.
|
|
31462
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31498
31463
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31499
31464
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
31500
31465
|
},
|
|
31501
31466
|
JSXAttribute(node) {
|
|
31502
|
-
const filename = normalizeFilename$1(context.
|
|
31467
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31503
31468
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31504
31469
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
31505
31470
|
},
|
|
31506
31471
|
"JSXAttribute:exit"(node) {
|
|
31507
|
-
const filename = normalizeFilename$1(context.
|
|
31472
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31508
31473
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31509
31474
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
31510
31475
|
}
|
|
@@ -31585,7 +31550,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
31585
31550
|
severity: "warn",
|
|
31586
31551
|
recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
31587
31552
|
create: (context) => ({ CallExpression(node) {
|
|
31588
|
-
const filename = normalizeFilename$1(context.
|
|
31553
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31589
31554
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31590
31555
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
31591
31556
|
const callback = node.arguments?.[0];
|
|
@@ -33758,6 +33723,17 @@ const reactDoctorRules = [
|
|
|
33758
33723
|
category: "State & Effects"
|
|
33759
33724
|
}
|
|
33760
33725
|
},
|
|
33726
|
+
{
|
|
33727
|
+
key: "react-doctor/no-prop-types",
|
|
33728
|
+
id: "no-prop-types",
|
|
33729
|
+
source: "react-doctor",
|
|
33730
|
+
originallyExternal: false,
|
|
33731
|
+
rule: {
|
|
33732
|
+
...noPropTypes,
|
|
33733
|
+
framework: "global",
|
|
33734
|
+
category: "Architecture"
|
|
33735
|
+
}
|
|
33736
|
+
},
|
|
33761
33737
|
{
|
|
33762
33738
|
key: "react-doctor/no-pure-black-background",
|
|
33763
33739
|
id: "no-pure-black-background",
|
|
@@ -34308,17 +34284,6 @@ const reactDoctorRules = [
|
|
|
34308
34284
|
category: "TanStack Query"
|
|
34309
34285
|
}
|
|
34310
34286
|
},
|
|
34311
|
-
{
|
|
34312
|
-
key: "react-doctor/react-compiler-destructure-method",
|
|
34313
|
-
id: "react-compiler-destructure-method",
|
|
34314
|
-
source: "react-doctor",
|
|
34315
|
-
originallyExternal: false,
|
|
34316
|
-
rule: {
|
|
34317
|
-
...reactCompilerDestructureMethod,
|
|
34318
|
-
framework: "global",
|
|
34319
|
-
category: "Architecture"
|
|
34320
|
-
}
|
|
34321
|
-
},
|
|
34322
34287
|
{
|
|
34323
34288
|
key: "react-doctor/react-compiler-no-manual-memoization",
|
|
34324
34289
|
id: "react-compiler-no-manual-memoization",
|
|
@@ -35254,7 +35219,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
|
|
|
35254
35219
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
35255
35220
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
35256
35221
|
const isReactNativeFileActive = (context) => {
|
|
35257
|
-
const rawFilename = context.
|
|
35222
|
+
const rawFilename = context.filename;
|
|
35258
35223
|
if (!rawFilename) return true;
|
|
35259
35224
|
const filename = normalizeFilename$1(rawFilename);
|
|
35260
35225
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
@@ -35742,7 +35707,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
35742
35707
|
};
|
|
35743
35708
|
const enrichedContext = {
|
|
35744
35709
|
report: baseContext.report,
|
|
35745
|
-
|
|
35710
|
+
get filename() {
|
|
35711
|
+
return baseContext.filename ?? baseContext.getFilename?.();
|
|
35712
|
+
},
|
|
35746
35713
|
settings: baseContext.settings,
|
|
35747
35714
|
get scopes() {
|
|
35748
35715
|
return getScopes();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11-dev.d917f62",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|