oxlint-plugin-react-doctor 0.2.10 → 0.2.11-dev.d0f5206
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 +401 -20
- package/dist/index.js +2244 -594
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { analyze } from "eslint-scope";
|
|
3
3
|
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import { parseSync } from "oxc-parser";
|
|
5
6
|
//#region src/plugin/utils/is-testlike-filename.ts
|
|
6
7
|
const NON_PRODUCTION_PATH_SEGMENTS = [
|
|
7
8
|
"/test/",
|
|
@@ -223,7 +224,7 @@ const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
|
223
224
|
//#endregion
|
|
224
225
|
//#region src/plugin/utils/define-rule.ts
|
|
225
226
|
const wrapCreateForTestNoise = (create) => ((context) => {
|
|
226
|
-
if (isTestlikeFilename(context.
|
|
227
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
227
228
|
return create(context);
|
|
228
229
|
});
|
|
229
230
|
const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -444,6 +445,13 @@ const REACT_HOC_NAMES = new Set([
|
|
|
444
445
|
"React.memo",
|
|
445
446
|
"React.forwardRef"
|
|
446
447
|
]);
|
|
448
|
+
const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
|
|
449
|
+
const COMPONENT_HOC_WRAPPER_NAMES = new Set([
|
|
450
|
+
"memo",
|
|
451
|
+
"forwardRef",
|
|
452
|
+
"observer",
|
|
453
|
+
"lazy"
|
|
454
|
+
]);
|
|
447
455
|
const SUBSCRIPTION_METHOD_NAMES = new Set([
|
|
448
456
|
"subscribe",
|
|
449
457
|
"addEventListener",
|
|
@@ -813,6 +821,12 @@ const getElementType = (openingElement, settings) => {
|
|
|
813
821
|
return baseName;
|
|
814
822
|
};
|
|
815
823
|
//#endregion
|
|
824
|
+
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
825
|
+
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
826
|
+
if (!rawFilename) return false;
|
|
827
|
+
return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
|
|
828
|
+
};
|
|
829
|
+
//#endregion
|
|
816
830
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
817
831
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
818
832
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -983,6 +997,7 @@ const altText = defineRule({
|
|
|
983
997
|
recommendation: "Provide `alt` (or aria-label / aria-labelledby) for non-decorative images.",
|
|
984
998
|
category: "Accessibility",
|
|
985
999
|
create: (context) => {
|
|
1000
|
+
if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
|
|
986
1001
|
const settings = resolveSettings$53(context.settings);
|
|
987
1002
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
988
1003
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
@@ -1019,7 +1034,7 @@ const altText = defineRule({
|
|
|
1019
1034
|
});
|
|
1020
1035
|
//#endregion
|
|
1021
1036
|
//#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
|
|
1022
|
-
const buildMessage$
|
|
1037
|
+
const buildMessage$30 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
|
|
1023
1038
|
const DEFAULT_AMBIGUOUS = [
|
|
1024
1039
|
"click here",
|
|
1025
1040
|
"here",
|
|
@@ -1076,14 +1091,14 @@ const anchorAmbiguousText = defineRule({
|
|
|
1076
1091
|
const normalized = normalizeText(accessibleText);
|
|
1077
1092
|
if (ambiguousSet.has(normalized)) context.report({
|
|
1078
1093
|
node: node.openingElement.name,
|
|
1079
|
-
message: buildMessage$
|
|
1094
|
+
message: buildMessage$30(normalized)
|
|
1080
1095
|
});
|
|
1081
1096
|
} };
|
|
1082
1097
|
}
|
|
1083
1098
|
});
|
|
1084
1099
|
//#endregion
|
|
1085
1100
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1086
|
-
const MESSAGE$
|
|
1101
|
+
const MESSAGE$49 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
|
|
1087
1102
|
const anchorHasContent = defineRule({
|
|
1088
1103
|
id: "anchor-has-content",
|
|
1089
1104
|
tags: ["react-jsx-only"],
|
|
@@ -1098,7 +1113,7 @@ const anchorHasContent = defineRule({
|
|
|
1098
1113
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1099
1114
|
context.report({
|
|
1100
1115
|
node: opening.name,
|
|
1101
|
-
message: MESSAGE$
|
|
1116
|
+
message: MESSAGE$49
|
|
1102
1117
|
});
|
|
1103
1118
|
} })
|
|
1104
1119
|
});
|
|
@@ -1491,7 +1506,7 @@ const parseJsxValue = (value) => {
|
|
|
1491
1506
|
};
|
|
1492
1507
|
//#endregion
|
|
1493
1508
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
1494
|
-
const MESSAGE$
|
|
1509
|
+
const MESSAGE$48 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
|
|
1495
1510
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
1496
1511
|
id: "aria-activedescendant-has-tabindex",
|
|
1497
1512
|
tags: ["react-jsx-only"],
|
|
@@ -1508,14 +1523,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
1508
1523
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
1509
1524
|
context.report({
|
|
1510
1525
|
node: node.name,
|
|
1511
|
-
message: MESSAGE$
|
|
1526
|
+
message: MESSAGE$48
|
|
1512
1527
|
});
|
|
1513
1528
|
return;
|
|
1514
1529
|
}
|
|
1515
1530
|
if (isInteractiveElement(tag, node)) return;
|
|
1516
1531
|
context.report({
|
|
1517
1532
|
node: node.name,
|
|
1518
|
-
message: MESSAGE$
|
|
1533
|
+
message: MESSAGE$48
|
|
1519
1534
|
});
|
|
1520
1535
|
} })
|
|
1521
1536
|
});
|
|
@@ -1655,7 +1670,7 @@ const ARIA_PROPERTIES = new Map([
|
|
|
1655
1670
|
const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
|
|
1656
1671
|
//#endregion
|
|
1657
1672
|
//#region src/plugin/rules/a11y/aria-props.ts
|
|
1658
|
-
const buildMessage$
|
|
1673
|
+
const buildMessage$29 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
|
|
1659
1674
|
const ariaProps = defineRule({
|
|
1660
1675
|
id: "aria-props",
|
|
1661
1676
|
tags: ["react-jsx-only"],
|
|
@@ -1668,7 +1683,7 @@ const ariaProps = defineRule({
|
|
|
1668
1683
|
if (!name || !name.startsWith("aria-")) return;
|
|
1669
1684
|
if (!isValidAriaProperty(name)) context.report({
|
|
1670
1685
|
node: node.name,
|
|
1671
|
-
message: buildMessage$
|
|
1686
|
+
message: buildMessage$29(name)
|
|
1672
1687
|
});
|
|
1673
1688
|
} })
|
|
1674
1689
|
});
|
|
@@ -1819,7 +1834,7 @@ const buildExpectedDescription = (propType) => {
|
|
|
1819
1834
|
case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
|
|
1820
1835
|
}
|
|
1821
1836
|
};
|
|
1822
|
-
const buildMessage$
|
|
1837
|
+
const buildMessage$28 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
|
|
1823
1838
|
const allowNoneValue = (propType) => {
|
|
1824
1839
|
switch (propType.kind) {
|
|
1825
1840
|
case "boolean":
|
|
@@ -1952,13 +1967,13 @@ const ariaProptypes = defineRule({
|
|
|
1952
1967
|
if (!node.value) {
|
|
1953
1968
|
if (!allowNoneValue(propType)) context.report({
|
|
1954
1969
|
node,
|
|
1955
|
-
message: buildMessage$
|
|
1970
|
+
message: buildMessage$28(propName, propType)
|
|
1956
1971
|
});
|
|
1957
1972
|
return;
|
|
1958
1973
|
}
|
|
1959
1974
|
if (!isValidValueForType(propType, node.value)) context.report({
|
|
1960
1975
|
node,
|
|
1961
|
-
message: buildMessage$
|
|
1976
|
+
message: buildMessage$28(propName, propType)
|
|
1962
1977
|
});
|
|
1963
1978
|
} })
|
|
1964
1979
|
});
|
|
@@ -2270,7 +2285,7 @@ const ariaRole = defineRule({
|
|
|
2270
2285
|
});
|
|
2271
2286
|
//#endregion
|
|
2272
2287
|
//#region src/plugin/rules/a11y/aria-unsupported-elements.ts
|
|
2273
|
-
const buildMessage$
|
|
2288
|
+
const buildMessage$27 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
|
|
2274
2289
|
const ariaUnsupportedElements = defineRule({
|
|
2275
2290
|
id: "aria-unsupported-elements",
|
|
2276
2291
|
tags: ["react-jsx-only"],
|
|
@@ -2287,7 +2302,7 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2287
2302
|
if (!attrName) continue;
|
|
2288
2303
|
if (attrName.startsWith("aria-") || attrName === "role") context.report({
|
|
2289
2304
|
node: attribute,
|
|
2290
|
-
message: buildMessage$
|
|
2305
|
+
message: buildMessage$27(tag, attrName)
|
|
2291
2306
|
});
|
|
2292
2307
|
}
|
|
2293
2308
|
} })
|
|
@@ -2320,7 +2335,7 @@ const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
|
|
|
2320
2335
|
"BigInt",
|
|
2321
2336
|
"Reflect"
|
|
2322
2337
|
]);
|
|
2323
|
-
const MUTATING_ARRAY_METHODS
|
|
2338
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
2324
2339
|
"push",
|
|
2325
2340
|
"pop",
|
|
2326
2341
|
"shift",
|
|
@@ -2331,6 +2346,12 @@ const MUTATING_ARRAY_METHODS$1 = new Set([
|
|
|
2331
2346
|
"fill",
|
|
2332
2347
|
"copyWithin"
|
|
2333
2348
|
]);
|
|
2349
|
+
const MUTATING_COLLECTION_METHODS = new Set([
|
|
2350
|
+
"add",
|
|
2351
|
+
"clear",
|
|
2352
|
+
"delete",
|
|
2353
|
+
"set"
|
|
2354
|
+
]);
|
|
2334
2355
|
const CHAINABLE_ITERATION_METHODS = new Set([
|
|
2335
2356
|
"map",
|
|
2336
2357
|
"filter",
|
|
@@ -2542,7 +2563,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2542
2563
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2543
2564
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2544
2565
|
*/
|
|
2545
|
-
const isFunctionLike$
|
|
2566
|
+
const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2546
2567
|
//#endregion
|
|
2547
2568
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2548
2569
|
/**
|
|
@@ -2565,7 +2586,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2565
2586
|
let firstAwait = null;
|
|
2566
2587
|
walkAst(block, (child) => {
|
|
2567
2588
|
if (firstAwait) return false;
|
|
2568
|
-
if (child !== block && isFunctionLike$
|
|
2589
|
+
if (child !== block && isFunctionLike$2(child)) return false;
|
|
2569
2590
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2570
2591
|
});
|
|
2571
2592
|
return firstAwait;
|
|
@@ -2712,6 +2733,17 @@ const asyncAwaitInLoop = defineRule({
|
|
|
2712
2733
|
}
|
|
2713
2734
|
});
|
|
2714
2735
|
//#endregion
|
|
2736
|
+
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
2737
|
+
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
2738
|
+
"implements",
|
|
2739
|
+
"returnType",
|
|
2740
|
+
"superTypeArguments",
|
|
2741
|
+
"superTypeParameters",
|
|
2742
|
+
"typeAnnotation",
|
|
2743
|
+
"typeArguments",
|
|
2744
|
+
"typeParameters"
|
|
2745
|
+
]);
|
|
2746
|
+
//#endregion
|
|
2715
2747
|
//#region src/plugin/utils/collect-pattern-names.ts
|
|
2716
2748
|
const collectPatternNames = (pattern, into) => {
|
|
2717
2749
|
if (!pattern) return;
|
|
@@ -2741,14 +2773,6 @@ const collectPatternNames = (pattern, into) => {
|
|
|
2741
2773
|
};
|
|
2742
2774
|
//#endregion
|
|
2743
2775
|
//#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
2776
|
const collectScopedReferencesInPattern = (pattern, into, shadowed) => {
|
|
2753
2777
|
if (!pattern) return;
|
|
2754
2778
|
if (isNodeOfType(pattern, "Identifier")) return;
|
|
@@ -2809,7 +2833,7 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
2809
2833
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
2810
2834
|
for (const [key, child] of Object.entries(node)) {
|
|
2811
2835
|
if (key === "parent") continue;
|
|
2812
|
-
if (
|
|
2836
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
2813
2837
|
if (Array.isArray(child)) {
|
|
2814
2838
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
2815
2839
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -3017,13 +3041,13 @@ const asyncDeferAwait = defineRule({
|
|
|
3017
3041
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
3018
3042
|
if (!functionBody) return;
|
|
3019
3043
|
walkAst(functionBody, (descendant) => {
|
|
3020
|
-
if (isFunctionLike$
|
|
3044
|
+
if (isFunctionLike$2(descendant)) return false;
|
|
3021
3045
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
3022
3046
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
3023
3047
|
});
|
|
3024
3048
|
};
|
|
3025
3049
|
const enterFunction = (node) => {
|
|
3026
|
-
if (!isFunctionLike$
|
|
3050
|
+
if (!isFunctionLike$2(node)) return;
|
|
3027
3051
|
if (!node.async) return;
|
|
3028
3052
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
3029
3053
|
inspectAllStatementBlocks(node.body);
|
|
@@ -3127,7 +3151,7 @@ const asyncParallel = defineRule({
|
|
|
3127
3151
|
severity: "warn",
|
|
3128
3152
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
3129
3153
|
create: (context) => {
|
|
3130
|
-
const filename = normalizeFilename$1(context.
|
|
3154
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
3131
3155
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
3132
3156
|
let hasTestLibraryImport = false;
|
|
3133
3157
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -3154,7 +3178,7 @@ const asyncParallel = defineRule({
|
|
|
3154
3178
|
});
|
|
3155
3179
|
//#endregion
|
|
3156
3180
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
3157
|
-
const buildMessage$
|
|
3181
|
+
const buildMessage$26 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
|
|
3158
3182
|
const AUTOFILL_TOKENS = new Set([
|
|
3159
3183
|
"off",
|
|
3160
3184
|
"on",
|
|
@@ -3242,7 +3266,7 @@ const autocompleteValid = defineRule({
|
|
|
3242
3266
|
if (!AUTOFILL_TOKENS.has(token)) {
|
|
3243
3267
|
context.report({
|
|
3244
3268
|
node: attribute,
|
|
3245
|
-
message: buildMessage$
|
|
3269
|
+
message: buildMessage$26(value)
|
|
3246
3270
|
});
|
|
3247
3271
|
return;
|
|
3248
3272
|
}
|
|
@@ -3330,7 +3354,7 @@ const buttonHasType = defineRule({
|
|
|
3330
3354
|
recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
|
|
3331
3355
|
create: (context) => {
|
|
3332
3356
|
const settings = resolveSettings$48(context.settings);
|
|
3333
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3357
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3334
3358
|
return {
|
|
3335
3359
|
JSXOpeningElement(node) {
|
|
3336
3360
|
if (isTestlikeFile) return;
|
|
@@ -3517,7 +3541,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
3517
3541
|
//#endregion
|
|
3518
3542
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
3519
3543
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3520
|
-
const MESSAGE$
|
|
3544
|
+
const MESSAGE$47 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
|
|
3521
3545
|
const KEY_HANDLERS = [
|
|
3522
3546
|
"onKeyUp",
|
|
3523
3547
|
"onKeyDown",
|
|
@@ -3530,7 +3554,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3530
3554
|
recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
|
|
3531
3555
|
category: "Accessibility",
|
|
3532
3556
|
create: (context) => {
|
|
3533
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3557
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3534
3558
|
return { JSXOpeningElement(node) {
|
|
3535
3559
|
if (isTestlikeFile) return;
|
|
3536
3560
|
const tag = getElementType(node, context.settings);
|
|
@@ -3548,7 +3572,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3548
3572
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
3549
3573
|
context.report({
|
|
3550
3574
|
node: node.name,
|
|
3551
|
-
message: MESSAGE$
|
|
3575
|
+
message: MESSAGE$47
|
|
3552
3576
|
});
|
|
3553
3577
|
} };
|
|
3554
3578
|
}
|
|
@@ -3659,7 +3683,7 @@ const stripParenExpression = (node) => {
|
|
|
3659
3683
|
};
|
|
3660
3684
|
//#endregion
|
|
3661
3685
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3662
|
-
const MESSAGE$
|
|
3686
|
+
const MESSAGE$46 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3663
3687
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
3664
3688
|
const DEFAULT_LABELLING_PROPS = [
|
|
3665
3689
|
"alt",
|
|
@@ -3791,7 +3815,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3791
3815
|
category: "Accessibility",
|
|
3792
3816
|
create: (context) => {
|
|
3793
3817
|
const settings = resolveSettings$46(context.settings);
|
|
3794
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3818
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3795
3819
|
return { JSXElement(node) {
|
|
3796
3820
|
if (isTestlikeFile) return;
|
|
3797
3821
|
const opening = node.openingElement;
|
|
@@ -3819,7 +3843,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3819
3843
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
3820
3844
|
context.report({
|
|
3821
3845
|
node: opening,
|
|
3822
|
-
message: MESSAGE$
|
|
3846
|
+
message: MESSAGE$46
|
|
3823
3847
|
});
|
|
3824
3848
|
} };
|
|
3825
3849
|
}
|
|
@@ -4143,14 +4167,21 @@ const isEs6Component = (node) => {
|
|
|
4143
4167
|
};
|
|
4144
4168
|
//#endregion
|
|
4145
4169
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
4146
|
-
const MESSAGE$
|
|
4170
|
+
const MESSAGE$45 = "Component is missing a `displayName` — assign one for easier debugging.";
|
|
4171
|
+
const DEFAULT_ADDITIONAL_HOCS = [
|
|
4172
|
+
"observer",
|
|
4173
|
+
"lazy",
|
|
4174
|
+
"withTracking"
|
|
4175
|
+
];
|
|
4147
4176
|
const resolveSettings$45 = (settings) => {
|
|
4148
4177
|
const reactDoctor = settings?.["react-doctor"];
|
|
4149
4178
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
4179
|
+
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
4150
4180
|
return {
|
|
4151
4181
|
ignoreTranspilerName: ruleSettings.ignoreTranspilerName ?? false,
|
|
4152
4182
|
checkContextObjects: ruleSettings.checkContextObjects ?? false,
|
|
4153
|
-
reactVersion: ruleSettings.reactVersion ?? ""
|
|
4183
|
+
reactVersion: ruleSettings.reactVersion ?? "",
|
|
4184
|
+
additionalHoCs
|
|
4154
4185
|
};
|
|
4155
4186
|
};
|
|
4156
4187
|
const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
@@ -4223,29 +4254,28 @@ const isCreateContextCall = (node) => {
|
|
|
4223
4254
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
4224
4255
|
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
|
|
4225
4256
|
};
|
|
4226
|
-
const isObserverCall = (node) => {
|
|
4227
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
4228
|
-
const callee = node.callee;
|
|
4229
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name === "observer";
|
|
4230
|
-
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "observer";
|
|
4231
|
-
};
|
|
4232
|
-
const getCallName = (node) => {
|
|
4233
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
4234
|
-
const callee = node.callee;
|
|
4235
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
4236
|
-
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee);
|
|
4237
|
-
return null;
|
|
4238
|
-
};
|
|
4239
4257
|
const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
|
|
4240
4258
|
const firstCallArgument = (node) => {
|
|
4241
4259
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
4242
4260
|
const first = node.arguments[0];
|
|
4243
4261
|
return first ? first : null;
|
|
4244
4262
|
};
|
|
4245
|
-
const
|
|
4246
|
-
|
|
4247
|
-
|
|
4263
|
+
const resolveHoCCalleeName = (node, additionalHoCs) => {
|
|
4264
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
4265
|
+
const callee = node.callee;
|
|
4266
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
4267
|
+
if (callee.name === "memo" || callee.name === "forwardRef") return callee.name;
|
|
4268
|
+
if (additionalHoCs.has(callee.name)) return callee.name;
|
|
4269
|
+
return null;
|
|
4270
|
+
}
|
|
4271
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
4272
|
+
const propertyName = callee.property.name;
|
|
4273
|
+
if (propertyName === "memo" || propertyName === "forwardRef") return propertyName;
|
|
4274
|
+
if (additionalHoCs.has(propertyName)) return propertyName;
|
|
4275
|
+
}
|
|
4276
|
+
return null;
|
|
4248
4277
|
};
|
|
4278
|
+
const isDisplayNameHoC = (node, additionalHoCs) => resolveHoCCalleeName(node, additionalHoCs) !== null;
|
|
4249
4279
|
const supportsComposedForwardRefDisplayName = (version) => {
|
|
4250
4280
|
if (!version) return false;
|
|
4251
4281
|
if (isReactVersionAtLeast$1(version, 15, 7)) return true;
|
|
@@ -4253,17 +4283,17 @@ const supportsComposedForwardRefDisplayName = (version) => {
|
|
|
4253
4283
|
return Boolean(match && Number(match[1]) >= 11);
|
|
4254
4284
|
};
|
|
4255
4285
|
const shouldReportHoCDisplayName = (node, settings) => {
|
|
4256
|
-
if (!isDisplayNameHoC(node)) return false;
|
|
4286
|
+
if (!isDisplayNameHoC(node, settings.additionalHoCs)) return false;
|
|
4257
4287
|
if (!containsJsx$1(node)) return false;
|
|
4258
4288
|
const assignedName = getAssignedName(node);
|
|
4259
4289
|
const programRoot = findProgramRoot(node);
|
|
4260
4290
|
if (assignedName && programRoot && hasDisplayNameAssignment(assignedName, programRoot)) return false;
|
|
4261
|
-
const callName =
|
|
4291
|
+
const callName = resolveHoCCalleeName(node, settings.additionalHoCs);
|
|
4262
4292
|
const firstArgument = firstCallArgument(node);
|
|
4263
4293
|
if (!firstArgument) return false;
|
|
4264
|
-
if (callName === "forwardRef" && isNodeOfType(node.parent, "CallExpression") &&
|
|
4294
|
+
if (callName === "forwardRef" && isNodeOfType(node.parent, "CallExpression") && resolveHoCCalleeName(node.parent, settings.additionalHoCs) === "memo" && firstCallArgument(node.parent) === node && supportsComposedForwardRefDisplayName(settings.reactVersion)) return false;
|
|
4265
4295
|
if (callName === "memo" && isNodeOfType(firstArgument, "CallExpression")) {
|
|
4266
|
-
if (
|
|
4296
|
+
if (resolveHoCCalleeName(firstArgument, settings.additionalHoCs) !== "forwardRef") return false;
|
|
4267
4297
|
return !supportsComposedForwardRefDisplayName(settings.reactVersion);
|
|
4268
4298
|
}
|
|
4269
4299
|
if (isNamedFunctionLike(firstArgument)) return false;
|
|
@@ -4339,7 +4369,7 @@ const displayName = defineRule({
|
|
|
4339
4369
|
const reportAt = (node) => {
|
|
4340
4370
|
context.report({
|
|
4341
4371
|
node,
|
|
4342
|
-
message: MESSAGE$
|
|
4372
|
+
message: MESSAGE$45
|
|
4343
4373
|
});
|
|
4344
4374
|
};
|
|
4345
4375
|
return {
|
|
@@ -4417,10 +4447,6 @@ const displayName = defineRule({
|
|
|
4417
4447
|
reportAt(node);
|
|
4418
4448
|
return;
|
|
4419
4449
|
}
|
|
4420
|
-
if (isObserverCall(node) && containsJsx$1(node)) {
|
|
4421
|
-
reportAt(node);
|
|
4422
|
-
return;
|
|
4423
|
-
}
|
|
4424
4450
|
if (shouldReportHoCDisplayName(node, settings)) {
|
|
4425
4451
|
reportAt(node);
|
|
4426
4452
|
return;
|
|
@@ -4462,7 +4488,7 @@ const displayName = defineRule({
|
|
|
4462
4488
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4463
4489
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4464
4490
|
if (!node || typeof node !== "object") return;
|
|
4465
|
-
if (isFunctionLike$
|
|
4491
|
+
if (isFunctionLike$2(node)) return;
|
|
4466
4492
|
visitor(node);
|
|
4467
4493
|
const nodeRecord = node;
|
|
4468
4494
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4550,7 +4576,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4550
4576
|
let didFindRelease = false;
|
|
4551
4577
|
walkAst(node, (child) => {
|
|
4552
4578
|
if (didFindRelease) return false;
|
|
4553
|
-
if (child !== node && isFunctionLike$
|
|
4579
|
+
if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4554
4580
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4555
4581
|
didFindRelease = true;
|
|
4556
4582
|
return false;
|
|
@@ -4559,7 +4585,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4559
4585
|
return didFindRelease;
|
|
4560
4586
|
};
|
|
4561
4587
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4562
|
-
if (!isFunctionLike$
|
|
4588
|
+
if (!isFunctionLike$2(node)) return false;
|
|
4563
4589
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4564
4590
|
};
|
|
4565
4591
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
@@ -4801,7 +4827,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4801
4827
|
};
|
|
4802
4828
|
const isFunctionBodyBlock = (block) => {
|
|
4803
4829
|
if (!block.parent) return false;
|
|
4804
|
-
return isFunctionLike$
|
|
4830
|
+
return isFunctionLike$2(block.parent);
|
|
4805
4831
|
};
|
|
4806
4832
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4807
4833
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -4928,8 +4954,35 @@ const inferReferenceFlag = (identifier) => {
|
|
|
4928
4954
|
const setNodeScope = (node, state) => {
|
|
4929
4955
|
state.nodeScope.set(node, state.currentScope);
|
|
4930
4956
|
};
|
|
4957
|
+
const walkParameterReferences = (pattern, state) => {
|
|
4958
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
4959
|
+
walkParameterReferences(pattern.left, state);
|
|
4960
|
+
const defaultValue = pattern.right ?? null;
|
|
4961
|
+
if (defaultValue) walk(defaultValue, state);
|
|
4962
|
+
return;
|
|
4963
|
+
}
|
|
4964
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
4965
|
+
for (const property of pattern.properties) {
|
|
4966
|
+
const propertyNode = property;
|
|
4967
|
+
if (isNodeOfType(propertyNode, "RestElement")) {
|
|
4968
|
+
walkParameterReferences(propertyNode.argument, state);
|
|
4969
|
+
continue;
|
|
4970
|
+
}
|
|
4971
|
+
if (!isNodeOfType(propertyNode, "Property")) continue;
|
|
4972
|
+
const propertyDetail = propertyNode;
|
|
4973
|
+
if (propertyDetail.computed) walk(propertyDetail.key, state);
|
|
4974
|
+
walkParameterReferences(propertyDetail.value, state);
|
|
4975
|
+
}
|
|
4976
|
+
return;
|
|
4977
|
+
}
|
|
4978
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
4979
|
+
for (const element of pattern.elements) if (element) walkParameterReferences(element, state);
|
|
4980
|
+
return;
|
|
4981
|
+
}
|
|
4982
|
+
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
4983
|
+
};
|
|
4931
4984
|
const walk = (node, state) => {
|
|
4932
|
-
if (isFunctionLike$
|
|
4985
|
+
if (isFunctionLike$2(node)) {
|
|
4933
4986
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
4934
4987
|
setNodeScope(node, state);
|
|
4935
4988
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -4943,7 +4996,9 @@ const walk = (node, state) => {
|
|
|
4943
4996
|
});
|
|
4944
4997
|
tagAsBinding(state, node.id);
|
|
4945
4998
|
}
|
|
4946
|
-
|
|
4999
|
+
const functionParams = node.params ?? [];
|
|
5000
|
+
handleFunctionParameters(functionParams, fnScope, state);
|
|
5001
|
+
for (const param of functionParams) walkParameterReferences(param, state);
|
|
4947
5002
|
const body = node.body;
|
|
4948
5003
|
if (body) walk(body, state);
|
|
4949
5004
|
popScope(state);
|
|
@@ -4985,6 +5040,7 @@ const walk = (node, state) => {
|
|
|
4985
5040
|
const nodeRecord = node;
|
|
4986
5041
|
for (const key of Object.keys(nodeRecord)) {
|
|
4987
5042
|
if (key === "parent") continue;
|
|
5043
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4988
5044
|
const child = nodeRecord[key];
|
|
4989
5045
|
if (Array.isArray(child)) {
|
|
4990
5046
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5047,6 +5103,7 @@ const walk = (node, state) => {
|
|
|
5047
5103
|
const nodeRecord = node;
|
|
5048
5104
|
for (const key of Object.keys(nodeRecord)) {
|
|
5049
5105
|
if (key === "parent") continue;
|
|
5106
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5050
5107
|
const child = nodeRecord[key];
|
|
5051
5108
|
if (Array.isArray(child)) {
|
|
5052
5109
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5166,20 +5223,12 @@ const isAstDescendant = (inner, outer) => {
|
|
|
5166
5223
|
};
|
|
5167
5224
|
//#endregion
|
|
5168
5225
|
//#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
5226
|
const closureCaptures = (functionNode, scopes) => {
|
|
5178
5227
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
5179
5228
|
const out = [];
|
|
5180
5229
|
const seen = /* @__PURE__ */ new Set();
|
|
5181
5230
|
const visit = (node) => {
|
|
5182
|
-
if (node !== functionNode && isFunctionLike$
|
|
5231
|
+
if (node !== functionNode && isFunctionLike$2(node)) {
|
|
5183
5232
|
const innerCaptures = closureCaptures(node, scopes);
|
|
5184
5233
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
5185
5234
|
if (!seen.has(reference.id)) {
|
|
@@ -5201,7 +5250,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5201
5250
|
const record = node;
|
|
5202
5251
|
for (const key of Object.keys(record)) {
|
|
5203
5252
|
if (key === "parent") continue;
|
|
5204
|
-
if (
|
|
5253
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5205
5254
|
const child = record[key];
|
|
5206
5255
|
if (Array.isArray(child)) {
|
|
5207
5256
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
@@ -5251,7 +5300,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
5251
5300
|
"ParenthesizedExpression",
|
|
5252
5301
|
"ChainExpression"
|
|
5253
5302
|
]);
|
|
5254
|
-
const unwrapExpression = (node) => {
|
|
5303
|
+
const unwrapExpression$1 = (node) => {
|
|
5255
5304
|
let current = node;
|
|
5256
5305
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
5257
5306
|
const inner = current.expression;
|
|
@@ -5358,7 +5407,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
5358
5407
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
5359
5408
|
const initializerRaw = declarator.init;
|
|
5360
5409
|
if (!initializerRaw) return false;
|
|
5361
|
-
const initializer = unwrapExpression(initializerRaw);
|
|
5410
|
+
const initializer = unwrapExpression$1(initializerRaw);
|
|
5362
5411
|
if (symbol.kind === "const") {
|
|
5363
5412
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
5364
5413
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -5378,13 +5427,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
5378
5427
|
return false;
|
|
5379
5428
|
};
|
|
5380
5429
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
5381
|
-
const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
|
|
5430
|
+
const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
|
|
5382
5431
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
5383
5432
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
5384
5433
|
};
|
|
5385
5434
|
const getFunctionValueNode = (symbol) => {
|
|
5386
5435
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
5387
|
-
const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
|
|
5436
|
+
const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
|
|
5388
5437
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
5389
5438
|
return null;
|
|
5390
5439
|
};
|
|
@@ -5519,23 +5568,23 @@ const computeDepKey = (reference) => {
|
|
|
5519
5568
|
return fullName;
|
|
5520
5569
|
};
|
|
5521
5570
|
const computeDeclaredDepKey = (entry) => {
|
|
5522
|
-
const stripped = unwrapExpression(entry);
|
|
5571
|
+
const stripped = unwrapExpression$1(entry);
|
|
5523
5572
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
5524
5573
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
5525
5574
|
return null;
|
|
5526
5575
|
};
|
|
5527
5576
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
5528
5577
|
if (!depsArgument) return false;
|
|
5529
|
-
const strippedDepsArgument = unwrapExpression(depsArgument);
|
|
5578
|
+
const strippedDepsArgument = unwrapExpression$1(depsArgument);
|
|
5530
5579
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
5531
5580
|
return strippedDepsArgument.elements.some((element) => {
|
|
5532
5581
|
if (!element) return false;
|
|
5533
|
-
const strippedElement = unwrapExpression(element);
|
|
5582
|
+
const strippedElement = unwrapExpression$1(element);
|
|
5534
5583
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
5535
5584
|
});
|
|
5536
5585
|
};
|
|
5537
5586
|
const stringifyMemberChain = (node) => {
|
|
5538
|
-
const stripped = unwrapExpression(node);
|
|
5587
|
+
const stripped = unwrapExpression$1(node);
|
|
5539
5588
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
5540
5589
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
5541
5590
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -5564,33 +5613,6 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
5564
5613
|
if (!depKey) continue;
|
|
5565
5614
|
keys.add(depKey);
|
|
5566
5615
|
}
|
|
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
5616
|
return {
|
|
5595
5617
|
keys,
|
|
5596
5618
|
stableCapturedNames
|
|
@@ -5604,13 +5626,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
5604
5626
|
return false;
|
|
5605
5627
|
};
|
|
5606
5628
|
const getMemberRootIdentifier = (node) => {
|
|
5607
|
-
const stripped = unwrapExpression(node);
|
|
5629
|
+
const stripped = unwrapExpression$1(node);
|
|
5608
5630
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
5609
5631
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
5610
5632
|
return null;
|
|
5611
5633
|
};
|
|
5612
5634
|
const hasComputedMemberExpression = (node) => {
|
|
5613
|
-
const stripped = unwrapExpression(node);
|
|
5635
|
+
const stripped = unwrapExpression$1(node);
|
|
5614
5636
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
5615
5637
|
if (stripped.computed) return true;
|
|
5616
5638
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -5631,7 +5653,7 @@ const isRegExpLiteral = (node) => {
|
|
|
5631
5653
|
};
|
|
5632
5654
|
const isUnstableInitializer = (node) => {
|
|
5633
5655
|
if (!node) return false;
|
|
5634
|
-
const stripped = unwrapExpression(node);
|
|
5656
|
+
const stripped = unwrapExpression$1(node);
|
|
5635
5657
|
if (isRegExpLiteral(stripped)) return true;
|
|
5636
5658
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
5637
5659
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -5787,7 +5809,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
5787
5809
|
const visit = (current) => {
|
|
5788
5810
|
if (didFindMemberCall) return;
|
|
5789
5811
|
if (isNodeOfType(current, "CallExpression")) {
|
|
5790
|
-
if (getMemberRootIdentifier(unwrapExpression(current.callee))?.name === rootName) {
|
|
5812
|
+
if (getMemberRootIdentifier(unwrapExpression$1(current.callee))?.name === rootName) {
|
|
5791
5813
|
didFindMemberCall = true;
|
|
5792
5814
|
return;
|
|
5793
5815
|
}
|
|
@@ -5849,7 +5871,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5849
5871
|
return;
|
|
5850
5872
|
}
|
|
5851
5873
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
5852
|
-
const callbackExpression = unwrapExpression(callbackArgument);
|
|
5874
|
+
const callbackExpression = unwrapExpression$1(callbackArgument);
|
|
5853
5875
|
let callbackToAnalyze = null;
|
|
5854
5876
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
5855
5877
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -5857,7 +5879,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5857
5879
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
5858
5880
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
5859
5881
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
5860
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
5882
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$1(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
5861
5883
|
else if (depsArgumentRaw) {
|
|
5862
5884
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
5863
5885
|
context.report({
|
|
@@ -5914,7 +5936,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5914
5936
|
});
|
|
5915
5937
|
return;
|
|
5916
5938
|
}
|
|
5917
|
-
const depsArgument = unwrapExpression(depsArgumentRaw);
|
|
5939
|
+
const depsArgument = unwrapExpression$1(depsArgumentRaw);
|
|
5918
5940
|
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
5919
5941
|
if (isAutoDependenciesHook(hookName)) return;
|
|
5920
5942
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
@@ -5955,11 +5977,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5955
5977
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
5956
5978
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
5957
5979
|
if (!element) return false;
|
|
5958
|
-
return isLiteralOrEmptyTemplate(unwrapExpression(element));
|
|
5980
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$1(element));
|
|
5959
5981
|
});
|
|
5960
5982
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
5961
5983
|
if (!element) return false;
|
|
5962
|
-
return isNonStringLiteral(unwrapExpression(element));
|
|
5984
|
+
return isNonStringLiteral(unwrapExpression$1(element));
|
|
5963
5985
|
});
|
|
5964
5986
|
if (hasNonStringLiteralDep) context.report({
|
|
5965
5987
|
node: depsArgument,
|
|
@@ -5980,7 +6002,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5980
6002
|
});
|
|
5981
6003
|
continue;
|
|
5982
6004
|
}
|
|
5983
|
-
const stripped = unwrapExpression(elementNode);
|
|
6005
|
+
const stripped = unwrapExpression$1(elementNode);
|
|
5984
6006
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
5985
6007
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
5986
6008
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -6200,7 +6222,7 @@ const flattenJsxName$1 = (name) => {
|
|
|
6200
6222
|
return "";
|
|
6201
6223
|
};
|
|
6202
6224
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
6203
|
-
const buildMessage$
|
|
6225
|
+
const buildMessage$25 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
|
|
6204
6226
|
const forbidComponentProps = defineRule({
|
|
6205
6227
|
id: "forbid-component-props",
|
|
6206
6228
|
severity: "warn",
|
|
@@ -6226,7 +6248,7 @@ const forbidComponentProps = defineRule({
|
|
|
6226
6248
|
if (!isForbiddenForTag(entry, tag)) continue;
|
|
6227
6249
|
context.report({
|
|
6228
6250
|
node: attribute,
|
|
6229
|
-
message: buildMessage$
|
|
6251
|
+
message: buildMessage$25(propName, entry.message)
|
|
6230
6252
|
});
|
|
6231
6253
|
break;
|
|
6232
6254
|
}
|
|
@@ -6236,7 +6258,7 @@ const forbidComponentProps = defineRule({
|
|
|
6236
6258
|
});
|
|
6237
6259
|
//#endregion
|
|
6238
6260
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6239
|
-
const buildMessage$
|
|
6261
|
+
const buildMessage$24 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
|
|
6240
6262
|
const resolveSettings$43 = (settings) => {
|
|
6241
6263
|
const reactDoctor = settings?.["react-doctor"];
|
|
6242
6264
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
@@ -6274,7 +6296,7 @@ const forbidDomProps = defineRule({
|
|
|
6274
6296
|
if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
|
|
6275
6297
|
context.report({
|
|
6276
6298
|
node: attribute.name,
|
|
6277
|
-
message: buildMessage$
|
|
6299
|
+
message: buildMessage$24(propName, descriptor.message)
|
|
6278
6300
|
});
|
|
6279
6301
|
}
|
|
6280
6302
|
} };
|
|
@@ -6344,7 +6366,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6344
6366
|
};
|
|
6345
6367
|
//#endregion
|
|
6346
6368
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6347
|
-
const buildMessage$
|
|
6369
|
+
const buildMessage$23 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
|
|
6348
6370
|
const resolveSettings$42 = (settings) => {
|
|
6349
6371
|
const reactDoctor = settings?.["react-doctor"];
|
|
6350
6372
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
@@ -6368,7 +6390,7 @@ const forbidElements = defineRule({
|
|
|
6368
6390
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6369
6391
|
context.report({
|
|
6370
6392
|
node: node.name,
|
|
6371
|
-
message: buildMessage$
|
|
6393
|
+
message: buildMessage$23(fullName, forbidMap.get(fullName))
|
|
6372
6394
|
});
|
|
6373
6395
|
},
|
|
6374
6396
|
CallExpression(node) {
|
|
@@ -6388,7 +6410,7 @@ const forbidElements = defineRule({
|
|
|
6388
6410
|
if (!elementName || !forbidMap.has(elementName)) return;
|
|
6389
6411
|
context.report({
|
|
6390
6412
|
node: firstArgument,
|
|
6391
|
-
message: buildMessage$
|
|
6413
|
+
message: buildMessage$23(elementName, forbidMap.get(elementName))
|
|
6392
6414
|
});
|
|
6393
6415
|
}
|
|
6394
6416
|
};
|
|
@@ -6396,7 +6418,7 @@ const forbidElements = defineRule({
|
|
|
6396
6418
|
});
|
|
6397
6419
|
//#endregion
|
|
6398
6420
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
6399
|
-
const MESSAGE$
|
|
6421
|
+
const MESSAGE$44 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
|
|
6400
6422
|
const forwardRefUsesRef = defineRule({
|
|
6401
6423
|
id: "forward-ref-uses-ref",
|
|
6402
6424
|
severity: "warn",
|
|
@@ -6415,13 +6437,13 @@ const forwardRefUsesRef = defineRule({
|
|
|
6415
6437
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
6416
6438
|
context.report({
|
|
6417
6439
|
node: inner,
|
|
6418
|
-
message: MESSAGE$
|
|
6440
|
+
message: MESSAGE$44
|
|
6419
6441
|
});
|
|
6420
6442
|
} })
|
|
6421
6443
|
});
|
|
6422
6444
|
//#endregion
|
|
6423
6445
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
6424
|
-
const MESSAGE$
|
|
6446
|
+
const MESSAGE$43 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
|
|
6425
6447
|
const DEFAULT_HEADING_TAGS = [
|
|
6426
6448
|
"h1",
|
|
6427
6449
|
"h2",
|
|
@@ -6453,7 +6475,7 @@ const headingHasContent = defineRule({
|
|
|
6453
6475
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6454
6476
|
context.report({
|
|
6455
6477
|
node,
|
|
6456
|
-
message: MESSAGE$
|
|
6478
|
+
message: MESSAGE$43
|
|
6457
6479
|
});
|
|
6458
6480
|
} };
|
|
6459
6481
|
}
|
|
@@ -6589,7 +6611,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
6589
6611
|
});
|
|
6590
6612
|
//#endregion
|
|
6591
6613
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6592
|
-
const MESSAGE$
|
|
6614
|
+
const MESSAGE$42 = "`<html>` element must have a non-empty `lang` attribute.";
|
|
6593
6615
|
const resolveSettings$39 = (settings) => {
|
|
6594
6616
|
const reactDoctor = settings?.["react-doctor"];
|
|
6595
6617
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -6636,7 +6658,7 @@ const htmlHasLang = defineRule({
|
|
|
6636
6658
|
if (!lang) {
|
|
6637
6659
|
context.report({
|
|
6638
6660
|
node: node.name,
|
|
6639
|
-
message: MESSAGE$
|
|
6661
|
+
message: MESSAGE$42
|
|
6640
6662
|
});
|
|
6641
6663
|
return;
|
|
6642
6664
|
}
|
|
@@ -6644,13 +6666,13 @@ const htmlHasLang = defineRule({
|
|
|
6644
6666
|
if (verdict === "missing" || verdict === "empty") {
|
|
6645
6667
|
context.report({
|
|
6646
6668
|
node: lang,
|
|
6647
|
-
message: MESSAGE$
|
|
6669
|
+
message: MESSAGE$42
|
|
6648
6670
|
});
|
|
6649
6671
|
return;
|
|
6650
6672
|
}
|
|
6651
6673
|
if (hasSpread && !lang) context.report({
|
|
6652
6674
|
node: node.name,
|
|
6653
|
-
message: MESSAGE$
|
|
6675
|
+
message: MESSAGE$42
|
|
6654
6676
|
});
|
|
6655
6677
|
} };
|
|
6656
6678
|
}
|
|
@@ -6690,7 +6712,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
|
|
|
6690
6712
|
"table",
|
|
6691
6713
|
"ul"
|
|
6692
6714
|
]);
|
|
6693
|
-
const buildMessage$
|
|
6715
|
+
const buildMessage$22 = (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
6716
|
const isParagraphElement = (candidate) => {
|
|
6695
6717
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6696
6718
|
const opening = candidate.openingElement;
|
|
@@ -6718,7 +6740,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
6718
6740
|
if (!findEnclosingParagraph(node)) return;
|
|
6719
6741
|
context.report({
|
|
6720
6742
|
node: node.name,
|
|
6721
|
-
message: buildMessage$
|
|
6743
|
+
message: buildMessage$22(childTagName)
|
|
6722
6744
|
});
|
|
6723
6745
|
} })
|
|
6724
6746
|
});
|
|
@@ -6738,7 +6760,7 @@ const ROW_GROUPS = new Set([
|
|
|
6738
6760
|
"tbody",
|
|
6739
6761
|
"tfoot"
|
|
6740
6762
|
]);
|
|
6741
|
-
const buildMessage$
|
|
6763
|
+
const buildMessage$21 = (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
6764
|
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
6765
|
const getHostTagName = (jsxElement) => {
|
|
6744
6766
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
@@ -6806,28 +6828,28 @@ const htmlNoInvalidTableNesting = defineRule({
|
|
|
6806
6828
|
if (ROW_GROUPS.has(tagName)) {
|
|
6807
6829
|
if (actualParent !== "table") context.report({
|
|
6808
6830
|
node: node.openingElement.name,
|
|
6809
|
-
message: buildMessage$
|
|
6831
|
+
message: buildMessage$21(tagName, "`<table>`", actualParent)
|
|
6810
6832
|
});
|
|
6811
6833
|
return;
|
|
6812
6834
|
}
|
|
6813
6835
|
if (tagName === "tr") {
|
|
6814
6836
|
if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
|
|
6815
6837
|
node: node.openingElement.name,
|
|
6816
|
-
message: buildMessage$
|
|
6838
|
+
message: buildMessage$21(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
|
|
6817
6839
|
});
|
|
6818
6840
|
return;
|
|
6819
6841
|
}
|
|
6820
6842
|
if (tagName === "td" || tagName === "th") {
|
|
6821
6843
|
if (actualParent !== "tr") context.report({
|
|
6822
6844
|
node: node.openingElement.name,
|
|
6823
|
-
message: buildMessage$
|
|
6845
|
+
message: buildMessage$21(tagName, "`<tr>`", actualParent)
|
|
6824
6846
|
});
|
|
6825
6847
|
}
|
|
6826
6848
|
} })
|
|
6827
6849
|
});
|
|
6828
6850
|
//#endregion
|
|
6829
6851
|
//#region src/plugin/rules/correctness/html-no-nested-interactive.ts
|
|
6830
|
-
const buildMessage$
|
|
6852
|
+
const buildMessage$20 = (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
6853
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
6832
6854
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6833
6855
|
const opening = candidate.openingElement;
|
|
@@ -6855,13 +6877,13 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
6855
6877
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
6856
6878
|
context.report({
|
|
6857
6879
|
node: node.name,
|
|
6858
|
-
message: buildMessage$
|
|
6880
|
+
message: buildMessage$20(tagName)
|
|
6859
6881
|
});
|
|
6860
6882
|
} })
|
|
6861
6883
|
});
|
|
6862
6884
|
//#endregion
|
|
6863
6885
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
6864
|
-
const MESSAGE$
|
|
6886
|
+
const MESSAGE$41 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
|
|
6865
6887
|
const evaluateTitleValue = (value) => {
|
|
6866
6888
|
if (!value) return "missing";
|
|
6867
6889
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -6900,14 +6922,14 @@ const iframeHasTitle = defineRule({
|
|
|
6900
6922
|
if (!titleAttr) {
|
|
6901
6923
|
if (hasSpread || tag === "iframe") context.report({
|
|
6902
6924
|
node: node.name,
|
|
6903
|
-
message: MESSAGE$
|
|
6925
|
+
message: MESSAGE$41
|
|
6904
6926
|
});
|
|
6905
6927
|
return;
|
|
6906
6928
|
}
|
|
6907
6929
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
6908
6930
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
6909
6931
|
node: titleAttr,
|
|
6910
|
-
message: MESSAGE$
|
|
6932
|
+
message: MESSAGE$41
|
|
6911
6933
|
});
|
|
6912
6934
|
} })
|
|
6913
6935
|
});
|
|
@@ -7010,7 +7032,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
7010
7032
|
});
|
|
7011
7033
|
//#endregion
|
|
7012
7034
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
7013
|
-
const MESSAGE$
|
|
7035
|
+
const MESSAGE$40 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
|
|
7014
7036
|
const DEFAULT_COMPONENTS = ["img"];
|
|
7015
7037
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
7016
7038
|
"image",
|
|
@@ -7072,7 +7094,7 @@ const imgRedundantAlt = defineRule({
|
|
|
7072
7094
|
if (!altAttribute) return;
|
|
7073
7095
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
7074
7096
|
node: altAttribute,
|
|
7075
|
-
message: MESSAGE$
|
|
7097
|
+
message: MESSAGE$40
|
|
7076
7098
|
});
|
|
7077
7099
|
} };
|
|
7078
7100
|
}
|
|
@@ -7267,7 +7289,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
7267
7289
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7268
7290
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7269
7291
|
};
|
|
7270
|
-
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7292
|
+
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7271
7293
|
const getFirstParameterName = (fn) => {
|
|
7272
7294
|
const parameters = fn.params ?? [];
|
|
7273
7295
|
if (parameters.length !== 1) return null;
|
|
@@ -7385,7 +7407,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7385
7407
|
const args = node.arguments ?? [];
|
|
7386
7408
|
if (args.length === 0) return;
|
|
7387
7409
|
const reader = args[0];
|
|
7388
|
-
if (!isFunctionLike(reader)) return;
|
|
7410
|
+
if (!isFunctionLike$1(reader)) return;
|
|
7389
7411
|
const getParameterName = getFirstParameterName(reader);
|
|
7390
7412
|
if (!getParameterName) return;
|
|
7391
7413
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -7401,7 +7423,6 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7401
7423
|
//#endregion
|
|
7402
7424
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
7403
7425
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
7404
|
-
const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
|
|
7405
7426
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
7406
7427
|
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7407
7428
|
const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
@@ -7639,7 +7660,7 @@ const isInsideLoopContext = (node) => {
|
|
|
7639
7660
|
let current = node.parent;
|
|
7640
7661
|
while (current) {
|
|
7641
7662
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
7642
|
-
if (isFunctionLike$
|
|
7663
|
+
if (isFunctionLike$2(current)) {
|
|
7643
7664
|
if (isIteratorCallback(current)) return true;
|
|
7644
7665
|
return false;
|
|
7645
7666
|
}
|
|
@@ -7993,7 +8014,7 @@ const jsHoistIntl = defineRule({
|
|
|
7993
8014
|
let cursor = node.parent ?? null;
|
|
7994
8015
|
let inFunctionBody = false;
|
|
7995
8016
|
while (cursor) {
|
|
7996
|
-
if (isFunctionLike$
|
|
8017
|
+
if (isFunctionLike$2(cursor)) {
|
|
7997
8018
|
inFunctionBody = true;
|
|
7998
8019
|
const fnParent = cursor.parent;
|
|
7999
8020
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -8423,6 +8444,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8423
8444
|
id: "js-tosorted-immutable",
|
|
8424
8445
|
tags: ["test-noise"],
|
|
8425
8446
|
severity: "warn",
|
|
8447
|
+
disabledBy: ["react-native"],
|
|
8426
8448
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
8427
8449
|
create: (context) => ({ CallExpression(node) {
|
|
8428
8450
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -8711,7 +8733,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
8711
8733
|
const settings = resolveSettings$34(context.settings);
|
|
8712
8734
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8713
8735
|
const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
|
|
8714
|
-
const filename =
|
|
8736
|
+
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8715
8737
|
const extensionOnly = path.extname(filename).slice(1);
|
|
8716
8738
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
8717
8739
|
let didReportMismatch = false;
|
|
@@ -9337,7 +9359,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
9337
9359
|
};
|
|
9338
9360
|
//#endregion
|
|
9339
9361
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9340
|
-
const buildMessage$
|
|
9362
|
+
const buildMessage$19 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
9341
9363
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9342
9364
|
const resolveSettings$30 = (settings) => {
|
|
9343
9365
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -9404,7 +9426,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9404
9426
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
9405
9427
|
if (total > max) context.report({
|
|
9406
9428
|
node,
|
|
9407
|
-
message: buildMessage$
|
|
9429
|
+
message: buildMessage$19(total, max)
|
|
9408
9430
|
});
|
|
9409
9431
|
};
|
|
9410
9432
|
return {
|
|
@@ -9419,7 +9441,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9419
9441
|
});
|
|
9420
9442
|
//#endregion
|
|
9421
9443
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
9422
|
-
const MESSAGE$
|
|
9444
|
+
const MESSAGE$39 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
|
|
9423
9445
|
const LITERAL_TEXT_TAGS = new Set([
|
|
9424
9446
|
"code",
|
|
9425
9447
|
"pre",
|
|
@@ -9454,11 +9476,20 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
9454
9476
|
if (isInsideLiteralTextTag(node)) return;
|
|
9455
9477
|
context.report({
|
|
9456
9478
|
node,
|
|
9457
|
-
message: MESSAGE$
|
|
9479
|
+
message: MESSAGE$39
|
|
9458
9480
|
});
|
|
9459
9481
|
} })
|
|
9460
9482
|
});
|
|
9461
9483
|
//#endregion
|
|
9484
|
+
//#region src/plugin/utils/is-canonical-react-namespace-name.ts
|
|
9485
|
+
const isCanonicalReactNamespaceName = (namespaceName) => {
|
|
9486
|
+
if (namespaceName === "React") return true;
|
|
9487
|
+
if (namespaceName === "react") return true;
|
|
9488
|
+
if (namespaceName.startsWith("_react")) return true;
|
|
9489
|
+
if (namespaceName.startsWith("_React")) return true;
|
|
9490
|
+
return false;
|
|
9491
|
+
};
|
|
9492
|
+
//#endregion
|
|
9462
9493
|
//#region src/plugin/utils/is-inside-function-scope.ts
|
|
9463
9494
|
const FUNCTION_NODE_TYPES$1 = new Set([
|
|
9464
9495
|
"FunctionDeclaration",
|
|
@@ -9476,7 +9507,12 @@ const isInsideFunctionScope = (node) => {
|
|
|
9476
9507
|
};
|
|
9477
9508
|
//#endregion
|
|
9478
9509
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
9479
|
-
const MESSAGE$
|
|
9510
|
+
const MESSAGE$38 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
|
|
9511
|
+
const CONTEXT_MODULES$1 = [
|
|
9512
|
+
"react",
|
|
9513
|
+
"use-context-selector",
|
|
9514
|
+
"react-tracked"
|
|
9515
|
+
];
|
|
9480
9516
|
const isConstructedValue = (expression) => {
|
|
9481
9517
|
const stripped = stripParenExpression(expression);
|
|
9482
9518
|
if (isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "NewExpression") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment")) return true;
|
|
@@ -9484,10 +9520,57 @@ const isConstructedValue = (expression) => {
|
|
|
9484
9520
|
if (isNodeOfType(stripped, "LogicalExpression")) return isConstructedValue(stripped.left) || isConstructedValue(stripped.right);
|
|
9485
9521
|
return false;
|
|
9486
9522
|
};
|
|
9487
|
-
const
|
|
9523
|
+
const isProviderMemberName = (node) => {
|
|
9488
9524
|
if (!isNodeOfType(node, "JSXMemberExpression")) return false;
|
|
9489
9525
|
return node.property.name === "Provider";
|
|
9490
9526
|
};
|
|
9527
|
+
const isCreateContextCallExpression = (expression) => {
|
|
9528
|
+
const stripped = stripParenExpression(expression);
|
|
9529
|
+
if (!isNodeOfType(stripped, "CallExpression")) return false;
|
|
9530
|
+
const callee = stripped.callee;
|
|
9531
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
9532
|
+
for (const moduleName of CONTEXT_MODULES$1) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
|
|
9533
|
+
return false;
|
|
9534
|
+
}
|
|
9535
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
9536
|
+
const namespaceIdentifier = callee.object;
|
|
9537
|
+
const propertyIdentifier = callee.property;
|
|
9538
|
+
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
|
|
9539
|
+
if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
|
|
9540
|
+
if (propertyIdentifier.name !== "createContext") return false;
|
|
9541
|
+
if (isCanonicalReactNamespaceName(namespaceIdentifier.name)) return true;
|
|
9542
|
+
for (const moduleName of CONTEXT_MODULES$1) {
|
|
9543
|
+
if (getImportedNameFromModule(namespaceIdentifier, namespaceIdentifier.name, moduleName) === null) continue;
|
|
9544
|
+
return true;
|
|
9545
|
+
}
|
|
9546
|
+
}
|
|
9547
|
+
return false;
|
|
9548
|
+
};
|
|
9549
|
+
const collectContextBindings = (programRoot) => {
|
|
9550
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
9551
|
+
if (!isNodeOfType(programRoot, "Program")) return bindings;
|
|
9552
|
+
for (const topLevel of programRoot.body ?? []) {
|
|
9553
|
+
let declaration = topLevel;
|
|
9554
|
+
if (isNodeOfType(topLevel, "ExportNamedDeclaration") && topLevel.declaration) declaration = topLevel.declaration;
|
|
9555
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) continue;
|
|
9556
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
9557
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
9558
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
9559
|
+
if (!declarator.init) continue;
|
|
9560
|
+
if (!isAstNode(declarator.init)) continue;
|
|
9561
|
+
if (!isCreateContextCallExpression(declarator.init)) continue;
|
|
9562
|
+
bindings.add(declarator.id.name);
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
return bindings;
|
|
9566
|
+
};
|
|
9567
|
+
const isCreateContextBindingJsxName = (node, contextBindings) => {
|
|
9568
|
+
if (!isNodeOfType(node, "JSXIdentifier")) return false;
|
|
9569
|
+
if (!contextBindings.has(node.name)) return false;
|
|
9570
|
+
const binding = findVariableInitializer(node, node.name);
|
|
9571
|
+
if (!binding) return false;
|
|
9572
|
+
return binding.scopeOwner.type === "Program";
|
|
9573
|
+
};
|
|
9491
9574
|
const jsxNoConstructedContextValues = defineRule({
|
|
9492
9575
|
id: "jsx-no-constructed-context-values",
|
|
9493
9576
|
tags: ["react-jsx-only"],
|
|
@@ -9495,27 +9578,36 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9495
9578
|
recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
|
|
9496
9579
|
category: "Performance",
|
|
9497
9580
|
create: (context) => {
|
|
9498
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
if (
|
|
9506
|
-
|
|
9507
|
-
const
|
|
9508
|
-
|
|
9509
|
-
if (!
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9581
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9582
|
+
let contextBindings = /* @__PURE__ */ new Set();
|
|
9583
|
+
return {
|
|
9584
|
+
Program(node) {
|
|
9585
|
+
contextBindings = collectContextBindings(node);
|
|
9586
|
+
},
|
|
9587
|
+
JSXOpeningElement(node) {
|
|
9588
|
+
if (isTestlikeFile) return;
|
|
9589
|
+
const nameNode = node.name;
|
|
9590
|
+
const isLegacyProvider = isProviderMemberName(nameNode);
|
|
9591
|
+
const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings);
|
|
9592
|
+
if (!isLegacyProvider && !isReact19Shorthand) return;
|
|
9593
|
+
if (!isInsideFunctionScope(node)) return;
|
|
9594
|
+
for (const attribute of node.attributes) {
|
|
9595
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
9596
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
9597
|
+
if (attribute.name.name !== "value") continue;
|
|
9598
|
+
const attributeValue = attribute.value;
|
|
9599
|
+
if (!attributeValue) continue;
|
|
9600
|
+
if (!isNodeOfType(attributeValue, "JSXExpressionContainer")) continue;
|
|
9601
|
+
const innerExpression = attributeValue.expression;
|
|
9602
|
+
if (!innerExpression || innerExpression.type === "JSXEmptyExpression") continue;
|
|
9603
|
+
if (!isConstructedValue(innerExpression)) continue;
|
|
9604
|
+
context.report({
|
|
9605
|
+
node: attribute,
|
|
9606
|
+
message: MESSAGE$38
|
|
9607
|
+
});
|
|
9608
|
+
}
|
|
9517
9609
|
}
|
|
9518
|
-
}
|
|
9610
|
+
};
|
|
9519
9611
|
}
|
|
9520
9612
|
});
|
|
9521
9613
|
//#endregion
|
|
@@ -9541,7 +9633,8 @@ const jsxNoDuplicateProps = defineRule({
|
|
|
9541
9633
|
//#endregion
|
|
9542
9634
|
//#region src/plugin/utils/build-same-file-memo-registry.ts
|
|
9543
9635
|
const HOC_NAMES_FOR_MEMOISATION = new Set([
|
|
9544
|
-
|
|
9636
|
+
"memo",
|
|
9637
|
+
"React.memo",
|
|
9545
9638
|
"observer",
|
|
9546
9639
|
"observable",
|
|
9547
9640
|
"lazy",
|
|
@@ -9595,7 +9688,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
9595
9688
|
};
|
|
9596
9689
|
//#endregion
|
|
9597
9690
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
9598
|
-
const MESSAGE$
|
|
9691
|
+
const MESSAGE$37 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
|
|
9599
9692
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
9600
9693
|
"icon",
|
|
9601
9694
|
"Icon",
|
|
@@ -9841,7 +9934,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
9841
9934
|
recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
|
|
9842
9935
|
category: "Performance",
|
|
9843
9936
|
create: (context) => {
|
|
9844
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9937
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9845
9938
|
let memoRegistry = null;
|
|
9846
9939
|
return {
|
|
9847
9940
|
Program(node) {
|
|
@@ -9863,7 +9956,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
9863
9956
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
9864
9957
|
context.report({
|
|
9865
9958
|
node,
|
|
9866
|
-
message: MESSAGE$
|
|
9959
|
+
message: MESSAGE$37
|
|
9867
9960
|
});
|
|
9868
9961
|
}
|
|
9869
9962
|
};
|
|
@@ -10151,7 +10244,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
10151
10244
|
];
|
|
10152
10245
|
//#endregion
|
|
10153
10246
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
10154
|
-
const MESSAGE$
|
|
10247
|
+
const MESSAGE$36 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
|
|
10155
10248
|
const isDataArrayPropName = (propName) => {
|
|
10156
10249
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
10157
10250
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10212,7 +10305,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10212
10305
|
recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
|
|
10213
10306
|
category: "Performance",
|
|
10214
10307
|
create: (context) => {
|
|
10215
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10308
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10216
10309
|
let memoRegistry = null;
|
|
10217
10310
|
return {
|
|
10218
10311
|
Program(node) {
|
|
@@ -10234,7 +10327,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10234
10327
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
10235
10328
|
context.report({
|
|
10236
10329
|
node,
|
|
10237
|
-
message: MESSAGE$
|
|
10330
|
+
message: MESSAGE$36
|
|
10238
10331
|
});
|
|
10239
10332
|
}
|
|
10240
10333
|
};
|
|
@@ -10492,7 +10585,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
10492
10585
|
]);
|
|
10493
10586
|
//#endregion
|
|
10494
10587
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
10495
|
-
const MESSAGE$
|
|
10588
|
+
const MESSAGE$35 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
|
|
10496
10589
|
const isAccessorPredicateName = (propName) => {
|
|
10497
10590
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
10498
10591
|
if (propName.length <= prefix.length) continue;
|
|
@@ -10674,7 +10767,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10674
10767
|
recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
|
|
10675
10768
|
category: "Performance",
|
|
10676
10769
|
create: (context) => {
|
|
10677
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10770
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10678
10771
|
let memoRegistry = null;
|
|
10679
10772
|
return {
|
|
10680
10773
|
Program(node) {
|
|
@@ -10697,7 +10790,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10697
10790
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
10698
10791
|
context.report({
|
|
10699
10792
|
node,
|
|
10700
|
-
message: MESSAGE$
|
|
10793
|
+
message: MESSAGE$35
|
|
10701
10794
|
});
|
|
10702
10795
|
}
|
|
10703
10796
|
};
|
|
@@ -10917,7 +11010,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
10917
11010
|
];
|
|
10918
11011
|
//#endregion
|
|
10919
11012
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
10920
|
-
const MESSAGE$
|
|
11013
|
+
const MESSAGE$34 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
|
|
10921
11014
|
const isConfigObjectPropName = (propName) => {
|
|
10922
11015
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
10923
11016
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10980,7 +11073,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10980
11073
|
recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
|
|
10981
11074
|
category: "Performance",
|
|
10982
11075
|
create: (context) => {
|
|
10983
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11076
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10984
11077
|
let memoRegistry = null;
|
|
10985
11078
|
return {
|
|
10986
11079
|
Program(node) {
|
|
@@ -11004,7 +11097,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11004
11097
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
11005
11098
|
context.report({
|
|
11006
11099
|
node,
|
|
11007
|
-
message: MESSAGE$
|
|
11100
|
+
message: MESSAGE$34
|
|
11008
11101
|
});
|
|
11009
11102
|
}
|
|
11010
11103
|
};
|
|
@@ -11012,7 +11105,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11012
11105
|
});
|
|
11013
11106
|
//#endregion
|
|
11014
11107
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
11015
|
-
const MESSAGE$
|
|
11108
|
+
const MESSAGE$33 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
|
|
11016
11109
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
11017
11110
|
const resolveSettings$29 = (settings) => {
|
|
11018
11111
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -11052,7 +11145,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11052
11145
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
11053
11146
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
11054
11147
|
node: attribute,
|
|
11055
|
-
message: MESSAGE$
|
|
11148
|
+
message: MESSAGE$33
|
|
11056
11149
|
});
|
|
11057
11150
|
}
|
|
11058
11151
|
} };
|
|
@@ -11340,7 +11433,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
11340
11433
|
});
|
|
11341
11434
|
//#endregion
|
|
11342
11435
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11343
|
-
const buildMessage$
|
|
11436
|
+
const buildMessage$18 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
11344
11437
|
const KNOWN_GLOBALS = new Set([
|
|
11345
11438
|
"globalThis",
|
|
11346
11439
|
"window",
|
|
@@ -11375,7 +11468,7 @@ const jsxNoUndef = defineRule({
|
|
|
11375
11468
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
11376
11469
|
context.report({
|
|
11377
11470
|
node: node.name,
|
|
11378
|
-
message: buildMessage$
|
|
11471
|
+
message: buildMessage$18(rootIdentifier)
|
|
11379
11472
|
});
|
|
11380
11473
|
} })
|
|
11381
11474
|
});
|
|
@@ -11474,7 +11567,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
11474
11567
|
});
|
|
11475
11568
|
//#endregion
|
|
11476
11569
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
11477
|
-
const buildMessage$
|
|
11570
|
+
const buildMessage$17 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
|
|
11478
11571
|
const resolveSettings$26 = (settings) => {
|
|
11479
11572
|
const reactDoctor = settings?.["react-doctor"];
|
|
11480
11573
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -11590,7 +11683,7 @@ const jsxPascalCase = defineRule({
|
|
|
11590
11683
|
if (!isPascal && !isAllCaps) {
|
|
11591
11684
|
context.report({
|
|
11592
11685
|
node,
|
|
11593
|
-
message: buildMessage$
|
|
11686
|
+
message: buildMessage$17(segment, settings.allowAllCaps)
|
|
11594
11687
|
});
|
|
11595
11688
|
return;
|
|
11596
11689
|
}
|
|
@@ -11642,7 +11735,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
11642
11735
|
});
|
|
11643
11736
|
//#endregion
|
|
11644
11737
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
11645
|
-
const MESSAGE$
|
|
11738
|
+
const MESSAGE$32 = "JSX prop spreading is forbidden — list each prop explicitly.";
|
|
11646
11739
|
const resolveSettings$25 = (settings) => {
|
|
11647
11740
|
const reactDoctor = settings?.["react-doctor"];
|
|
11648
11741
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -11682,7 +11775,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11682
11775
|
}
|
|
11683
11776
|
context.report({
|
|
11684
11777
|
node: attribute,
|
|
11685
|
-
message: MESSAGE$
|
|
11778
|
+
message: MESSAGE$32
|
|
11686
11779
|
});
|
|
11687
11780
|
}
|
|
11688
11781
|
} };
|
|
@@ -11785,7 +11878,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
11785
11878
|
category: "Accessibility",
|
|
11786
11879
|
create: (context) => {
|
|
11787
11880
|
const settings = resolveSettings$24(context.settings);
|
|
11788
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11881
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
11789
11882
|
return { JSXElement(node) {
|
|
11790
11883
|
if (isTestlikeFile) return;
|
|
11791
11884
|
const opening = node.openingElement;
|
|
@@ -11837,7 +11930,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
11837
11930
|
});
|
|
11838
11931
|
//#endregion
|
|
11839
11932
|
//#region src/plugin/rules/a11y/lang.ts
|
|
11840
|
-
const MESSAGE$
|
|
11933
|
+
const MESSAGE$31 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
|
|
11841
11934
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
11842
11935
|
"aa",
|
|
11843
11936
|
"ab",
|
|
@@ -12048,7 +12141,7 @@ const lang = defineRule({
|
|
|
12048
12141
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
12049
12142
|
context.report({
|
|
12050
12143
|
node: langAttr,
|
|
12051
|
-
message: MESSAGE$
|
|
12144
|
+
message: MESSAGE$31
|
|
12052
12145
|
});
|
|
12053
12146
|
return;
|
|
12054
12147
|
}
|
|
@@ -12057,13 +12150,13 @@ const lang = defineRule({
|
|
|
12057
12150
|
if (value === null) return;
|
|
12058
12151
|
if (!isValidLangTag(value)) context.report({
|
|
12059
12152
|
node: langAttr,
|
|
12060
|
-
message: MESSAGE$
|
|
12153
|
+
message: MESSAGE$31
|
|
12061
12154
|
});
|
|
12062
12155
|
} })
|
|
12063
12156
|
});
|
|
12064
12157
|
//#endregion
|
|
12065
12158
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
12066
|
-
const MESSAGE$
|
|
12159
|
+
const MESSAGE$30 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
|
|
12067
12160
|
const DEFAULT_AUDIO = ["audio"];
|
|
12068
12161
|
const DEFAULT_VIDEO = ["video"];
|
|
12069
12162
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -12103,7 +12196,7 @@ const mediaHasCaption = defineRule({
|
|
|
12103
12196
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
12104
12197
|
context.report({
|
|
12105
12198
|
node: node.name,
|
|
12106
|
-
message: MESSAGE$
|
|
12199
|
+
message: MESSAGE$30
|
|
12107
12200
|
});
|
|
12108
12201
|
return;
|
|
12109
12202
|
}
|
|
@@ -12120,7 +12213,7 @@ const mediaHasCaption = defineRule({
|
|
|
12120
12213
|
return kindValue.value.toLowerCase() === "captions";
|
|
12121
12214
|
})) context.report({
|
|
12122
12215
|
node: node.name,
|
|
12123
|
-
message: MESSAGE$
|
|
12216
|
+
message: MESSAGE$30
|
|
12124
12217
|
});
|
|
12125
12218
|
} };
|
|
12126
12219
|
}
|
|
@@ -12323,7 +12416,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12323
12416
|
severity: "warn",
|
|
12324
12417
|
recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
12325
12418
|
create: (context) => ({ Program(programNode) {
|
|
12326
|
-
const filename = normalizeFilename$1(context.
|
|
12419
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12327
12420
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12328
12421
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12329
12422
|
if (!programNode.body?.some((statement) => {
|
|
@@ -12388,7 +12481,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
12388
12481
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
12389
12482
|
const callback = getEffectCallback(node);
|
|
12390
12483
|
if (!callback || !containsFetchCall(callback)) return;
|
|
12391
|
-
const filename = normalizeFilename$1(context.
|
|
12484
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12392
12485
|
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
|
|
12393
12486
|
node,
|
|
12394
12487
|
message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
|
|
@@ -12421,7 +12514,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
12421
12514
|
severity: "warn",
|
|
12422
12515
|
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
12516
|
create: (context) => {
|
|
12424
|
-
const filename = normalizeFilename$1(context.
|
|
12517
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12425
12518
|
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
12426
12519
|
return { CallExpression(node) {
|
|
12427
12520
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -12490,7 +12583,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
12490
12583
|
recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
12491
12584
|
create: (context) => ({ ImportDeclaration(node) {
|
|
12492
12585
|
if (node.source?.value !== "next/head") return;
|
|
12493
|
-
const filename = normalizeFilename$1(context.
|
|
12586
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12494
12587
|
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
12495
12588
|
context.report({
|
|
12496
12589
|
node,
|
|
@@ -12507,7 +12600,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
12507
12600
|
severity: "warn",
|
|
12508
12601
|
recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
12509
12602
|
create: (context) => {
|
|
12510
|
-
const filename = normalizeFilename$1(context.
|
|
12603
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12511
12604
|
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12512
12605
|
return { JSXOpeningElement(node) {
|
|
12513
12606
|
if (isOgRoute) return;
|
|
@@ -12811,7 +12904,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
12811
12904
|
};
|
|
12812
12905
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
12813
12906
|
if (remainingDepth <= 0) return [];
|
|
12814
|
-
if (isFunctionLike$
|
|
12907
|
+
if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
|
|
12815
12908
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
12816
12909
|
for (const callArgument of expression.arguments ?? []) {
|
|
12817
12910
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -12861,7 +12954,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12861
12954
|
resolveBinding = buildProgramBindingLookup(node);
|
|
12862
12955
|
},
|
|
12863
12956
|
ExportNamedDeclaration(node) {
|
|
12864
|
-
const filename = normalizeFilename$1(context.
|
|
12957
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12865
12958
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
12866
12959
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
12867
12960
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -12934,7 +13027,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
12934
13027
|
});
|
|
12935
13028
|
//#endregion
|
|
12936
13029
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
12937
|
-
const MESSAGE$
|
|
13030
|
+
const MESSAGE$29 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
|
|
12938
13031
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
12939
13032
|
const noAccessKey = defineRule({
|
|
12940
13033
|
id: "no-access-key",
|
|
@@ -12950,7 +13043,7 @@ const noAccessKey = defineRule({
|
|
|
12950
13043
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
12951
13044
|
context.report({
|
|
12952
13045
|
node: accessKey,
|
|
12953
|
-
message: MESSAGE$
|
|
13046
|
+
message: MESSAGE$29
|
|
12954
13047
|
});
|
|
12955
13048
|
return;
|
|
12956
13049
|
}
|
|
@@ -12960,7 +13053,7 @@ const noAccessKey = defineRule({
|
|
|
12960
13053
|
if (isUndefinedIdentifier(expression)) return;
|
|
12961
13054
|
context.report({
|
|
12962
13055
|
node: accessKey,
|
|
12963
|
-
message: MESSAGE$
|
|
13056
|
+
message: MESSAGE$29
|
|
12964
13057
|
});
|
|
12965
13058
|
}
|
|
12966
13059
|
} })
|
|
@@ -13269,7 +13362,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
13269
13362
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
13270
13363
|
if (isNodeOfType(fn, "Identifier")) {
|
|
13271
13364
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
13272
|
-
if (definitionNode && isFunctionLike$
|
|
13365
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
|
|
13273
13366
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13274
13367
|
const initializer = definitionNode.init;
|
|
13275
13368
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -13362,14 +13455,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
13362
13455
|
return node ?? null;
|
|
13363
13456
|
};
|
|
13364
13457
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
13365
|
-
if (isFunctionLike$
|
|
13458
|
+
if (isFunctionLike$2(node)) return true;
|
|
13366
13459
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
13367
13460
|
if (isNodeOfType(node, "Identifier")) {
|
|
13368
13461
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
13369
|
-
if (definitionNode && isFunctionLike$
|
|
13462
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return true;
|
|
13370
13463
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13371
13464
|
const initializer = definitionNode.init;
|
|
13372
|
-
return isFunctionLike$
|
|
13465
|
+
return isFunctionLike$2(initializer);
|
|
13373
13466
|
}
|
|
13374
13467
|
}
|
|
13375
13468
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -13379,7 +13472,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13379
13472
|
if (visited.has(node)) return false;
|
|
13380
13473
|
visited.add(node);
|
|
13381
13474
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
13382
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$
|
|
13475
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
|
|
13383
13476
|
const record = node;
|
|
13384
13477
|
for (const [key, value] of Object.entries(record)) {
|
|
13385
13478
|
if (key === "parent") continue;
|
|
@@ -13391,7 +13484,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13391
13484
|
};
|
|
13392
13485
|
const hasCleanup = (analysis, node) => {
|
|
13393
13486
|
const fn = getEffectFn(analysis, node);
|
|
13394
|
-
if (!isFunctionLike$
|
|
13487
|
+
if (!isFunctionLike$2(fn)) return false;
|
|
13395
13488
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
13396
13489
|
return hasCleanupReturn(analysis, fn.body);
|
|
13397
13490
|
};
|
|
@@ -13405,9 +13498,9 @@ const findContainingNode = (analysis, node) => {
|
|
|
13405
13498
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
13406
13499
|
const noAdjustStateOnPropChange = defineRule({
|
|
13407
13500
|
id: "no-adjust-state-on-prop-change",
|
|
13408
|
-
severity: "
|
|
13501
|
+
severity: "error",
|
|
13409
13502
|
tags: ["test-noise"],
|
|
13410
|
-
recommendation: "Adjust the state inline during render
|
|
13503
|
+
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
13504
|
create: (context) => ({ CallExpression(node) {
|
|
13412
13505
|
if (!isUseEffect(node)) return;
|
|
13413
13506
|
const analysis = getProgramAnalysis(node);
|
|
@@ -13426,14 +13519,14 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13426
13519
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
13427
13520
|
context.report({
|
|
13428
13521
|
node: callExpr,
|
|
13429
|
-
message: "
|
|
13522
|
+
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
13523
|
});
|
|
13431
13524
|
}
|
|
13432
13525
|
} })
|
|
13433
13526
|
});
|
|
13434
13527
|
//#endregion
|
|
13435
13528
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
13436
|
-
const MESSAGE$
|
|
13529
|
+
const MESSAGE$28 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
|
|
13437
13530
|
const noAriaHiddenOnFocusable = defineRule({
|
|
13438
13531
|
id: "no-aria-hidden-on-focusable",
|
|
13439
13532
|
tags: ["react-jsx-only"],
|
|
@@ -13459,7 +13552,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
13459
13552
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
13460
13553
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
13461
13554
|
node: ariaHidden,
|
|
13462
|
-
message: MESSAGE$
|
|
13555
|
+
message: MESSAGE$28
|
|
13463
13556
|
});
|
|
13464
13557
|
} })
|
|
13465
13558
|
});
|
|
@@ -13739,7 +13832,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
13739
13832
|
let current = node;
|
|
13740
13833
|
while (current.parent) {
|
|
13741
13834
|
const parent = current.parent;
|
|
13742
|
-
if (isFunctionLike$
|
|
13835
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13743
13836
|
const callee = parent.callee;
|
|
13744
13837
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
|
|
13745
13838
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -13758,7 +13851,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
13758
13851
|
let current = node;
|
|
13759
13852
|
while (current.parent) {
|
|
13760
13853
|
const parent = current.parent;
|
|
13761
|
-
if (isFunctionLike$
|
|
13854
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13762
13855
|
const callee = parent.callee;
|
|
13763
13856
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
13764
13857
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -13826,7 +13919,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
13826
13919
|
});
|
|
13827
13920
|
//#endregion
|
|
13828
13921
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
13829
|
-
const MESSAGE$
|
|
13922
|
+
const MESSAGE$27 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
|
|
13830
13923
|
const SECOND_INDEX_METHODS = new Set([
|
|
13831
13924
|
"every",
|
|
13832
13925
|
"filter",
|
|
@@ -14028,7 +14121,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14028
14121
|
}
|
|
14029
14122
|
context.report({
|
|
14030
14123
|
node: keyAttribute,
|
|
14031
|
-
message: MESSAGE$
|
|
14124
|
+
message: MESSAGE$27
|
|
14032
14125
|
});
|
|
14033
14126
|
},
|
|
14034
14127
|
CallExpression(node) {
|
|
@@ -14048,7 +14141,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14048
14141
|
if (propName !== "key") continue;
|
|
14049
14142
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
14050
14143
|
node: property,
|
|
14051
|
-
message: MESSAGE$
|
|
14144
|
+
message: MESSAGE$27
|
|
14052
14145
|
});
|
|
14053
14146
|
}
|
|
14054
14147
|
}
|
|
@@ -14056,7 +14149,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14056
14149
|
});
|
|
14057
14150
|
//#endregion
|
|
14058
14151
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
14059
|
-
const MESSAGE$
|
|
14152
|
+
const MESSAGE$26 = "`autoFocus` should not be used — it disrupts users who expect the page focus to remain at the top of the document on load.";
|
|
14060
14153
|
const resolveSettings$21 = (settings) => {
|
|
14061
14154
|
const reactDoctor = settings?.["react-doctor"];
|
|
14062
14155
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -14093,7 +14186,7 @@ const noAutofocus = defineRule({
|
|
|
14093
14186
|
category: "Accessibility",
|
|
14094
14187
|
create: (context) => {
|
|
14095
14188
|
const settings = resolveSettings$21(context.settings);
|
|
14096
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
14189
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
14097
14190
|
return { JSXOpeningElement(node) {
|
|
14098
14191
|
if (isTestlikeFile) return;
|
|
14099
14192
|
const autoFocusAttribute = node.attributes.find((attribute) => {
|
|
@@ -14111,7 +14204,7 @@ const noAutofocus = defineRule({
|
|
|
14111
14204
|
}
|
|
14112
14205
|
context.report({
|
|
14113
14206
|
node: autoFocusAttribute,
|
|
14114
|
-
message: MESSAGE$
|
|
14207
|
+
message: MESSAGE$26
|
|
14115
14208
|
});
|
|
14116
14209
|
} };
|
|
14117
14210
|
}
|
|
@@ -14467,7 +14560,7 @@ const noBarrelImport = defineRule({
|
|
|
14467
14560
|
if (didReportForFile) return;
|
|
14468
14561
|
const source = node.source?.value;
|
|
14469
14562
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
14470
|
-
const filename = normalizeFilename$1(context.
|
|
14563
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
14471
14564
|
if (!filename) return;
|
|
14472
14565
|
const importRequests = getRuntimeImportRequests(node);
|
|
14473
14566
|
if (importRequests.length === 0) return;
|
|
@@ -14602,7 +14695,7 @@ const noChainStateUpdates = defineRule({
|
|
|
14602
14695
|
});
|
|
14603
14696
|
//#endregion
|
|
14604
14697
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
14605
|
-
const MESSAGE$
|
|
14698
|
+
const MESSAGE$25 = "Avoid passing children using a `children` prop — nest them between the JSX tags or pass them as additional `React.createElement` arguments instead.";
|
|
14606
14699
|
const noChildrenProp = defineRule({
|
|
14607
14700
|
id: "no-children-prop",
|
|
14608
14701
|
severity: "warn",
|
|
@@ -14613,7 +14706,7 @@ const noChildrenProp = defineRule({
|
|
|
14613
14706
|
if (node.name.name !== "children") return;
|
|
14614
14707
|
context.report({
|
|
14615
14708
|
node: node.name,
|
|
14616
|
-
message: MESSAGE$
|
|
14709
|
+
message: MESSAGE$25
|
|
14617
14710
|
});
|
|
14618
14711
|
},
|
|
14619
14712
|
CallExpression(node) {
|
|
@@ -14626,7 +14719,7 @@ const noChildrenProp = defineRule({
|
|
|
14626
14719
|
const propertyKey = property.key;
|
|
14627
14720
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
14628
14721
|
node: propertyKey,
|
|
14629
|
-
message: MESSAGE$
|
|
14722
|
+
message: MESSAGE$25
|
|
14630
14723
|
});
|
|
14631
14724
|
}
|
|
14632
14725
|
}
|
|
@@ -14634,7 +14727,7 @@ const noChildrenProp = defineRule({
|
|
|
14634
14727
|
});
|
|
14635
14728
|
//#endregion
|
|
14636
14729
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
14637
|
-
const MESSAGE$
|
|
14730
|
+
const MESSAGE$24 = "`React.cloneElement` is uncommon and leads to fragile components.";
|
|
14638
14731
|
const noCloneElement = defineRule({
|
|
14639
14732
|
id: "no-clone-element",
|
|
14640
14733
|
severity: "warn",
|
|
@@ -14646,7 +14739,7 @@ const noCloneElement = defineRule({
|
|
|
14646
14739
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
14647
14740
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
14648
14741
|
node: callee,
|
|
14649
|
-
message: MESSAGE$
|
|
14742
|
+
message: MESSAGE$24
|
|
14650
14743
|
});
|
|
14651
14744
|
return;
|
|
14652
14745
|
}
|
|
@@ -14659,14 +14752,231 @@ const noCloneElement = defineRule({
|
|
|
14659
14752
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
14660
14753
|
context.report({
|
|
14661
14754
|
node: callee,
|
|
14662
|
-
message: MESSAGE$
|
|
14755
|
+
message: MESSAGE$24
|
|
14663
14756
|
});
|
|
14664
14757
|
}
|
|
14665
14758
|
} })
|
|
14666
14759
|
});
|
|
14667
14760
|
//#endregion
|
|
14761
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
14762
|
+
const hocWrapperCalleeName = (callee) => {
|
|
14763
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
14764
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
14765
|
+
return null;
|
|
14766
|
+
};
|
|
14767
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
14768
|
+
let current = functionNode;
|
|
14769
|
+
for (;;) {
|
|
14770
|
+
const parent = current.parent;
|
|
14771
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
14772
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
14773
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
14774
|
+
current = parent;
|
|
14775
|
+
continue;
|
|
14776
|
+
}
|
|
14777
|
+
}
|
|
14778
|
+
break;
|
|
14779
|
+
}
|
|
14780
|
+
const binding = current.parent;
|
|
14781
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
14782
|
+
return null;
|
|
14783
|
+
};
|
|
14784
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
14785
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
14786
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
14787
|
+
};
|
|
14788
|
+
const nearestEnclosingFunction = (node) => {
|
|
14789
|
+
let cursor = node.parent;
|
|
14790
|
+
while (cursor) {
|
|
14791
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
14792
|
+
cursor = cursor.parent ?? null;
|
|
14793
|
+
}
|
|
14794
|
+
return null;
|
|
14795
|
+
};
|
|
14796
|
+
//#endregion
|
|
14797
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
14798
|
+
const enclosingComponentOrHookName = (node) => {
|
|
14799
|
+
const functionNode = nearestEnclosingFunction(node);
|
|
14800
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
14801
|
+
};
|
|
14802
|
+
//#endregion
|
|
14803
|
+
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
14804
|
+
const MESSAGE$23 = "createContext() called inside a component or hook — every render creates a brand new Context object, resetting every consumer and disconnecting Provider/Consumer pairs. Move createContext to module scope (outside the component) so the Context identity is stable across renders.";
|
|
14805
|
+
const CONTEXT_MODULES = [
|
|
14806
|
+
"react",
|
|
14807
|
+
"use-context-selector",
|
|
14808
|
+
"react-tracked"
|
|
14809
|
+
];
|
|
14810
|
+
const isCreateContextCallee = (callee) => {
|
|
14811
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
14812
|
+
for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
|
|
14813
|
+
return false;
|
|
14814
|
+
}
|
|
14815
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
14816
|
+
const namespaceIdentifier = callee.object;
|
|
14817
|
+
const propertyIdentifier = callee.property;
|
|
14818
|
+
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
|
|
14819
|
+
if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
|
|
14820
|
+
if (propertyIdentifier.name !== "createContext") return false;
|
|
14821
|
+
const namespaceName = namespaceIdentifier.name;
|
|
14822
|
+
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
14823
|
+
for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
|
|
14824
|
+
return false;
|
|
14825
|
+
}
|
|
14826
|
+
return false;
|
|
14827
|
+
};
|
|
14828
|
+
const noCreateContextInRender = defineRule({
|
|
14829
|
+
id: "no-create-context-in-render",
|
|
14830
|
+
severity: "error",
|
|
14831
|
+
category: "Correctness",
|
|
14832
|
+
recommendation: "Move `createContext(...)` to module scope so its identity is stable across renders.",
|
|
14833
|
+
create: (context) => ({ CallExpression(node) {
|
|
14834
|
+
if (!isCreateContextCallee(node.callee)) return;
|
|
14835
|
+
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
14836
|
+
if (!componentOrHookName) return;
|
|
14837
|
+
context.report({
|
|
14838
|
+
node,
|
|
14839
|
+
message: `${MESSAGE$23} (called inside "${componentOrHookName}")`
|
|
14840
|
+
});
|
|
14841
|
+
} })
|
|
14842
|
+
});
|
|
14843
|
+
//#endregion
|
|
14844
|
+
//#region src/plugin/rules/state-and-effects/no-create-store-in-render.ts
|
|
14845
|
+
const STORE_FACTORIES = [
|
|
14846
|
+
{
|
|
14847
|
+
module: "zustand",
|
|
14848
|
+
exportedName: "create",
|
|
14849
|
+
humanLabel: "zustand.create"
|
|
14850
|
+
},
|
|
14851
|
+
{
|
|
14852
|
+
module: "zustand",
|
|
14853
|
+
exportedName: "createStore",
|
|
14854
|
+
humanLabel: "zustand.createStore"
|
|
14855
|
+
},
|
|
14856
|
+
{
|
|
14857
|
+
module: "zustand/vanilla",
|
|
14858
|
+
exportedName: "createStore",
|
|
14859
|
+
humanLabel: "zustand.createStore"
|
|
14860
|
+
},
|
|
14861
|
+
{
|
|
14862
|
+
module: "zustand/vanilla",
|
|
14863
|
+
exportedName: "create",
|
|
14864
|
+
humanLabel: "zustand.create"
|
|
14865
|
+
},
|
|
14866
|
+
{
|
|
14867
|
+
module: "redux",
|
|
14868
|
+
exportedName: "createStore",
|
|
14869
|
+
humanLabel: "redux.createStore"
|
|
14870
|
+
},
|
|
14871
|
+
{
|
|
14872
|
+
module: "@reduxjs/toolkit",
|
|
14873
|
+
exportedName: "configureStore",
|
|
14874
|
+
humanLabel: "@reduxjs/toolkit.configureStore"
|
|
14875
|
+
},
|
|
14876
|
+
{
|
|
14877
|
+
module: "@reduxjs/toolkit",
|
|
14878
|
+
exportedName: "createSlice",
|
|
14879
|
+
humanLabel: "createSlice"
|
|
14880
|
+
},
|
|
14881
|
+
{
|
|
14882
|
+
module: "jotai",
|
|
14883
|
+
exportedName: "atom",
|
|
14884
|
+
humanLabel: "jotai.atom"
|
|
14885
|
+
},
|
|
14886
|
+
{
|
|
14887
|
+
module: "jotai/vanilla",
|
|
14888
|
+
exportedName: "atom",
|
|
14889
|
+
humanLabel: "jotai.atom"
|
|
14890
|
+
},
|
|
14891
|
+
{
|
|
14892
|
+
module: "jotai",
|
|
14893
|
+
exportedName: "createStore",
|
|
14894
|
+
humanLabel: "jotai.createStore"
|
|
14895
|
+
},
|
|
14896
|
+
{
|
|
14897
|
+
module: "valtio",
|
|
14898
|
+
exportedName: "proxy",
|
|
14899
|
+
humanLabel: "valtio.proxy"
|
|
14900
|
+
},
|
|
14901
|
+
{
|
|
14902
|
+
module: "valtio/vanilla",
|
|
14903
|
+
exportedName: "proxy",
|
|
14904
|
+
humanLabel: "valtio.proxy"
|
|
14905
|
+
},
|
|
14906
|
+
{
|
|
14907
|
+
module: "mobx",
|
|
14908
|
+
exportedName: "observable",
|
|
14909
|
+
humanLabel: "mobx.observable"
|
|
14910
|
+
},
|
|
14911
|
+
{
|
|
14912
|
+
module: "mobx",
|
|
14913
|
+
exportedName: "makeAutoObservable",
|
|
14914
|
+
humanLabel: "mobx.makeAutoObservable"
|
|
14915
|
+
},
|
|
14916
|
+
{
|
|
14917
|
+
module: "mobx",
|
|
14918
|
+
exportedName: "makeObservable",
|
|
14919
|
+
humanLabel: "mobx.makeObservable"
|
|
14920
|
+
},
|
|
14921
|
+
{
|
|
14922
|
+
module: "nanostores",
|
|
14923
|
+
exportedName: "atom",
|
|
14924
|
+
humanLabel: "nanostores.atom"
|
|
14925
|
+
},
|
|
14926
|
+
{
|
|
14927
|
+
module: "nanostores",
|
|
14928
|
+
exportedName: "map",
|
|
14929
|
+
humanLabel: "nanostores.map"
|
|
14930
|
+
},
|
|
14931
|
+
{
|
|
14932
|
+
module: "@xstate/store",
|
|
14933
|
+
exportedName: "createStore",
|
|
14934
|
+
humanLabel: "@xstate/store.createStore"
|
|
14935
|
+
}
|
|
14936
|
+
];
|
|
14937
|
+
const STORE_FACTORY_LOOKUP = /* @__PURE__ */ new Map();
|
|
14938
|
+
for (const factory of STORE_FACTORIES) {
|
|
14939
|
+
const bucket = STORE_FACTORY_LOOKUP.get(factory.exportedName) ?? [];
|
|
14940
|
+
STORE_FACTORY_LOOKUP.set(factory.exportedName, [...bucket, factory]);
|
|
14941
|
+
}
|
|
14942
|
+
const resolveStoreFactoryForCallee = (callee) => {
|
|
14943
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
14944
|
+
const localName = callee.name;
|
|
14945
|
+
for (const factoryBucket of STORE_FACTORY_LOOKUP.values()) for (const factory of factoryBucket) if (getImportedNameFromModule(callee, localName, factory.module) === factory.exportedName) return factory;
|
|
14946
|
+
return null;
|
|
14947
|
+
}
|
|
14948
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
14949
|
+
const namespaceIdentifier = callee.object;
|
|
14950
|
+
const propertyIdentifier = callee.property;
|
|
14951
|
+
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return null;
|
|
14952
|
+
if (!isNodeOfType(propertyIdentifier, "Identifier")) return null;
|
|
14953
|
+
const propertyName = propertyIdentifier.name;
|
|
14954
|
+
const factoryBucket = STORE_FACTORY_LOOKUP.get(propertyName);
|
|
14955
|
+
if (!factoryBucket) return null;
|
|
14956
|
+
for (const factory of factoryBucket) if (isImportedFromModule(namespaceIdentifier, namespaceIdentifier.name, factory.module)) return factory;
|
|
14957
|
+
return null;
|
|
14958
|
+
}
|
|
14959
|
+
return null;
|
|
14960
|
+
};
|
|
14961
|
+
const noCreateStoreInRender = defineRule({
|
|
14962
|
+
id: "no-create-store-in-render",
|
|
14963
|
+
severity: "error",
|
|
14964
|
+
category: "Correctness",
|
|
14965
|
+
recommendation: "Hoist the store/atom/observable construction to module scope — render functions and hooks must not allocate state containers.",
|
|
14966
|
+
create: (context) => ({ CallExpression(node) {
|
|
14967
|
+
const factory = resolveStoreFactoryForCallee(node.callee);
|
|
14968
|
+
if (!factory) return;
|
|
14969
|
+
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
14970
|
+
if (!componentOrHookName) return;
|
|
14971
|
+
context.report({
|
|
14972
|
+
node,
|
|
14973
|
+
message: `\`${factory.humanLabel}(...)\` called inside "${componentOrHookName}" allocates a fresh state container on every render — subscribers disconnect, identities (action creators, reducer reference, store instance) churn, and persisted state resets. Hoist the call to module scope.`
|
|
14974
|
+
});
|
|
14975
|
+
} })
|
|
14976
|
+
});
|
|
14977
|
+
//#endregion
|
|
14668
14978
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
14669
|
-
const MESSAGE$
|
|
14979
|
+
const MESSAGE$22 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
|
|
14670
14980
|
const noDanger = defineRule({
|
|
14671
14981
|
id: "no-danger",
|
|
14672
14982
|
severity: "warn",
|
|
@@ -14677,7 +14987,7 @@ const noDanger = defineRule({
|
|
|
14677
14987
|
if (!propAttribute) return;
|
|
14678
14988
|
context.report({
|
|
14679
14989
|
node: propAttribute.name,
|
|
14680
|
-
message: MESSAGE$
|
|
14990
|
+
message: MESSAGE$22
|
|
14681
14991
|
});
|
|
14682
14992
|
},
|
|
14683
14993
|
CallExpression(node) {
|
|
@@ -14689,7 +14999,7 @@ const noDanger = defineRule({
|
|
|
14689
14999
|
const propertyKey = property.key;
|
|
14690
15000
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
14691
15001
|
node: propertyKey,
|
|
14692
|
-
message: MESSAGE$
|
|
15002
|
+
message: MESSAGE$22
|
|
14693
15003
|
});
|
|
14694
15004
|
}
|
|
14695
15005
|
}
|
|
@@ -14697,7 +15007,7 @@ const noDanger = defineRule({
|
|
|
14697
15007
|
});
|
|
14698
15008
|
//#endregion
|
|
14699
15009
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
14700
|
-
const MESSAGE$
|
|
15010
|
+
const MESSAGE$21 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
|
|
14701
15011
|
const isLineBreak = (child) => {
|
|
14702
15012
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
14703
15013
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -14766,7 +15076,7 @@ const noDangerWithChildren = defineRule({
|
|
|
14766
15076
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
14767
15077
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
14768
15078
|
node: opening,
|
|
14769
|
-
message: MESSAGE$
|
|
15079
|
+
message: MESSAGE$21
|
|
14770
15080
|
});
|
|
14771
15081
|
},
|
|
14772
15082
|
CallExpression(node) {
|
|
@@ -14778,7 +15088,7 @@ const noDangerWithChildren = defineRule({
|
|
|
14778
15088
|
if (!propsShape.hasDangerously) return;
|
|
14779
15089
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
14780
15090
|
node,
|
|
14781
|
-
message: MESSAGE$
|
|
15091
|
+
message: MESSAGE$21
|
|
14782
15092
|
});
|
|
14783
15093
|
}
|
|
14784
15094
|
})
|
|
@@ -15153,7 +15463,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
15153
15463
|
};
|
|
15154
15464
|
const getInlineFunctionNode = (node) => {
|
|
15155
15465
|
if (!node) return null;
|
|
15156
|
-
if (isFunctionLike$
|
|
15466
|
+
if (isFunctionLike$2(node)) return node;
|
|
15157
15467
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
15158
15468
|
for (const argument of node.arguments ?? []) {
|
|
15159
15469
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -15164,7 +15474,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
15164
15474
|
const getNearestComponentFunction = (node) => {
|
|
15165
15475
|
let cursor = node.parent ?? null;
|
|
15166
15476
|
while (cursor) {
|
|
15167
|
-
if (isFunctionLike$
|
|
15477
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
15168
15478
|
cursor = cursor.parent ?? null;
|
|
15169
15479
|
}
|
|
15170
15480
|
return null;
|
|
@@ -15345,7 +15655,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
15345
15655
|
//#endregion
|
|
15346
15656
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
15347
15657
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
15348
|
-
const MESSAGE$
|
|
15658
|
+
const MESSAGE$20 = "Do not use `this.setState` in `componentDidMount`.";
|
|
15349
15659
|
const resolveSettings$20 = (settings) => {
|
|
15350
15660
|
const reactDoctor = settings?.["react-doctor"];
|
|
15351
15661
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15363,7 +15673,7 @@ const noDidMountSetState = defineRule({
|
|
|
15363
15673
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15364
15674
|
context.report({
|
|
15365
15675
|
node: node.callee,
|
|
15366
|
-
message: MESSAGE$
|
|
15676
|
+
message: MESSAGE$20
|
|
15367
15677
|
});
|
|
15368
15678
|
} };
|
|
15369
15679
|
}
|
|
@@ -15371,7 +15681,7 @@ const noDidMountSetState = defineRule({
|
|
|
15371
15681
|
//#endregion
|
|
15372
15682
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
15373
15683
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
15374
|
-
const MESSAGE$
|
|
15684
|
+
const MESSAGE$19 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
|
|
15375
15685
|
const resolveSettings$19 = (settings) => {
|
|
15376
15686
|
const reactDoctor = settings?.["react-doctor"];
|
|
15377
15687
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15389,7 +15699,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
15389
15699
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15390
15700
|
context.report({
|
|
15391
15701
|
node: node.callee,
|
|
15392
|
-
message: MESSAGE$
|
|
15702
|
+
message: MESSAGE$19
|
|
15393
15703
|
});
|
|
15394
15704
|
} };
|
|
15395
15705
|
}
|
|
@@ -15412,7 +15722,7 @@ const isStateMemberExpression = (node) => {
|
|
|
15412
15722
|
};
|
|
15413
15723
|
//#endregion
|
|
15414
15724
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
15415
|
-
const MESSAGE$
|
|
15725
|
+
const MESSAGE$18 = "Never mutate `this.state` directly.";
|
|
15416
15726
|
const shouldIgnoreMutation = (node) => {
|
|
15417
15727
|
let isConstructor = false;
|
|
15418
15728
|
let isInsideCallExpression = false;
|
|
@@ -15434,7 +15744,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
15434
15744
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
15435
15745
|
context.report({
|
|
15436
15746
|
node: reportNode,
|
|
15437
|
-
message: MESSAGE$
|
|
15747
|
+
message: MESSAGE$18
|
|
15438
15748
|
});
|
|
15439
15749
|
};
|
|
15440
15750
|
const noDirectMutationState = defineRule({
|
|
@@ -15490,7 +15800,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
15490
15800
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
15491
15801
|
if (!node || typeof node !== "object") return;
|
|
15492
15802
|
let nextShadowedStateNames = shadowedStateNames;
|
|
15493
|
-
if (isFunctionLike$
|
|
15803
|
+
if (isFunctionLike$2(node)) {
|
|
15494
15804
|
const localBindings = collectFunctionLocalBindings(node);
|
|
15495
15805
|
if (localBindings.size > 0) {
|
|
15496
15806
|
const merged = new Set(shadowedStateNames);
|
|
@@ -15536,7 +15846,7 @@ const noDirectStateMutation = defineRule({
|
|
|
15536
15846
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
15537
15847
|
if (!isNodeOfType(callee.property, "Identifier")) return;
|
|
15538
15848
|
const methodName = callee.property.name;
|
|
15539
|
-
if (!MUTATING_ARRAY_METHODS
|
|
15849
|
+
if (!MUTATING_ARRAY_METHODS.has(methodName)) return;
|
|
15540
15850
|
const rootName = getRootIdentifierName(callee.object);
|
|
15541
15851
|
if (!rootName || !stateValueToSetter.has(rootName)) return;
|
|
15542
15852
|
if (currentlyShadowed.has(rootName)) return;
|
|
@@ -15597,7 +15907,7 @@ const noDisabledZoom = defineRule({
|
|
|
15597
15907
|
});
|
|
15598
15908
|
//#endregion
|
|
15599
15909
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
15600
|
-
const buildMessage$
|
|
15910
|
+
const buildMessage$16 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
15601
15911
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
15602
15912
|
const resolveSettings$18 = (settings) => {
|
|
15603
15913
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -15617,7 +15927,7 @@ const noDistractingElements = defineRule({
|
|
|
15617
15927
|
const tag = getElementType(node, context.settings);
|
|
15618
15928
|
if (distractingTags.has(tag)) context.report({
|
|
15619
15929
|
node: node.name,
|
|
15620
|
-
message: buildMessage$
|
|
15930
|
+
message: buildMessage$16(tag)
|
|
15621
15931
|
});
|
|
15622
15932
|
} };
|
|
15623
15933
|
}
|
|
@@ -16057,6 +16367,69 @@ const noEffectEventInDeps = defineRule({
|
|
|
16057
16367
|
}
|
|
16058
16368
|
});
|
|
16059
16369
|
//#endregion
|
|
16370
|
+
//#region src/plugin/rules/state-and-effects/no-effect-with-fresh-deps.ts
|
|
16371
|
+
const classifyFreshDependency = (expression) => {
|
|
16372
|
+
const stripped = stripParenExpression(expression);
|
|
16373
|
+
if (isNodeOfType(stripped, "ObjectExpression")) return "object";
|
|
16374
|
+
if (isNodeOfType(stripped, "ArrayExpression")) return "array";
|
|
16375
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) return "function";
|
|
16376
|
+
if (isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment")) return "JSX";
|
|
16377
|
+
if (isNodeOfType(stripped, "NewExpression")) return "instance";
|
|
16378
|
+
return null;
|
|
16379
|
+
};
|
|
16380
|
+
const resolveDependencyFreshness = (dep) => {
|
|
16381
|
+
const directKind = classifyFreshDependency(dep);
|
|
16382
|
+
if (directKind) return {
|
|
16383
|
+
kind: directKind,
|
|
16384
|
+
viaBindingName: null
|
|
16385
|
+
};
|
|
16386
|
+
const stripped = stripParenExpression(dep);
|
|
16387
|
+
if (!isNodeOfType(stripped, "Identifier")) return null;
|
|
16388
|
+
const binding = findVariableInitializer(stripped, stripped.name);
|
|
16389
|
+
if (!binding || !binding.initializer) return null;
|
|
16390
|
+
if (binding.scopeOwner.type === "Program") return null;
|
|
16391
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
16392
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== binding.initializer) return null;
|
|
16393
|
+
const indirectKind = classifyFreshDependency(binding.initializer);
|
|
16394
|
+
if (!indirectKind) return null;
|
|
16395
|
+
return {
|
|
16396
|
+
kind: indirectKind,
|
|
16397
|
+
viaBindingName: stripped.name
|
|
16398
|
+
};
|
|
16399
|
+
};
|
|
16400
|
+
const noEffectWithFreshDeps = defineRule({
|
|
16401
|
+
id: "no-effect-with-fresh-deps",
|
|
16402
|
+
severity: "error",
|
|
16403
|
+
category: "State & Effects",
|
|
16404
|
+
recommendation: "Move the constructed value into the hook body (so it's recomputed during render) and instead depend on its primitive inputs, or wrap the value in useMemo / useCallback so its reference is stable.",
|
|
16405
|
+
create: (context) => ({ CallExpression(node) {
|
|
16406
|
+
if (!isHookCall$1(node, HOOKS_WITH_DEPS)) return;
|
|
16407
|
+
const args = node.arguments ?? [];
|
|
16408
|
+
if (args.length < 2) return;
|
|
16409
|
+
const depsNode = args[1];
|
|
16410
|
+
if (!depsNode) return;
|
|
16411
|
+
const stripped = stripParenExpression(depsNode);
|
|
16412
|
+
if (!isNodeOfType(stripped, "ArrayExpression")) return;
|
|
16413
|
+
const calleeNode = node.callee;
|
|
16414
|
+
let hookName;
|
|
16415
|
+
if (isNodeOfType(calleeNode, "Identifier")) hookName = calleeNode.name;
|
|
16416
|
+
else if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) hookName = calleeNode.property.name;
|
|
16417
|
+
else hookName = "hook";
|
|
16418
|
+
const elements = stripped.elements ?? [];
|
|
16419
|
+
for (const element of elements) {
|
|
16420
|
+
if (!element) continue;
|
|
16421
|
+
if (isNodeOfType(element, "SpreadElement")) continue;
|
|
16422
|
+
const freshness = resolveDependencyFreshness(element);
|
|
16423
|
+
if (!freshness) continue;
|
|
16424
|
+
const message = freshness.viaBindingName ? `${hookName} dep array element \`${freshness.viaBindingName}\` is a render-local ${freshness.kind} (declared in the same component scope); \`===\` will always fail because the binding is re-allocated each render. Hoist it to module scope or wrap it in useMemo/useCallback.` : `${hookName} dep array contains a freshly-allocated ${freshness.kind}; \`===\` will always fail on this element so the hook runs every render. Move the value into the hook body or memoize it with useMemo/useCallback so its reference is stable.`;
|
|
16425
|
+
context.report({
|
|
16426
|
+
node: element,
|
|
16427
|
+
message
|
|
16428
|
+
});
|
|
16429
|
+
}
|
|
16430
|
+
} })
|
|
16431
|
+
});
|
|
16432
|
+
//#endregion
|
|
16060
16433
|
//#region src/plugin/rules/security/no-eval.ts
|
|
16061
16434
|
const noEval = defineRule({
|
|
16062
16435
|
id: "no-eval",
|
|
@@ -16947,7 +17320,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
16947
17320
|
"ReactDOM",
|
|
16948
17321
|
"ReactDom"
|
|
16949
17322
|
]);
|
|
16950
|
-
const MESSAGE$
|
|
17323
|
+
const MESSAGE$17 = "Unexpected call to `findDOMNode` — removed in React 19.";
|
|
16951
17324
|
const noFindDomNode = defineRule({
|
|
16952
17325
|
id: "no-find-dom-node",
|
|
16953
17326
|
severity: "warn",
|
|
@@ -16957,7 +17330,7 @@ const noFindDomNode = defineRule({
|
|
|
16957
17330
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
16958
17331
|
context.report({
|
|
16959
17332
|
node: callee,
|
|
16960
|
-
message: MESSAGE$
|
|
17333
|
+
message: MESSAGE$17
|
|
16961
17334
|
});
|
|
16962
17335
|
return;
|
|
16963
17336
|
}
|
|
@@ -16968,7 +17341,7 @@ const noFindDomNode = defineRule({
|
|
|
16968
17341
|
if (callee.property.name !== "findDOMNode") return;
|
|
16969
17342
|
context.report({
|
|
16970
17343
|
node: callee.property,
|
|
16971
|
-
message: MESSAGE$
|
|
17344
|
+
message: MESSAGE$17
|
|
16972
17345
|
});
|
|
16973
17346
|
}
|
|
16974
17347
|
} })
|
|
@@ -17458,7 +17831,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
17458
17831
|
});
|
|
17459
17832
|
//#endregion
|
|
17460
17833
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
17461
|
-
const buildMessage$
|
|
17834
|
+
const buildMessage$15 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
17462
17835
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
17463
17836
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
17464
17837
|
tr: ["none", "presentation"],
|
|
@@ -17502,7 +17875,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
17502
17875
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
17503
17876
|
context.report({
|
|
17504
17877
|
node: roleAttribute,
|
|
17505
|
-
message: buildMessage$
|
|
17878
|
+
message: buildMessage$15(elementType, firstRole)
|
|
17506
17879
|
});
|
|
17507
17880
|
} };
|
|
17508
17881
|
}
|
|
@@ -17703,7 +18076,7 @@ const isInsideClassBody = (node) => {
|
|
|
17703
18076
|
let current = node.parent;
|
|
17704
18077
|
while (current) {
|
|
17705
18078
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
17706
|
-
if (isFunctionLike$
|
|
18079
|
+
if (isFunctionLike$2(current)) return false;
|
|
17707
18080
|
current = current.parent;
|
|
17708
18081
|
}
|
|
17709
18082
|
return false;
|
|
@@ -17946,7 +18319,7 @@ const noMoment = defineRule({
|
|
|
17946
18319
|
});
|
|
17947
18320
|
//#endregion
|
|
17948
18321
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17949
|
-
const buildMessage$
|
|
18322
|
+
const buildMessage$14 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17950
18323
|
const resolveSettings$16 = (settings) => {
|
|
17951
18324
|
const reactDoctor = settings?.["react-doctor"];
|
|
17952
18325
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -18246,7 +18619,7 @@ const noMultiComp = defineRule({
|
|
|
18246
18619
|
category: "Architecture",
|
|
18247
18620
|
create: (context) => {
|
|
18248
18621
|
const settings = resolveSettings$16(context.settings);
|
|
18249
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
18622
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
18250
18623
|
return { Program(node) {
|
|
18251
18624
|
if (isTestlikeFile) return;
|
|
18252
18625
|
const visitContext = {
|
|
@@ -18267,7 +18640,7 @@ const noMultiComp = defineRule({
|
|
|
18267
18640
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
18268
18641
|
for (const component of flagged.slice(1)) context.report({
|
|
18269
18642
|
node: component.reportNode,
|
|
18270
|
-
message: buildMessage$
|
|
18643
|
+
message: buildMessage$14(component.name)
|
|
18271
18644
|
});
|
|
18272
18645
|
} };
|
|
18273
18646
|
}
|
|
@@ -18344,25 +18717,315 @@ const noMutableInDeps = defineRule({
|
|
|
18344
18717
|
}
|
|
18345
18718
|
});
|
|
18346
18719
|
//#endregion
|
|
18347
|
-
//#region src/plugin/rules/state-and-effects/
|
|
18348
|
-
const
|
|
18349
|
-
|
|
18350
|
-
"
|
|
18351
|
-
"
|
|
18352
|
-
"
|
|
18353
|
-
"
|
|
18354
|
-
"
|
|
18355
|
-
"
|
|
18356
|
-
"
|
|
18357
|
-
"
|
|
18358
|
-
"
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
"
|
|
18362
|
-
"
|
|
18363
|
-
"delete",
|
|
18364
|
-
"set"
|
|
18720
|
+
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
18721
|
+
const LODASH_MUTATOR_NAMES = new Set([
|
|
18722
|
+
"set",
|
|
18723
|
+
"unset",
|
|
18724
|
+
"update",
|
|
18725
|
+
"merge",
|
|
18726
|
+
"defaults",
|
|
18727
|
+
"defaultsDeep",
|
|
18728
|
+
"assign",
|
|
18729
|
+
"assignIn",
|
|
18730
|
+
"pull",
|
|
18731
|
+
"pullAll",
|
|
18732
|
+
"pullAllBy",
|
|
18733
|
+
"pullAt",
|
|
18734
|
+
"remove",
|
|
18735
|
+
"fill"
|
|
18365
18736
|
]);
|
|
18737
|
+
const LODASH_MUTATING_MODULES = new Set(["lodash", "lodash-es"]);
|
|
18738
|
+
const isLodashMutatingImport = (sourceValue) => {
|
|
18739
|
+
if (LODASH_MUTATING_MODULES.has(sourceValue)) return true;
|
|
18740
|
+
if (sourceValue.startsWith("lodash/fp") || sourceValue.startsWith("lodash-es/fp")) return false;
|
|
18741
|
+
if (sourceValue.startsWith("lodash/") || sourceValue.startsWith("lodash-es/")) return true;
|
|
18742
|
+
return false;
|
|
18743
|
+
};
|
|
18744
|
+
const isLodashMutatorCall = (callExpression) => {
|
|
18745
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
18746
|
+
const callee = callExpression.callee;
|
|
18747
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
18748
|
+
if (!LODASH_MUTATOR_NAMES.has(callee.name)) return false;
|
|
18749
|
+
const initializer = findVariableInitializer(callee, callee.name)?.initializer;
|
|
18750
|
+
if (!initializer) return false;
|
|
18751
|
+
if (!isNodeOfType(initializer, "ImportSpecifier") && !isNodeOfType(initializer, "ImportDefaultSpecifier") && !isNodeOfType(initializer, "ImportNamespaceSpecifier")) return false;
|
|
18752
|
+
const importDeclaration = initializer.parent;
|
|
18753
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
18754
|
+
const sourceValue = importDeclaration.source?.value;
|
|
18755
|
+
if (typeof sourceValue !== "string") return false;
|
|
18756
|
+
return isLodashMutatingImport(sourceValue);
|
|
18757
|
+
}
|
|
18758
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
18759
|
+
const propertyName = getStaticMemberPropertyName(callee);
|
|
18760
|
+
if (!propertyName || !LODASH_MUTATOR_NAMES.has(propertyName)) return false;
|
|
18761
|
+
const receiver = callee.object;
|
|
18762
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
18763
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer;
|
|
18764
|
+
if (!initializer) return false;
|
|
18765
|
+
if (!isNodeOfType(initializer, "ImportNamespaceSpecifier") && !isNodeOfType(initializer, "ImportDefaultSpecifier")) return false;
|
|
18766
|
+
const importDeclaration = initializer.parent;
|
|
18767
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
18768
|
+
const sourceValue = importDeclaration.source?.value;
|
|
18769
|
+
if (typeof sourceValue !== "string") return false;
|
|
18770
|
+
return isLodashMutatingImport(sourceValue);
|
|
18771
|
+
}
|
|
18772
|
+
return false;
|
|
18773
|
+
};
|
|
18774
|
+
//#endregion
|
|
18775
|
+
//#region src/plugin/utils/find-exported-function-body.ts
|
|
18776
|
+
const isFunctionLike = (node) => {
|
|
18777
|
+
if (!node) return false;
|
|
18778
|
+
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
18779
|
+
};
|
|
18780
|
+
const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
18781
|
+
if (!isNodeOfType(programRoot, "Program")) return null;
|
|
18782
|
+
const localBindings = /* @__PURE__ */ new Map();
|
|
18783
|
+
const namedExports = /* @__PURE__ */ new Map();
|
|
18784
|
+
let defaultExport = null;
|
|
18785
|
+
let defaultExportIdentifierName = null;
|
|
18786
|
+
const recordVariableDeclaration = (declaration) => {
|
|
18787
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
18788
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
18789
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
18790
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
18791
|
+
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18792
|
+
}
|
|
18793
|
+
};
|
|
18794
|
+
for (const statement of programRoot.body ?? []) {
|
|
18795
|
+
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
18796
|
+
recordVariableDeclaration(statement);
|
|
18797
|
+
continue;
|
|
18798
|
+
}
|
|
18799
|
+
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
|
|
18800
|
+
localBindings.set(statement.id.name, statement);
|
|
18801
|
+
continue;
|
|
18802
|
+
}
|
|
18803
|
+
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
18804
|
+
const declaration = statement.declaration;
|
|
18805
|
+
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
18806
|
+
recordVariableDeclaration(declaration);
|
|
18807
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
18808
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
18809
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
18810
|
+
namedExports.set(declarator.id.name, declarator.id.name);
|
|
18811
|
+
}
|
|
18812
|
+
} else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
18813
|
+
localBindings.set(declaration.id.name, declaration);
|
|
18814
|
+
namedExports.set(declaration.id.name, declaration.id.name);
|
|
18815
|
+
}
|
|
18816
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
18817
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
18818
|
+
const local = specifier.local;
|
|
18819
|
+
const exported = specifier.exported;
|
|
18820
|
+
if (!isNodeOfType(local, "Identifier")) continue;
|
|
18821
|
+
const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
|
|
18822
|
+
if (!exportedNameSpec) continue;
|
|
18823
|
+
namedExports.set(exportedNameSpec, local.name);
|
|
18824
|
+
}
|
|
18825
|
+
continue;
|
|
18826
|
+
}
|
|
18827
|
+
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
18828
|
+
const declaration = statement.declaration;
|
|
18829
|
+
if (!declaration) continue;
|
|
18830
|
+
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
18831
|
+
localBindings.set(declaration.id.name, declaration);
|
|
18832
|
+
defaultExport = declaration;
|
|
18833
|
+
continue;
|
|
18834
|
+
}
|
|
18835
|
+
if (isFunctionLike(declaration)) {
|
|
18836
|
+
defaultExport = declaration;
|
|
18837
|
+
continue;
|
|
18838
|
+
}
|
|
18839
|
+
if (isNodeOfType(declaration, "Identifier")) {
|
|
18840
|
+
defaultExportIdentifierName = declaration.name;
|
|
18841
|
+
continue;
|
|
18842
|
+
}
|
|
18843
|
+
}
|
|
18844
|
+
}
|
|
18845
|
+
if (exportedName === "default") {
|
|
18846
|
+
if (defaultExport) return defaultExport;
|
|
18847
|
+
if (defaultExportIdentifierName) {
|
|
18848
|
+
const binding = localBindings.get(defaultExportIdentifierName);
|
|
18849
|
+
if (binding) return binding;
|
|
18850
|
+
}
|
|
18851
|
+
}
|
|
18852
|
+
const localName = namedExports.get(exportedName);
|
|
18853
|
+
if (!localName) return null;
|
|
18854
|
+
return localBindings.get(localName) ?? null;
|
|
18855
|
+
};
|
|
18856
|
+
const resolveImportedExportName = (importSpecifier) => {
|
|
18857
|
+
if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
|
|
18858
|
+
const imported = importSpecifier.imported;
|
|
18859
|
+
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
18860
|
+
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
18861
|
+
return null;
|
|
18862
|
+
}
|
|
18863
|
+
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
18864
|
+
return null;
|
|
18865
|
+
};
|
|
18866
|
+
const findReExportSourcesForName = (programRoot, exportedName) => {
|
|
18867
|
+
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
18868
|
+
const exportAllSources = [];
|
|
18869
|
+
for (const statement of programRoot.body ?? []) {
|
|
18870
|
+
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
18871
|
+
const sourceValue = statement.source.value;
|
|
18872
|
+
if (typeof sourceValue !== "string") continue;
|
|
18873
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
18874
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
18875
|
+
const exported = specifier.exported;
|
|
18876
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
|
|
18877
|
+
}
|
|
18878
|
+
}
|
|
18879
|
+
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
18880
|
+
const sourceValue = statement.source.value;
|
|
18881
|
+
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
18882
|
+
}
|
|
18883
|
+
}
|
|
18884
|
+
return exportAllSources;
|
|
18885
|
+
};
|
|
18886
|
+
//#endregion
|
|
18887
|
+
//#region src/plugin/utils/attach-parent-references.ts
|
|
18888
|
+
const attachParentReferences = (root) => {
|
|
18889
|
+
const visit = (node, parent) => {
|
|
18890
|
+
const writableNode = node;
|
|
18891
|
+
writableNode.parent = parent;
|
|
18892
|
+
const nodeRecord = node;
|
|
18893
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
18894
|
+
if (key === "parent") continue;
|
|
18895
|
+
const child = nodeRecord[key];
|
|
18896
|
+
if (Array.isArray(child)) {
|
|
18897
|
+
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
18898
|
+
} else if (isAstNode(child)) visit(child, node);
|
|
18899
|
+
}
|
|
18900
|
+
};
|
|
18901
|
+
visit(root, null);
|
|
18902
|
+
};
|
|
18903
|
+
//#endregion
|
|
18904
|
+
//#region src/plugin/utils/parse-source-file.ts
|
|
18905
|
+
const FILENAME_TO_LANG = {
|
|
18906
|
+
".ts": "ts",
|
|
18907
|
+
".tsx": "tsx",
|
|
18908
|
+
".js": "js",
|
|
18909
|
+
".jsx": "jsx",
|
|
18910
|
+
".mjs": "js",
|
|
18911
|
+
".cjs": "js",
|
|
18912
|
+
".mts": "ts",
|
|
18913
|
+
".cts": "ts"
|
|
18914
|
+
};
|
|
18915
|
+
const resolveLang = (filename) => {
|
|
18916
|
+
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
18917
|
+
};
|
|
18918
|
+
const parseCache = /* @__PURE__ */ new Map();
|
|
18919
|
+
const parseSourceFile = (absoluteFilePath) => {
|
|
18920
|
+
let fileStat;
|
|
18921
|
+
try {
|
|
18922
|
+
fileStat = fs.statSync(absoluteFilePath);
|
|
18923
|
+
} catch {
|
|
18924
|
+
return null;
|
|
18925
|
+
}
|
|
18926
|
+
if (!fileStat.isFile()) return null;
|
|
18927
|
+
if (fileStat.size > 2e6) return null;
|
|
18928
|
+
const cached = parseCache.get(absoluteFilePath);
|
|
18929
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
18930
|
+
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
18931
|
+
parseCache.set(absoluteFilePath, {
|
|
18932
|
+
mtimeMs: fileStat.mtimeMs,
|
|
18933
|
+
size: fileStat.size,
|
|
18934
|
+
program: null
|
|
18935
|
+
});
|
|
18936
|
+
return null;
|
|
18937
|
+
}
|
|
18938
|
+
let sourceText;
|
|
18939
|
+
try {
|
|
18940
|
+
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
18941
|
+
} catch {
|
|
18942
|
+
parseCache.set(absoluteFilePath, {
|
|
18943
|
+
mtimeMs: fileStat.mtimeMs,
|
|
18944
|
+
size: fileStat.size,
|
|
18945
|
+
program: null
|
|
18946
|
+
});
|
|
18947
|
+
return null;
|
|
18948
|
+
}
|
|
18949
|
+
let parsedProgram = null;
|
|
18950
|
+
try {
|
|
18951
|
+
const result = parseSync(absoluteFilePath, sourceText, {
|
|
18952
|
+
astType: "ts",
|
|
18953
|
+
lang: resolveLang(absoluteFilePath)
|
|
18954
|
+
});
|
|
18955
|
+
if (!result.errors.some((parseError) => parseError.severity === "Error")) {
|
|
18956
|
+
parsedProgram = result.program;
|
|
18957
|
+
attachParentReferences(parsedProgram);
|
|
18958
|
+
}
|
|
18959
|
+
} catch {
|
|
18960
|
+
parsedProgram = null;
|
|
18961
|
+
}
|
|
18962
|
+
parseCache.set(absoluteFilePath, {
|
|
18963
|
+
mtimeMs: fileStat.mtimeMs,
|
|
18964
|
+
size: fileStat.size,
|
|
18965
|
+
program: parsedProgram
|
|
18966
|
+
});
|
|
18967
|
+
return parsedProgram;
|
|
18968
|
+
};
|
|
18969
|
+
//#endregion
|
|
18970
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
|
|
18971
|
+
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
18972
|
+
if (visitedFilePaths.size >= 4) return null;
|
|
18973
|
+
if (visitedFilePaths.has(filePath)) return null;
|
|
18974
|
+
visitedFilePaths.add(filePath);
|
|
18975
|
+
const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
|
|
18976
|
+
const programRoot = parseSourceFile(actualFilePath);
|
|
18977
|
+
if (!programRoot) return null;
|
|
18978
|
+
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
18979
|
+
if (exported) return exported;
|
|
18980
|
+
for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
|
|
18981
|
+
const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
|
|
18982
|
+
if (!nextFilePath) continue;
|
|
18983
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
|
|
18984
|
+
if (resolved) return resolved;
|
|
18985
|
+
}
|
|
18986
|
+
return null;
|
|
18987
|
+
};
|
|
18988
|
+
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
18989
|
+
const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
|
|
18990
|
+
if (!resolvedFilePath) return null;
|
|
18991
|
+
return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
|
|
18992
|
+
};
|
|
18993
|
+
const resolveReducerFunction = (node, currentFilename) => {
|
|
18994
|
+
if (!node) return null;
|
|
18995
|
+
const unwrappedNode = stripParenExpression(node);
|
|
18996
|
+
if (isFunctionLike$2(unwrappedNode)) return {
|
|
18997
|
+
functionNode: unwrappedNode,
|
|
18998
|
+
crossFileSourceDisplay: null
|
|
18999
|
+
};
|
|
19000
|
+
if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
|
|
19001
|
+
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
19002
|
+
if (!initializer) return null;
|
|
19003
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
19004
|
+
if (isFunctionLike$2(unwrappedInitializer)) return {
|
|
19005
|
+
functionNode: unwrappedInitializer,
|
|
19006
|
+
crossFileSourceDisplay: null
|
|
19007
|
+
};
|
|
19008
|
+
if (isNodeOfType(initializer, "ImportSpecifier") || isNodeOfType(initializer, "ImportDefaultSpecifier")) {
|
|
19009
|
+
if (!currentFilename) return null;
|
|
19010
|
+
const importDeclaration = initializer.parent;
|
|
19011
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
19012
|
+
const sourceValue = importDeclaration.source?.value;
|
|
19013
|
+
if (typeof sourceValue !== "string") return null;
|
|
19014
|
+
if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
|
|
19015
|
+
const exportedName = resolveImportedExportName(initializer);
|
|
19016
|
+
if (!exportedName) return null;
|
|
19017
|
+
const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);
|
|
19018
|
+
if (!crossFileFunction) return null;
|
|
19019
|
+
return {
|
|
19020
|
+
functionNode: crossFileFunction,
|
|
19021
|
+
crossFileSourceDisplay: sourceValue
|
|
19022
|
+
};
|
|
19023
|
+
}
|
|
19024
|
+
return null;
|
|
19025
|
+
};
|
|
19026
|
+
//#endregion
|
|
19027
|
+
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
19028
|
+
const MESSAGE$16 = "Reducer mutates its current state and returns the same reference. Return a copied object or array so React can observe the update.";
|
|
18366
19029
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
18367
19030
|
"copyWithin",
|
|
18368
19031
|
"fill",
|
|
@@ -18381,7 +19044,6 @@ const cloneReducerPathState = (state) => ({
|
|
|
18381
19044
|
mutableStateSourceNames: new Set(state.mutableStateSourceNames),
|
|
18382
19045
|
mutations: [...state.mutations]
|
|
18383
19046
|
});
|
|
18384
|
-
const isFunctionLikeAstNode = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
|
|
18385
19047
|
const isSpecifierImportedFromReact = (node) => {
|
|
18386
19048
|
const parent = node.parent ?? null;
|
|
18387
19049
|
return parent !== null && isNodeOfType(parent, "ImportDeclaration") && parent.source.value === "react";
|
|
@@ -18408,19 +19070,16 @@ const isCallToImportedReactUseReducer = (node) => {
|
|
|
18408
19070
|
const binding = findVariableInitializer(callee.object, callee.object.name);
|
|
18409
19071
|
return Boolean(binding?.initializer && isReactNamespaceOrDefaultImportSpecifier(binding.initializer));
|
|
18410
19072
|
};
|
|
18411
|
-
const resolveSameFileReducerFunction = (node) => {
|
|
18412
|
-
if (!node) return null;
|
|
18413
|
-
const unwrappedNode = stripParenExpression(node);
|
|
18414
|
-
if (isFunctionLikeAstNode(unwrappedNode)) return unwrappedNode;
|
|
18415
|
-
if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
|
|
18416
|
-
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
18417
|
-
if (!initializer) return null;
|
|
18418
|
-
const unwrappedInitializer = stripParenExpression(initializer);
|
|
18419
|
-
return isFunctionLikeAstNode(unwrappedInitializer) ? unwrappedInitializer : null;
|
|
18420
|
-
};
|
|
18421
19073
|
const isStaticMethodCallOnNamedObject = (node, objectName, methodNames) => {
|
|
18422
19074
|
const unwrappedNode = stripParenExpression(node);
|
|
18423
|
-
|
|
19075
|
+
if (!isNodeOfType(unwrappedNode, "CallExpression")) return false;
|
|
19076
|
+
if (!isNodeOfType(unwrappedNode.callee, "MemberExpression")) return false;
|
|
19077
|
+
const calleeObject = unwrappedNode.callee.object;
|
|
19078
|
+
if (!isNodeOfType(calleeObject, "Identifier")) return false;
|
|
19079
|
+
if (calleeObject.name !== objectName) return false;
|
|
19080
|
+
if (!methodNames.has(getStaticMemberPropertyName(unwrappedNode.callee) ?? "")) return false;
|
|
19081
|
+
if (findVariableInitializer(calleeObject, calleeObject.name)) return false;
|
|
19082
|
+
return true;
|
|
18424
19083
|
};
|
|
18425
19084
|
const isExpressionRootedInMutableReducerStateSource = (node, state) => {
|
|
18426
19085
|
let current = stripParenExpression(node);
|
|
@@ -18456,11 +19115,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
18456
19115
|
return false;
|
|
18457
19116
|
};
|
|
18458
19117
|
const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
18459
|
-
if (
|
|
19118
|
+
if (isFunctionLike$2(node)) return [];
|
|
18460
19119
|
const mutations = [];
|
|
18461
19120
|
walkAst(node, (child) => {
|
|
18462
19121
|
const unwrappedChild = stripParenExpression(child);
|
|
18463
|
-
if (child !== node &&
|
|
19122
|
+
if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
|
|
18464
19123
|
if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
|
|
18465
19124
|
if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
|
|
18466
19125
|
return;
|
|
@@ -18479,6 +19138,10 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
18479
19138
|
mutations.push({ node: unwrappedChild });
|
|
18480
19139
|
return;
|
|
18481
19140
|
}
|
|
19141
|
+
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && isLodashMutatorCall(unwrappedChild)) {
|
|
19142
|
+
mutations.push({ node: unwrappedChild });
|
|
19143
|
+
return;
|
|
19144
|
+
}
|
|
18482
19145
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
18483
19146
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
18484
19147
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
@@ -18505,18 +19168,47 @@ const restoreOuterIdentityForBlockScopedNames = (blockState, outerState, blockSc
|
|
|
18505
19168
|
}
|
|
18506
19169
|
return nextState;
|
|
18507
19170
|
};
|
|
19171
|
+
const recordDestructuredAliasNames = (pattern, state) => {
|
|
19172
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
19173
|
+
for (const property of pattern.properties ?? []) {
|
|
19174
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
19175
|
+
const valueNode = property.value;
|
|
19176
|
+
let leafIdentifier = null;
|
|
19177
|
+
if (isNodeOfType(valueNode, "Identifier")) leafIdentifier = valueNode;
|
|
19178
|
+
else if (isNodeOfType(valueNode, "AssignmentPattern") && isNodeOfType(valueNode.left, "Identifier")) leafIdentifier = valueNode.left;
|
|
19179
|
+
if (!leafIdentifier) continue;
|
|
19180
|
+
state.mutableStateSourceNames.add(leafIdentifier.name);
|
|
19181
|
+
}
|
|
19182
|
+
return;
|
|
19183
|
+
}
|
|
19184
|
+
if (isNodeOfType(pattern, "ArrayPattern")) for (const element of pattern.elements ?? []) {
|
|
19185
|
+
if (!element) continue;
|
|
19186
|
+
if (isNodeOfType(element, "Identifier")) {
|
|
19187
|
+
state.mutableStateSourceNames.add(element.name);
|
|
19188
|
+
continue;
|
|
19189
|
+
}
|
|
19190
|
+
if (isNodeOfType(element, "AssignmentPattern") && isNodeOfType(element.left, "Identifier")) {
|
|
19191
|
+
state.mutableStateSourceNames.add(element.left.name);
|
|
19192
|
+
continue;
|
|
19193
|
+
}
|
|
19194
|
+
if (isNodeOfType(element, "RestElement") && isNodeOfType(element.argument, "Identifier")) {}
|
|
19195
|
+
}
|
|
19196
|
+
};
|
|
18508
19197
|
const updateReducerStateIdentityForVariableDeclaration = (declaration, state) => {
|
|
18509
19198
|
for (const declarator of declaration.declarations ?? []) {
|
|
18510
|
-
if (
|
|
18511
|
-
|
|
18512
|
-
|
|
18513
|
-
|
|
18514
|
-
|
|
18515
|
-
|
|
18516
|
-
|
|
19199
|
+
if (isNodeOfType(declarator.id, "Identifier")) {
|
|
19200
|
+
const name = declarator.id.name;
|
|
19201
|
+
state.originalStateReferenceNames.delete(name);
|
|
19202
|
+
state.mutableStateSourceNames.delete(name);
|
|
19203
|
+
if (isExpressionOriginalReducerStateReference(declarator.init, state)) {
|
|
19204
|
+
state.originalStateReferenceNames.add(name);
|
|
19205
|
+
state.mutableStateSourceNames.add(name);
|
|
19206
|
+
continue;
|
|
19207
|
+
}
|
|
19208
|
+
if (isExpressionReachableFromOriginalReducerState(declarator.init, state)) state.mutableStateSourceNames.add(name);
|
|
18517
19209
|
continue;
|
|
18518
19210
|
}
|
|
18519
|
-
if (isExpressionReachableFromOriginalReducerState(declarator.init, state)) state
|
|
19211
|
+
if ((isNodeOfType(declarator.id, "ObjectPattern") || isNodeOfType(declarator.id, "ArrayPattern")) && isExpressionReachableFromOriginalReducerState(declarator.init, state)) recordDestructuredAliasNames(declarator.id, state);
|
|
18520
19212
|
}
|
|
18521
19213
|
};
|
|
18522
19214
|
const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) => {
|
|
@@ -18531,24 +19223,40 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
|
|
|
18531
19223
|
}
|
|
18532
19224
|
if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
|
|
18533
19225
|
};
|
|
18534
|
-
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes) => {
|
|
18535
|
-
if (!
|
|
19226
|
+
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
|
|
19227
|
+
if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
|
|
18536
19228
|
const firstParam = functionNode.params?.[0];
|
|
18537
19229
|
const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
|
|
18538
19230
|
if (!stateName) return;
|
|
18539
19231
|
const reportReducerStateMutations = (mutations) => {
|
|
19232
|
+
if (mutations.length === 0) return;
|
|
19233
|
+
if (options.crossFileConsumerCallSite && options.crossFileSourceDisplay) {
|
|
19234
|
+
if (reportedNodes.has(options.crossFileConsumerCallSite)) return;
|
|
19235
|
+
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
19236
|
+
context.report({
|
|
19237
|
+
node: options.crossFileConsumerCallSite,
|
|
19238
|
+
message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
19239
|
+
});
|
|
19240
|
+
return;
|
|
19241
|
+
}
|
|
18540
19242
|
for (const mutation of mutations) {
|
|
18541
19243
|
if (reportedNodes.has(mutation.node)) continue;
|
|
18542
19244
|
reportedNodes.add(mutation.node);
|
|
18543
19245
|
context.report({
|
|
18544
19246
|
node: mutation.node,
|
|
18545
|
-
message: MESSAGE$
|
|
19247
|
+
message: MESSAGE$16
|
|
18546
19248
|
});
|
|
18547
19249
|
}
|
|
18548
19250
|
};
|
|
19251
|
+
let pathBudgetExceeded = false;
|
|
18549
19252
|
const analyzeReducerStatementListByPath = (statements, initialState) => {
|
|
19253
|
+
if (pathBudgetExceeded) return [cloneReducerPathState(initialState)];
|
|
18550
19254
|
let activeStates = [cloneReducerPathState(initialState)];
|
|
18551
19255
|
for (const statement of statements) {
|
|
19256
|
+
if (activeStates.length > 1e3) {
|
|
19257
|
+
pathBudgetExceeded = true;
|
|
19258
|
+
break;
|
|
19259
|
+
}
|
|
18552
19260
|
const nextStates = [];
|
|
18553
19261
|
for (const activeState of activeStates) {
|
|
18554
19262
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
@@ -18617,18 +19325,23 @@ const noMutatingReducerState = defineRule({
|
|
|
18617
19325
|
create: (context) => {
|
|
18618
19326
|
const analyzedReducers = /* @__PURE__ */ new WeakSet();
|
|
18619
19327
|
const reportedNodes = /* @__PURE__ */ new WeakSet();
|
|
19328
|
+
const currentFilename = context.filename;
|
|
18620
19329
|
return { CallExpression(node) {
|
|
18621
19330
|
if (!isCallToImportedReactUseReducer(node)) return;
|
|
18622
|
-
const
|
|
18623
|
-
if (!
|
|
18624
|
-
analyzedReducers.
|
|
18625
|
-
|
|
19331
|
+
const resolved = resolveReducerFunction(node.arguments?.[0], currentFilename);
|
|
19332
|
+
if (!resolved) return;
|
|
19333
|
+
if (analyzedReducers.has(resolved.functionNode)) return;
|
|
19334
|
+
analyzedReducers.add(resolved.functionNode);
|
|
19335
|
+
analyzeReactUseReducerFunctionForStateMutation(context, resolved.functionNode, reportedNodes, {
|
|
19336
|
+
crossFileConsumerCallSite: resolved.crossFileSourceDisplay ? node : null,
|
|
19337
|
+
crossFileSourceDisplay: resolved.crossFileSourceDisplay
|
|
19338
|
+
});
|
|
18626
19339
|
} };
|
|
18627
19340
|
}
|
|
18628
19341
|
});
|
|
18629
19342
|
//#endregion
|
|
18630
19343
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
18631
|
-
const buildMessage$
|
|
19344
|
+
const buildMessage$13 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
18632
19345
|
const noNamespace = defineRule({
|
|
18633
19346
|
id: "no-namespace",
|
|
18634
19347
|
severity: "warn",
|
|
@@ -18640,7 +19353,7 @@ const noNamespace = defineRule({
|
|
|
18640
19353
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
18641
19354
|
context.report({
|
|
18642
19355
|
node: namespaced,
|
|
18643
|
-
message: buildMessage$
|
|
19356
|
+
message: buildMessage$13(fullName)
|
|
18644
19357
|
});
|
|
18645
19358
|
},
|
|
18646
19359
|
CallExpression(node) {
|
|
@@ -18651,7 +19364,7 @@ const noNamespace = defineRule({
|
|
|
18651
19364
|
if (!firstArgument.value.includes(":")) return;
|
|
18652
19365
|
context.report({
|
|
18653
19366
|
node: firstArgument,
|
|
18654
|
-
message: buildMessage$
|
|
19367
|
+
message: buildMessage$13(firstArgument.value)
|
|
18655
19368
|
});
|
|
18656
19369
|
}
|
|
18657
19370
|
})
|
|
@@ -18695,7 +19408,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
18695
19408
|
});
|
|
18696
19409
|
//#endregion
|
|
18697
19410
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
18698
|
-
const buildMessage$
|
|
19411
|
+
const buildMessage$12 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
|
|
18699
19412
|
const INTERACTIVE_HANDLERS = [
|
|
18700
19413
|
"onClick",
|
|
18701
19414
|
"onMouseDown",
|
|
@@ -18721,13 +19434,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
18721
19434
|
}
|
|
18722
19435
|
context.report({
|
|
18723
19436
|
node: node.name,
|
|
18724
|
-
message: buildMessage$
|
|
19437
|
+
message: buildMessage$12(tag)
|
|
18725
19438
|
});
|
|
18726
19439
|
} })
|
|
18727
19440
|
});
|
|
18728
19441
|
//#endregion
|
|
18729
19442
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
18730
|
-
const buildMessage$
|
|
19443
|
+
const buildMessage$11 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
18731
19444
|
const DEFAULT_ALLOWED_ROLES = {
|
|
18732
19445
|
ul: [
|
|
18733
19446
|
"menu",
|
|
@@ -18791,14 +19504,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
18791
19504
|
if (!isInteractiveRole(firstRole)) return;
|
|
18792
19505
|
context.report({
|
|
18793
19506
|
node: roleAttribute,
|
|
18794
|
-
message: buildMessage$
|
|
19507
|
+
message: buildMessage$11(elementType, firstRole)
|
|
18795
19508
|
});
|
|
18796
19509
|
} };
|
|
18797
19510
|
}
|
|
18798
19511
|
});
|
|
18799
19512
|
//#endregion
|
|
18800
19513
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
18801
|
-
const MESSAGE$
|
|
19514
|
+
const MESSAGE$15 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
|
|
18802
19515
|
const resolveSettings$14 = (settings) => {
|
|
18803
19516
|
const reactDoctor = settings?.["react-doctor"];
|
|
18804
19517
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -18825,7 +19538,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18825
19538
|
if (numeric === null) {
|
|
18826
19539
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
18827
19540
|
node: tabIndex,
|
|
18828
|
-
message: MESSAGE$
|
|
19541
|
+
message: MESSAGE$15
|
|
18829
19542
|
});
|
|
18830
19543
|
return;
|
|
18831
19544
|
}
|
|
@@ -18838,7 +19551,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18838
19551
|
if (!roleAttribute) {
|
|
18839
19552
|
context.report({
|
|
18840
19553
|
node: tabIndex,
|
|
18841
|
-
message: MESSAGE$
|
|
19554
|
+
message: MESSAGE$15
|
|
18842
19555
|
});
|
|
18843
19556
|
return;
|
|
18844
19557
|
}
|
|
@@ -18852,7 +19565,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18852
19565
|
}
|
|
18853
19566
|
context.report({
|
|
18854
19567
|
node: tabIndex,
|
|
18855
|
-
message: MESSAGE$
|
|
19568
|
+
message: MESSAGE$15
|
|
18856
19569
|
});
|
|
18857
19570
|
} };
|
|
18858
19571
|
}
|
|
@@ -19381,6 +20094,63 @@ const noPropCallbackInEffect = defineRule({
|
|
|
19381
20094
|
}
|
|
19382
20095
|
});
|
|
19383
20096
|
//#endregion
|
|
20097
|
+
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
20098
|
+
const PROP_TYPES_PROPERTY = "propTypes";
|
|
20099
|
+
const isPropTypesKey = (key, computed) => {
|
|
20100
|
+
if (!key) return false;
|
|
20101
|
+
if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
|
|
20102
|
+
return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
|
|
20103
|
+
};
|
|
20104
|
+
const getComponentNameFromPropTypesAssignment = (left) => {
|
|
20105
|
+
if (!isNodeOfType(left, "MemberExpression")) return null;
|
|
20106
|
+
if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
|
|
20107
|
+
if (!isNodeOfType(left.object, "Identifier")) return null;
|
|
20108
|
+
if (!isUppercaseName(left.object.name)) return null;
|
|
20109
|
+
return left.object.name;
|
|
20110
|
+
};
|
|
20111
|
+
const getComponentNameFromClassProperty = (node) => {
|
|
20112
|
+
if (!node.static) return null;
|
|
20113
|
+
if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
|
|
20114
|
+
const classBody = node.parent;
|
|
20115
|
+
if (!isNodeOfType(classBody, "ClassBody")) return null;
|
|
20116
|
+
const classNode = classBody.parent;
|
|
20117
|
+
if (!classNode) return null;
|
|
20118
|
+
if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
|
|
20119
|
+
if (!isNodeOfType(classNode, "ClassExpression")) return null;
|
|
20120
|
+
const declarator = classNode.parent;
|
|
20121
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
20122
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
20123
|
+
if (!isUppercaseName(declarator.id.name)) return null;
|
|
20124
|
+
return declarator.id.name;
|
|
20125
|
+
};
|
|
20126
|
+
const buildMessage$10 = (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`;
|
|
20127
|
+
const noPropTypes = defineRule({
|
|
20128
|
+
id: "no-prop-types",
|
|
20129
|
+
requires: ["react:19"],
|
|
20130
|
+
tags: ["test-noise"],
|
|
20131
|
+
severity: "warn",
|
|
20132
|
+
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+.",
|
|
20133
|
+
create: (context) => ({
|
|
20134
|
+
AssignmentExpression(node) {
|
|
20135
|
+
if (node.operator !== "=") return;
|
|
20136
|
+
const componentName = getComponentNameFromPropTypesAssignment(node.left);
|
|
20137
|
+
if (!componentName) return;
|
|
20138
|
+
context.report({
|
|
20139
|
+
node: node.left,
|
|
20140
|
+
message: buildMessage$10(componentName)
|
|
20141
|
+
});
|
|
20142
|
+
},
|
|
20143
|
+
PropertyDefinition(node) {
|
|
20144
|
+
const componentName = getComponentNameFromClassProperty(node);
|
|
20145
|
+
if (!componentName) return;
|
|
20146
|
+
context.report({
|
|
20147
|
+
node: node.key,
|
|
20148
|
+
message: buildMessage$10(componentName)
|
|
20149
|
+
});
|
|
20150
|
+
}
|
|
20151
|
+
})
|
|
20152
|
+
});
|
|
20153
|
+
//#endregion
|
|
19384
20154
|
//#region src/plugin/rules/design/no-pure-black-background.ts
|
|
19385
20155
|
const noPureBlackBackground = defineRule({
|
|
19386
20156
|
id: "no-pure-black-background",
|
|
@@ -19412,8 +20182,94 @@ const noPureBlackBackground = defineRule({
|
|
|
19412
20182
|
})
|
|
19413
20183
|
});
|
|
19414
20184
|
//#endregion
|
|
20185
|
+
//#region src/plugin/rules/correctness/no-random-key.ts
|
|
20186
|
+
const ALWAYS_FRESH_DIRECT_CALLEES = new Set([
|
|
20187
|
+
"nanoid",
|
|
20188
|
+
"uuid",
|
|
20189
|
+
"uuidv4",
|
|
20190
|
+
"uuidV4",
|
|
20191
|
+
"v4",
|
|
20192
|
+
"cuid",
|
|
20193
|
+
"cuid2",
|
|
20194
|
+
"createId",
|
|
20195
|
+
"ulid",
|
|
20196
|
+
"objectid",
|
|
20197
|
+
"ObjectId",
|
|
20198
|
+
"shortid"
|
|
20199
|
+
]);
|
|
20200
|
+
const ALWAYS_FRESH_MEMBER_RECEIVERS = new Map([
|
|
20201
|
+
["Math", new Set(["random"])],
|
|
20202
|
+
["Date", new Set(["now"])],
|
|
20203
|
+
["performance", new Set(["now"])],
|
|
20204
|
+
["crypto", new Set([
|
|
20205
|
+
"randomUUID",
|
|
20206
|
+
"getRandomValues",
|
|
20207
|
+
"randomBytes"
|
|
20208
|
+
])]
|
|
20209
|
+
]);
|
|
20210
|
+
const isAlwaysFreshExpression = (expression) => {
|
|
20211
|
+
const stripped = stripParenExpression(expression);
|
|
20212
|
+
if (isNodeOfType(stripped, "NewExpression")) {
|
|
20213
|
+
if (isNodeOfType(stripped.callee, "Identifier") && stripped.callee.name === "Date") return "new Date()";
|
|
20214
|
+
}
|
|
20215
|
+
if (!isNodeOfType(stripped, "CallExpression")) return null;
|
|
20216
|
+
const callee = stripped.callee;
|
|
20217
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
20218
|
+
if (!ALWAYS_FRESH_DIRECT_CALLEES.has(callee.name)) return null;
|
|
20219
|
+
const binding = findVariableInitializer(callee, callee.name);
|
|
20220
|
+
if (binding?.initializer && !isNodeOfType(binding.initializer, "ImportSpecifier") && !isNodeOfType(binding.initializer, "ImportDefaultSpecifier") && !isNodeOfType(binding.initializer, "ImportNamespaceSpecifier")) return null;
|
|
20221
|
+
return `${callee.name}()`;
|
|
20222
|
+
}
|
|
20223
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
20224
|
+
const receiver = callee.object;
|
|
20225
|
+
const property = callee.property;
|
|
20226
|
+
if (!isNodeOfType(property, "Identifier")) return null;
|
|
20227
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
20228
|
+
if (ALWAYS_FRESH_MEMBER_RECEIVERS.get(receiver.name)?.has(property.name)) return `${receiver.name}.${property.name}()`;
|
|
20229
|
+
}
|
|
20230
|
+
}
|
|
20231
|
+
return null;
|
|
20232
|
+
};
|
|
20233
|
+
const variableLabelForUpdateArgument = (argument) => {
|
|
20234
|
+
if (!argument) return "counter";
|
|
20235
|
+
const stripped = stripParenExpression(argument);
|
|
20236
|
+
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
20237
|
+
if (isNodeOfType(stripped, "MemberExpression") && !stripped.computed && isNodeOfType(stripped.property, "Identifier")) return stripped.property.name;
|
|
20238
|
+
return "counter";
|
|
20239
|
+
};
|
|
20240
|
+
const looksLikeFreshUpdateExpression = (expression) => {
|
|
20241
|
+
const stripped = stripParenExpression(expression);
|
|
20242
|
+
if (isNodeOfType(stripped, "UpdateExpression")) {
|
|
20243
|
+
const label = variableLabelForUpdateArgument(stripped.argument);
|
|
20244
|
+
return stripped.prefix ? `${stripped.operator}${label}` : `${label}${stripped.operator}`;
|
|
20245
|
+
}
|
|
20246
|
+
if (isNodeOfType(stripped, "AssignmentExpression") && (stripped.operator === "+=" || stripped.operator === "-=")) return `${stripped.operator} side-effect`;
|
|
20247
|
+
return null;
|
|
20248
|
+
};
|
|
20249
|
+
const noRandomKey = defineRule({
|
|
20250
|
+
id: "no-random-key",
|
|
20251
|
+
severity: "error",
|
|
20252
|
+
category: "Correctness",
|
|
20253
|
+
recommendation: "Use a stable identifier from the item itself (`item.id`, a hash of the content, or the item's index when the list order is stable). Never derive the key from a fresh-each-call API.",
|
|
20254
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
20255
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
20256
|
+
if (node.name.name !== "key") return;
|
|
20257
|
+
if (!node.value) return;
|
|
20258
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
20259
|
+
const inner = node.value.expression;
|
|
20260
|
+
if (!inner) return;
|
|
20261
|
+
if (inner.type === "JSXEmptyExpression") return;
|
|
20262
|
+
const freshDescription = isAlwaysFreshExpression(inner) ?? looksLikeFreshUpdateExpression(inner);
|
|
20263
|
+
if (!freshDescription) return;
|
|
20264
|
+
context.report({
|
|
20265
|
+
node: node.value,
|
|
20266
|
+
message: `\`key={${freshDescription}}\` produces a new value on every render. Every list item is treated as a brand-new component — React unmounts and remounts the entire subtree, resetting state/focus/scroll and defeating reconciliation. Use a stable id from the item itself.`
|
|
20267
|
+
});
|
|
20268
|
+
} })
|
|
20269
|
+
});
|
|
20270
|
+
//#endregion
|
|
19415
20271
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
19416
|
-
const MESSAGE$
|
|
20272
|
+
const MESSAGE$14 = "`React.Children` is uncommon and leads to fragile components.";
|
|
19417
20273
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
19418
20274
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
19419
20275
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -19438,13 +20294,13 @@ const noReactChildren = defineRule({
|
|
|
19438
20294
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
19439
20295
|
context.report({
|
|
19440
20296
|
node: calleeOuter,
|
|
19441
|
-
message: MESSAGE$
|
|
20297
|
+
message: MESSAGE$14
|
|
19442
20298
|
});
|
|
19443
20299
|
return;
|
|
19444
20300
|
}
|
|
19445
20301
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
19446
20302
|
node: calleeOuter,
|
|
19447
|
-
message: MESSAGE$
|
|
20303
|
+
message: MESSAGE$14
|
|
19448
20304
|
});
|
|
19449
20305
|
} })
|
|
19450
20306
|
});
|
|
@@ -19640,7 +20496,7 @@ const getTagsForRole = (role) => {
|
|
|
19640
20496
|
};
|
|
19641
20497
|
//#endregion
|
|
19642
20498
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
19643
|
-
const buildMessage$
|
|
20499
|
+
const buildMessage$9 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
19644
20500
|
const resolveSettings$13 = (settings) => {
|
|
19645
20501
|
const reactDoctor = settings?.["react-doctor"];
|
|
19646
20502
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -19663,14 +20519,14 @@ const noRedundantRoles = defineRule({
|
|
|
19663
20519
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
19664
20520
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
19665
20521
|
node: roleAttr,
|
|
19666
|
-
message: buildMessage$
|
|
20522
|
+
message: buildMessage$9(tag, role)
|
|
19667
20523
|
});
|
|
19668
20524
|
} };
|
|
19669
20525
|
}
|
|
19670
20526
|
});
|
|
19671
20527
|
//#endregion
|
|
19672
20528
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
19673
|
-
const buildMessage$
|
|
20529
|
+
const buildMessage$8 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
19674
20530
|
const isPureComponentSuper = (superClass) => {
|
|
19675
20531
|
if (!superClass) return false;
|
|
19676
20532
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -19702,7 +20558,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
19702
20558
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
19703
20559
|
context.report({
|
|
19704
20560
|
node: reportNode,
|
|
19705
|
-
message: buildMessage$
|
|
20561
|
+
message: buildMessage$8(className)
|
|
19706
20562
|
});
|
|
19707
20563
|
};
|
|
19708
20564
|
return {
|
|
@@ -19761,7 +20617,7 @@ const noRenderPropChildren = defineRule({
|
|
|
19761
20617
|
});
|
|
19762
20618
|
//#endregion
|
|
19763
20619
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
19764
|
-
const MESSAGE$
|
|
20620
|
+
const MESSAGE$13 = "Do not use the return value from `ReactDOM.render`.";
|
|
19765
20621
|
const isReactDomRenderCall = (node) => {
|
|
19766
20622
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19767
20623
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -19784,7 +20640,7 @@ const noRenderReturnValue = defineRule({
|
|
|
19784
20640
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
19785
20641
|
context.report({
|
|
19786
20642
|
node: node.callee,
|
|
19787
|
-
message: MESSAGE$
|
|
20643
|
+
message: MESSAGE$13
|
|
19788
20644
|
});
|
|
19789
20645
|
} })
|
|
19790
20646
|
});
|
|
@@ -20179,7 +21035,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
20179
21035
|
const isInsideServerOnlyScope = (node) => {
|
|
20180
21036
|
let currentNode = node.parent ?? null;
|
|
20181
21037
|
while (currentNode) {
|
|
20182
|
-
if (isFunctionLike$
|
|
21038
|
+
if (isFunctionLike$2(currentNode)) {
|
|
20183
21039
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
20184
21040
|
}
|
|
20185
21041
|
currentNode = currentNode.parent ?? null;
|
|
@@ -20193,7 +21049,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
20193
21049
|
severity: "warn",
|
|
20194
21050
|
recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
|
|
20195
21051
|
create: (context) => {
|
|
20196
|
-
const filename = normalizeFilename$1(context.
|
|
21052
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
20197
21053
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
20198
21054
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
20199
21055
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -20233,6 +21089,363 @@ const noSecretsInClientCode = defineRule({
|
|
|
20233
21089
|
}
|
|
20234
21090
|
});
|
|
20235
21091
|
//#endregion
|
|
21092
|
+
//#region src/plugin/rules/state-and-effects/no-self-updating-effect.ts
|
|
21093
|
+
const doesConstructFreshReference = (node) => isNodeOfType(node, "ArrayExpression") || isNodeOfType(node, "ObjectExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "Literal") && "regex" in node;
|
|
21094
|
+
const expressionReadsStateValue = (node, stateName) => {
|
|
21095
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) return false;
|
|
21096
|
+
if (isNodeOfType(node, "Identifier")) return node.name === stateName;
|
|
21097
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21098
|
+
if (expressionReadsStateValue(node.object, stateName)) return true;
|
|
21099
|
+
return node.computed ? expressionReadsStateValue(node.property, stateName) : false;
|
|
21100
|
+
}
|
|
21101
|
+
if (isNodeOfType(node, "Property")) {
|
|
21102
|
+
if (node.computed && expressionReadsStateValue(node.key, stateName)) return true;
|
|
21103
|
+
return expressionReadsStateValue(node.value, stateName);
|
|
21104
|
+
}
|
|
21105
|
+
const nodeRecord = node;
|
|
21106
|
+
for (const childKey of Object.keys(nodeRecord)) {
|
|
21107
|
+
if (childKey === "parent" || childKey === "type") continue;
|
|
21108
|
+
const childValue = nodeRecord[childKey];
|
|
21109
|
+
if (Array.isArray(childValue)) {
|
|
21110
|
+
for (const childArrayItem of childValue) if (isAstNode(childArrayItem) && expressionReadsStateValue(childArrayItem, stateName)) return true;
|
|
21111
|
+
} else if (isAstNode(childValue) && expressionReadsStateValue(childValue, stateName)) return true;
|
|
21112
|
+
}
|
|
21113
|
+
return false;
|
|
21114
|
+
};
|
|
21115
|
+
const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
21116
|
+
const firstArgument = setterCall.arguments?.[0];
|
|
21117
|
+
if (!firstArgument) return false;
|
|
21118
|
+
const argument = stripParenExpression(firstArgument);
|
|
21119
|
+
if (isNodeOfType(argument, "Identifier") && argument.name === stateName) return false;
|
|
21120
|
+
if (isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression")) return true;
|
|
21121
|
+
if (doesConstructFreshReference(argument)) return true;
|
|
21122
|
+
return expressionReadsStateValue(argument, stateName);
|
|
21123
|
+
};
|
|
21124
|
+
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
21125
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
21126
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
21127
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
21128
|
+
if (!setterNames.has(expression.callee.name)) return null;
|
|
21129
|
+
return expression;
|
|
21130
|
+
};
|
|
21131
|
+
const collectDependencyStateNames = (depsNode) => {
|
|
21132
|
+
const dependencyNames = /* @__PURE__ */ new Set();
|
|
21133
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyNames;
|
|
21134
|
+
for (const element of depsNode.elements ?? []) if (isNodeOfType(element, "Identifier")) dependencyNames.add(element.name);
|
|
21135
|
+
return dependencyNames;
|
|
21136
|
+
};
|
|
21137
|
+
const isEarlyReturnGuard = (statement) => {
|
|
21138
|
+
if (!isNodeOfType(statement, "IfStatement")) return false;
|
|
21139
|
+
const consequent = statement.consequent;
|
|
21140
|
+
if (isNodeOfType(consequent, "ReturnStatement")) return true;
|
|
21141
|
+
if (isNodeOfType(consequent, "BlockStatement")) return (consequent.body ?? []).some((inner) => isNodeOfType(inner, "ReturnStatement"));
|
|
21142
|
+
return false;
|
|
21143
|
+
};
|
|
21144
|
+
const numericLiteralValue = (node) => {
|
|
21145
|
+
if (isNodeOfType(node, "Literal") && typeof node.value === "number") return node.value;
|
|
21146
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "-" && isNodeOfType(node.argument, "Literal") && typeof node.argument.value === "number") return -node.argument.value;
|
|
21147
|
+
return null;
|
|
21148
|
+
};
|
|
21149
|
+
const isStateLength = (node, stateName) => {
|
|
21150
|
+
const member = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
21151
|
+
return isNodeOfType(member, "MemberExpression") && !member.computed && isNodeOfType(member.property, "Identifier") && member.property.name === "length" && expressionReadsStateValue(member.object, stateName);
|
|
21152
|
+
};
|
|
21153
|
+
const isNullishLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
21154
|
+
const numericComparisonHolds = (operator, left, right) => {
|
|
21155
|
+
switch (operator) {
|
|
21156
|
+
case "<": return left < right;
|
|
21157
|
+
case "<=": return left <= right;
|
|
21158
|
+
case ">": return left > right;
|
|
21159
|
+
case ">=": return left >= right;
|
|
21160
|
+
case "===":
|
|
21161
|
+
case "==": return left === right;
|
|
21162
|
+
case "!==":
|
|
21163
|
+
case "!=": return left !== right;
|
|
21164
|
+
default: return false;
|
|
21165
|
+
}
|
|
21166
|
+
};
|
|
21167
|
+
const guardExitsWhenStateEmpty = (test, stateName) => {
|
|
21168
|
+
const node = isNodeOfType(test, "ChainExpression") ? test.expression : test;
|
|
21169
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return isStateLength(node.argument, stateName);
|
|
21170
|
+
if (isNodeOfType(node, "LogicalExpression")) {
|
|
21171
|
+
if (node.operator === "||") return guardExitsWhenStateEmpty(node.left, stateName) || guardExitsWhenStateEmpty(node.right, stateName);
|
|
21172
|
+
if (node.operator === "&&") return guardExitsWhenStateEmpty(node.left, stateName) && guardExitsWhenStateEmpty(node.right, stateName);
|
|
21173
|
+
return false;
|
|
21174
|
+
}
|
|
21175
|
+
if (isNodeOfType(node, "BinaryExpression")) {
|
|
21176
|
+
const leftIsLength = isStateLength(node.left, stateName);
|
|
21177
|
+
const rightIsLength = isStateLength(node.right, stateName);
|
|
21178
|
+
if (leftIsLength || rightIsLength) {
|
|
21179
|
+
const other = numericLiteralValue(leftIsLength ? node.right : node.left);
|
|
21180
|
+
if (other === null) return false;
|
|
21181
|
+
return leftIsLength ? numericComparisonHolds(node.operator, 0, other) : numericComparisonHolds(node.operator, other, 0);
|
|
21182
|
+
}
|
|
21183
|
+
if (node.operator === "==" || node.operator === "===") {
|
|
21184
|
+
if (expressionReadsStateValue(node.left, stateName) && isNullishLiteral(node.right)) return true;
|
|
21185
|
+
if (expressionReadsStateValue(node.right, stateName) && isNullishLiteral(node.left)) return true;
|
|
21186
|
+
}
|
|
21187
|
+
return false;
|
|
21188
|
+
}
|
|
21189
|
+
return false;
|
|
21190
|
+
};
|
|
21191
|
+
const isEmptyOrFalsyValue = (node) => {
|
|
21192
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).length === 0;
|
|
21193
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).length === 0;
|
|
21194
|
+
if (isNodeOfType(node, "Literal")) return node.value === null || node.value === "" || node.value === 0 || node.value === false;
|
|
21195
|
+
if (isNodeOfType(node, "Identifier")) return node.name === "undefined";
|
|
21196
|
+
return false;
|
|
21197
|
+
};
|
|
21198
|
+
const functionReturnExpression = (fn) => {
|
|
21199
|
+
if (!isNodeOfType(fn, "ArrowFunctionExpression") && !isNodeOfType(fn, "FunctionExpression")) return null;
|
|
21200
|
+
if (!isNodeOfType(fn.body, "BlockStatement")) return fn.body ? stripParenExpression(fn.body) : null;
|
|
21201
|
+
for (const statement of fn.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) return stripParenExpression(statement.argument);
|
|
21202
|
+
return null;
|
|
21203
|
+
};
|
|
21204
|
+
const isLengthReducingUpdater = (node) => {
|
|
21205
|
+
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
21206
|
+
const firstParameter = node.params?.[0];
|
|
21207
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
21208
|
+
const returned = functionReturnExpression(node);
|
|
21209
|
+
if (!returned || !isNodeOfType(returned, "CallExpression")) return false;
|
|
21210
|
+
const callee = returned.callee;
|
|
21211
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
21212
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== firstParameter.name) return false;
|
|
21213
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "slice") return false;
|
|
21214
|
+
const sliceStart = numericLiteralValue(returned.arguments?.[0]);
|
|
21215
|
+
return sliceStart !== null && sliceStart >= 1;
|
|
21216
|
+
};
|
|
21217
|
+
const writeProvablyConverges = (setterArgument, stateName, earlyReturnGuardTests) => {
|
|
21218
|
+
if (!isEmptyOrFalsyValue(setterArgument) && !isLengthReducingUpdater(setterArgument)) return false;
|
|
21219
|
+
return earlyReturnGuardTests.some((test) => guardExitsWhenStateEmpty(test, stateName));
|
|
21220
|
+
};
|
|
21221
|
+
const SYMBOLIC_DEPTH_LIMIT = 16;
|
|
21222
|
+
const unwrapChain = (node) => {
|
|
21223
|
+
let current = node;
|
|
21224
|
+
for (;;) {
|
|
21225
|
+
const withoutParens = stripParenExpression(current);
|
|
21226
|
+
if (withoutParens !== current) {
|
|
21227
|
+
current = withoutParens;
|
|
21228
|
+
continue;
|
|
21229
|
+
}
|
|
21230
|
+
if (isNodeOfType(current, "ChainExpression")) {
|
|
21231
|
+
current = current.expression;
|
|
21232
|
+
continue;
|
|
21233
|
+
}
|
|
21234
|
+
return current;
|
|
21235
|
+
}
|
|
21236
|
+
};
|
|
21237
|
+
const isUndefinedValue = (node) => isNodeOfType(node, "Identifier") && node.name === "undefined" || isNodeOfType(node, "Literal") && node.value === null;
|
|
21238
|
+
const literalsEqual = (a, b) => isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal") && a.value === b.value;
|
|
21239
|
+
const resolveValueNode = (node, writes, depth, seen) => {
|
|
21240
|
+
if (depth > SYMBOLIC_DEPTH_LIMIT) return null;
|
|
21241
|
+
const current = unwrapChain(node);
|
|
21242
|
+
if (isNodeOfType(current, "Identifier")) {
|
|
21243
|
+
if (seen.has(current.name)) return null;
|
|
21244
|
+
const written = writes.get(current.name);
|
|
21245
|
+
if (written) return resolveValueNode(written, writes, depth + 1, new Set(seen).add(current.name));
|
|
21246
|
+
return current;
|
|
21247
|
+
}
|
|
21248
|
+
if (isNodeOfType(current, "Literal") || isNodeOfType(current, "ArrayExpression") || isNodeOfType(current, "ObjectExpression")) return current;
|
|
21249
|
+
if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier")) {
|
|
21250
|
+
const objectValue = resolveValueNode(current.object, writes, depth + 1, seen);
|
|
21251
|
+
if (objectValue && isNodeOfType(objectValue, "ObjectExpression")) {
|
|
21252
|
+
const propertyKey = current.property.name;
|
|
21253
|
+
const properties = objectValue.properties ?? [];
|
|
21254
|
+
for (let index = properties.length - 1; index >= 0; index--) {
|
|
21255
|
+
const property = properties[index];
|
|
21256
|
+
if (isNodeOfType(property, "SpreadElement")) return null;
|
|
21257
|
+
if (isNodeOfType(property, "Property") && !property.computed && (isNodeOfType(property.key, "Identifier") && property.key.name === propertyKey || isNodeOfType(property.key, "Literal") && property.key.value === propertyKey)) return resolveValueNode(property.value, writes, depth + 1, seen);
|
|
21258
|
+
}
|
|
21259
|
+
}
|
|
21260
|
+
return null;
|
|
21261
|
+
}
|
|
21262
|
+
return null;
|
|
21263
|
+
};
|
|
21264
|
+
const resolveToNumber = (node, writes, depth, seen) => {
|
|
21265
|
+
const value = resolveValueNode(node, writes, depth, seen);
|
|
21266
|
+
if (value && isNodeOfType(value, "Literal") && typeof value.value === "number") return value.value;
|
|
21267
|
+
const current = unwrapChain(node);
|
|
21268
|
+
if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "length") {
|
|
21269
|
+
const objectValue = resolveValueNode(current.object, writes, depth, seen);
|
|
21270
|
+
if (objectValue && isNodeOfType(objectValue, "ArrayExpression")) {
|
|
21271
|
+
const elements = objectValue.elements ?? [];
|
|
21272
|
+
if (!elements.some((element) => element && isNodeOfType(element, "SpreadElement"))) return elements.length;
|
|
21273
|
+
}
|
|
21274
|
+
}
|
|
21275
|
+
return null;
|
|
21276
|
+
};
|
|
21277
|
+
const provablyEqualAfterWrites = (left, right, writes, depth, seen) => {
|
|
21278
|
+
const leftNumber = resolveToNumber(left, writes, depth, seen);
|
|
21279
|
+
const rightNumber = resolveToNumber(right, writes, depth, seen);
|
|
21280
|
+
if (leftNumber !== null && rightNumber !== null) return leftNumber === rightNumber;
|
|
21281
|
+
const a = resolveValueNode(left, writes, depth, seen);
|
|
21282
|
+
const b = resolveValueNode(right, writes, depth, seen);
|
|
21283
|
+
if (!a || !b) return false;
|
|
21284
|
+
if (literalsEqual(a, b)) return true;
|
|
21285
|
+
if (isUndefinedValue(a) && isUndefinedValue(b)) return true;
|
|
21286
|
+
return isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier") && a.name === b.name;
|
|
21287
|
+
};
|
|
21288
|
+
const provablyFalsyAfterWrites = (node, writes, depth, seen) => {
|
|
21289
|
+
const value = resolveValueNode(node, writes, depth, seen);
|
|
21290
|
+
if (value) {
|
|
21291
|
+
if (isUndefinedValue(value)) return true;
|
|
21292
|
+
if (isNodeOfType(value, "Literal")) return value.value === null || value.value === 0 || value.value === false || value.value === "";
|
|
21293
|
+
}
|
|
21294
|
+
return resolveToNumber(node, writes, depth, seen) === 0;
|
|
21295
|
+
};
|
|
21296
|
+
const guardProvenAfterWrites = (test, writes, depth, seen) => {
|
|
21297
|
+
if (depth > SYMBOLIC_DEPTH_LIMIT) return false;
|
|
21298
|
+
const node = unwrapChain(test);
|
|
21299
|
+
if (isNodeOfType(node, "LogicalExpression")) {
|
|
21300
|
+
if (node.operator === "&&") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) && guardProvenAfterWrites(node.right, writes, depth + 1, seen);
|
|
21301
|
+
if (node.operator === "||") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) || guardProvenAfterWrites(node.right, writes, depth + 1, seen);
|
|
21302
|
+
return false;
|
|
21303
|
+
}
|
|
21304
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return provablyFalsyAfterWrites(node.argument, writes, depth + 1, seen);
|
|
21305
|
+
if (isNodeOfType(node, "BinaryExpression")) {
|
|
21306
|
+
if (node.operator === "===" || node.operator === "==") return provablyEqualAfterWrites(node.left, node.right, writes, depth + 1, seen);
|
|
21307
|
+
const leftNumber = resolveToNumber(node.left, writes, depth + 1, seen);
|
|
21308
|
+
const rightNumber = resolveToNumber(node.right, writes, depth + 1, seen);
|
|
21309
|
+
if (leftNumber !== null && rightNumber !== null) return numericComparisonHolds(node.operator, leftNumber, rightNumber);
|
|
21310
|
+
return false;
|
|
21311
|
+
}
|
|
21312
|
+
const value = resolveValueNode(node, writes, depth + 1, seen);
|
|
21313
|
+
if (value) {
|
|
21314
|
+
if (isNodeOfType(value, "ArrayExpression") || isNodeOfType(value, "ObjectExpression")) return true;
|
|
21315
|
+
if (isNodeOfType(value, "Literal")) return Boolean(value.value);
|
|
21316
|
+
}
|
|
21317
|
+
return false;
|
|
21318
|
+
};
|
|
21319
|
+
const collectTopLevelWrites = (statements, setterNameToStateName, setterNames) => {
|
|
21320
|
+
const writes = /* @__PURE__ */ new Map();
|
|
21321
|
+
const setterCallNodes = /* @__PURE__ */ new Set();
|
|
21322
|
+
for (const statement of statements) {
|
|
21323
|
+
const setterCall = getUnconditionalSetterCall(statement, setterNames);
|
|
21324
|
+
if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
|
|
21325
|
+
setterCallNodes.add(setterCall);
|
|
21326
|
+
const stateName = setterNameToStateName.get(setterCall.callee.name);
|
|
21327
|
+
if (!stateName) continue;
|
|
21328
|
+
const argument = setterCall.arguments?.[0];
|
|
21329
|
+
if (!argument) continue;
|
|
21330
|
+
const newValue = isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression") ? functionReturnExpression(argument) : stripParenExpression(argument);
|
|
21331
|
+
if (newValue) writes.set(stateName, newValue);
|
|
21332
|
+
}
|
|
21333
|
+
return {
|
|
21334
|
+
writes,
|
|
21335
|
+
setterCallNodes
|
|
21336
|
+
};
|
|
21337
|
+
};
|
|
21338
|
+
const everySetterCall = (root, setterName, inspect) => {
|
|
21339
|
+
let ok = true;
|
|
21340
|
+
const visit = (node) => {
|
|
21341
|
+
if (!ok) return;
|
|
21342
|
+
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === setterName && !inspect(node)) {
|
|
21343
|
+
ok = false;
|
|
21344
|
+
return;
|
|
21345
|
+
}
|
|
21346
|
+
const record = node;
|
|
21347
|
+
for (const key of Object.keys(record)) {
|
|
21348
|
+
if (key === "parent" || key === "type") continue;
|
|
21349
|
+
const child = record[key];
|
|
21350
|
+
if (Array.isArray(child)) {
|
|
21351
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
21352
|
+
} else if (isAstNode(child)) visit(child);
|
|
21353
|
+
if (!ok) return;
|
|
21354
|
+
}
|
|
21355
|
+
};
|
|
21356
|
+
visit(root);
|
|
21357
|
+
return ok;
|
|
21358
|
+
};
|
|
21359
|
+
const everyWriteToStateDrivesTowardEmpty = (callbackBody, setterName) => everySetterCall(callbackBody, setterName, (call) => {
|
|
21360
|
+
const argument = call.arguments?.[0];
|
|
21361
|
+
if (!argument) return true;
|
|
21362
|
+
const value = stripParenExpression(argument);
|
|
21363
|
+
return isEmptyOrFalsyValue(value) || isLengthReducingUpdater(value);
|
|
21364
|
+
});
|
|
21365
|
+
const everySetterCallIsTopLevel = (callbackBody, setterNames, topLevelSetterCalls) => {
|
|
21366
|
+
let safe = true;
|
|
21367
|
+
const visit = (node) => {
|
|
21368
|
+
if (!safe) return;
|
|
21369
|
+
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && setterNames.has(node.callee.name) && !topLevelSetterCalls.has(node)) {
|
|
21370
|
+
safe = false;
|
|
21371
|
+
return;
|
|
21372
|
+
}
|
|
21373
|
+
const record = node;
|
|
21374
|
+
for (const key of Object.keys(record)) {
|
|
21375
|
+
if (key === "parent" || key === "type") continue;
|
|
21376
|
+
const child = record[key];
|
|
21377
|
+
if (Array.isArray(child)) {
|
|
21378
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
21379
|
+
} else if (isAstNode(child)) visit(child);
|
|
21380
|
+
if (!safe) return;
|
|
21381
|
+
}
|
|
21382
|
+
};
|
|
21383
|
+
visit(callbackBody);
|
|
21384
|
+
return safe;
|
|
21385
|
+
};
|
|
21386
|
+
const noSelfUpdatingEffect = defineRule({
|
|
21387
|
+
id: "no-self-updating-effect",
|
|
21388
|
+
severity: "warn",
|
|
21389
|
+
tags: ["test-noise"],
|
|
21390
|
+
recommendation: "Remove the feedback loop: derive the value during render, move the write into an event handler, or guard the update so it reaches a fixed point. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
21391
|
+
create: (context) => {
|
|
21392
|
+
const checkFunctionScope = (functionBody) => {
|
|
21393
|
+
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
21394
|
+
const useStateBindings = collectUseStateBindings(functionBody);
|
|
21395
|
+
if (useStateBindings.length === 0) return;
|
|
21396
|
+
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
21397
|
+
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
21398
|
+
const setterNames = new Set(setterNameToStateName.keys());
|
|
21399
|
+
for (const statement of functionBody.body ?? []) {
|
|
21400
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
21401
|
+
const effectCall = statement.expression;
|
|
21402
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
21403
|
+
if (!isHookCall$1(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
21404
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
21405
|
+
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
21406
|
+
if (dependencyStateNames.size === 0) continue;
|
|
21407
|
+
const callback = getEffectCallback(effectCall);
|
|
21408
|
+
if (!callback) continue;
|
|
21409
|
+
const callbackStatements = getCallbackStatements(callback);
|
|
21410
|
+
const firstWriteIndex = callbackStatements.findIndex((candidate) => getUnconditionalSetterCall(candidate, setterNames) !== null);
|
|
21411
|
+
const guardCutoff = firstWriteIndex < 0 ? callbackStatements.length : firstWriteIndex;
|
|
21412
|
+
const earlyReturnGuardTests = callbackStatements.slice(0, guardCutoff).filter(isEarlyReturnGuard).map((guard) => guard.test);
|
|
21413
|
+
const { writes: topLevelWrites, setterCallNodes } = collectTopLevelWrites(callbackStatements, setterNameToStateName, setterNames);
|
|
21414
|
+
if (everySetterCallIsTopLevel(callback, setterNames, setterCallNodes) && earlyReturnGuardTests.some((test) => guardProvenAfterWrites(test, topLevelWrites, 0, /* @__PURE__ */ new Set()))) continue;
|
|
21415
|
+
const reportedStateNames = /* @__PURE__ */ new Set();
|
|
21416
|
+
for (const callbackStatement of callbackStatements) {
|
|
21417
|
+
const setterCall = getUnconditionalSetterCall(callbackStatement, setterNames);
|
|
21418
|
+
if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
|
|
21419
|
+
const stateName = setterNameToStateName.get(setterCall.callee.name);
|
|
21420
|
+
if (!stateName || !dependencyStateNames.has(stateName)) continue;
|
|
21421
|
+
if (reportedStateNames.has(stateName)) continue;
|
|
21422
|
+
if (!isNonSettlingSetterArgument(setterCall, stateName)) continue;
|
|
21423
|
+
const firstArgument = setterCall.arguments?.[0];
|
|
21424
|
+
if (firstArgument && writeProvablyConverges(stripParenExpression(firstArgument), stateName, earlyReturnGuardTests) && everyWriteToStateDrivesTowardEmpty(callback, setterCall.callee.name)) continue;
|
|
21425
|
+
reportedStateNames.add(stateName);
|
|
21426
|
+
context.report({
|
|
21427
|
+
node: setterCall,
|
|
21428
|
+
message: `${setterCall.callee.name}() runs unconditionally inside this effect, which depends on \`${stateName}\` — setting the same state the effect reacts to re-runs the effect on every commit and causes a render loop. Derive the value during render, move the write into an event handler, or guard the update so it settles.`
|
|
21429
|
+
});
|
|
21430
|
+
}
|
|
21431
|
+
}
|
|
21432
|
+
};
|
|
21433
|
+
return {
|
|
21434
|
+
FunctionDeclaration(node) {
|
|
21435
|
+
const functionName = node.id?.name;
|
|
21436
|
+
if (!functionName || !isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
21437
|
+
checkFunctionScope(node.body);
|
|
21438
|
+
},
|
|
21439
|
+
VariableDeclarator(node) {
|
|
21440
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
21441
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
21442
|
+
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
21443
|
+
checkFunctionScope(node.init.body);
|
|
21444
|
+
}
|
|
21445
|
+
};
|
|
21446
|
+
}
|
|
21447
|
+
});
|
|
21448
|
+
//#endregion
|
|
20236
21449
|
//#region src/plugin/utils/get-parent-component.ts
|
|
20237
21450
|
const getParentComponent = (node) => {
|
|
20238
21451
|
let ancestor = node.parent;
|
|
@@ -20244,7 +21457,7 @@ const getParentComponent = (node) => {
|
|
|
20244
21457
|
};
|
|
20245
21458
|
//#endregion
|
|
20246
21459
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
20247
|
-
const MESSAGE$
|
|
21460
|
+
const MESSAGE$12 = "Do not use `this.setState` in components.";
|
|
20248
21461
|
const noSetState = defineRule({
|
|
20249
21462
|
id: "no-set-state",
|
|
20250
21463
|
severity: "warn",
|
|
@@ -20258,7 +21471,7 @@ const noSetState = defineRule({
|
|
|
20258
21471
|
if (!getParentComponent(node)) return;
|
|
20259
21472
|
context.report({
|
|
20260
21473
|
node: node.callee,
|
|
20261
|
-
message: MESSAGE$
|
|
21474
|
+
message: MESSAGE$12
|
|
20262
21475
|
});
|
|
20263
21476
|
} })
|
|
20264
21477
|
});
|
|
@@ -20417,7 +21630,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
20417
21630
|
};
|
|
20418
21631
|
//#endregion
|
|
20419
21632
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
20420
|
-
const MESSAGE$
|
|
21633
|
+
const MESSAGE$11 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
20421
21634
|
const DEFAULT_HANDLERS = [
|
|
20422
21635
|
"onClick",
|
|
20423
21636
|
"onMouseDown",
|
|
@@ -20448,7 +21661,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20448
21661
|
category: "Accessibility",
|
|
20449
21662
|
create: (context) => {
|
|
20450
21663
|
const settings = resolveSettings$12(context.settings);
|
|
20451
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
21664
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20452
21665
|
return { JSXOpeningElement(node) {
|
|
20453
21666
|
if (isTestlikeFile) return;
|
|
20454
21667
|
let hasNonBlockerHandler = false;
|
|
@@ -20476,7 +21689,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20476
21689
|
if (!roleAttribute || !roleAttribute.value) {
|
|
20477
21690
|
context.report({
|
|
20478
21691
|
node: node.name,
|
|
20479
|
-
message: MESSAGE$
|
|
21692
|
+
message: MESSAGE$11
|
|
20480
21693
|
});
|
|
20481
21694
|
return;
|
|
20482
21695
|
}
|
|
@@ -20486,14 +21699,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
20486
21699
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
20487
21700
|
context.report({
|
|
20488
21701
|
node: node.name,
|
|
20489
|
-
message: MESSAGE$
|
|
21702
|
+
message: MESSAGE$11
|
|
20490
21703
|
});
|
|
20491
21704
|
return;
|
|
20492
21705
|
}
|
|
20493
21706
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
20494
21707
|
context.report({
|
|
20495
21708
|
node: node.name,
|
|
20496
|
-
message: MESSAGE$
|
|
21709
|
+
message: MESSAGE$11
|
|
20497
21710
|
});
|
|
20498
21711
|
} };
|
|
20499
21712
|
}
|
|
@@ -20525,7 +21738,7 @@ const noStringRefs = defineRule({
|
|
|
20525
21738
|
recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
|
|
20526
21739
|
create: (context) => {
|
|
20527
21740
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
20528
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
21741
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20529
21742
|
return {
|
|
20530
21743
|
JSXAttribute(node) {
|
|
20531
21744
|
if (isTestlikeFile) return;
|
|
@@ -20549,7 +21762,7 @@ const noStringRefs = defineRule({
|
|
|
20549
21762
|
});
|
|
20550
21763
|
//#endregion
|
|
20551
21764
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
20552
|
-
const MESSAGE$
|
|
21765
|
+
const MESSAGE$10 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
20553
21766
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
20554
21767
|
let ancestor = node.parent;
|
|
20555
21768
|
while (ancestor) {
|
|
@@ -20617,7 +21830,7 @@ const noThisInSfc = defineRule({
|
|
|
20617
21830
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
20618
21831
|
context.report({
|
|
20619
21832
|
node,
|
|
20620
|
-
message: MESSAGE$
|
|
21833
|
+
message: MESSAGE$10
|
|
20621
21834
|
});
|
|
20622
21835
|
} };
|
|
20623
21836
|
}
|
|
@@ -20799,7 +22012,7 @@ const ESCAPED_VERSIONS = {
|
|
|
20799
22012
|
">": "`>` / `>`",
|
|
20800
22013
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
20801
22014
|
};
|
|
20802
|
-
const buildMessage$
|
|
22015
|
+
const buildMessage$7 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
20803
22016
|
const noUnescapedEntities = defineRule({
|
|
20804
22017
|
id: "no-unescaped-entities",
|
|
20805
22018
|
severity: "warn",
|
|
@@ -20810,7 +22023,7 @@ const noUnescapedEntities = defineRule({
|
|
|
20810
22023
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
20811
22024
|
context.report({
|
|
20812
22025
|
node,
|
|
20813
|
-
message: buildMessage$
|
|
22026
|
+
message: buildMessage$7(character)
|
|
20814
22027
|
});
|
|
20815
22028
|
return;
|
|
20816
22029
|
}
|
|
@@ -21831,7 +23044,7 @@ const SAFER_REPLACEMENT = {
|
|
|
21831
23044
|
componentWillUpdate: "componentDidUpdate",
|
|
21832
23045
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
21833
23046
|
};
|
|
21834
|
-
const buildMessage$
|
|
23047
|
+
const buildMessage$6 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
21835
23048
|
const resolveSettings$9 = (settings) => {
|
|
21836
23049
|
const reactDoctor = settings?.["react-doctor"];
|
|
21837
23050
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21879,7 +23092,7 @@ const noUnsafe = defineRule({
|
|
|
21879
23092
|
if (!getParentComponent(node)) return;
|
|
21880
23093
|
context.report({
|
|
21881
23094
|
node: node.key,
|
|
21882
|
-
message: buildMessage$
|
|
23095
|
+
message: buildMessage$6(name)
|
|
21883
23096
|
});
|
|
21884
23097
|
},
|
|
21885
23098
|
Property(node) {
|
|
@@ -21890,7 +23103,7 @@ const noUnsafe = defineRule({
|
|
|
21890
23103
|
if (isEs5Component(ancestor)) {
|
|
21891
23104
|
context.report({
|
|
21892
23105
|
node: node.key,
|
|
21893
|
-
message: buildMessage$
|
|
23106
|
+
message: buildMessage$6(name)
|
|
21894
23107
|
});
|
|
21895
23108
|
return;
|
|
21896
23109
|
}
|
|
@@ -21902,7 +23115,7 @@ const noUnsafe = defineRule({
|
|
|
21902
23115
|
});
|
|
21903
23116
|
//#endregion
|
|
21904
23117
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21905
|
-
const buildMessage$
|
|
23118
|
+
const buildMessage$5 = (parentName, isInProp, allowAsProps) => {
|
|
21906
23119
|
let message = "Don't define components inside another component";
|
|
21907
23120
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21908
23121
|
message += " — extract it to module scope.";
|
|
@@ -21971,7 +23184,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
21971
23184
|
const findEnclosingComponent = (node) => {
|
|
21972
23185
|
let walker = node.parent;
|
|
21973
23186
|
while (walker) {
|
|
21974
|
-
if (isFunctionLike$
|
|
23187
|
+
if (isFunctionLike$2(walker)) {
|
|
21975
23188
|
const componentName = inferFunctionLikeName(walker);
|
|
21976
23189
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
21977
23190
|
component: walker,
|
|
@@ -22137,7 +23350,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22137
23350
|
if (!enclosing) return;
|
|
22138
23351
|
context.report({
|
|
22139
23352
|
node: reportNode,
|
|
22140
|
-
message: buildMessage$
|
|
23353
|
+
message: buildMessage$5(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
22141
23354
|
});
|
|
22142
23355
|
};
|
|
22143
23356
|
const checkFunctionLike = (node) => {
|
|
@@ -22172,15 +23385,6 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22172
23385
|
}
|
|
22173
23386
|
});
|
|
22174
23387
|
//#endregion
|
|
22175
|
-
//#region src/plugin/utils/is-canonical-react-namespace-name.ts
|
|
22176
|
-
const isCanonicalReactNamespaceName = (namespaceName) => {
|
|
22177
|
-
if (namespaceName === "React") return true;
|
|
22178
|
-
if (namespaceName === "react") return true;
|
|
22179
|
-
if (namespaceName.startsWith("_react")) return true;
|
|
22180
|
-
if (namespaceName.startsWith("_React")) return true;
|
|
22181
|
-
return false;
|
|
22182
|
-
};
|
|
22183
|
-
//#endregion
|
|
22184
23388
|
//#region src/plugin/rules/performance/no-usememo-simple-expression.ts
|
|
22185
23389
|
const isSimpleExpression = (node) => {
|
|
22186
23390
|
if (!node) return false;
|
|
@@ -22269,7 +23473,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
22269
23473
|
//#endregion
|
|
22270
23474
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
22271
23475
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
22272
|
-
const MESSAGE$
|
|
23476
|
+
const MESSAGE$9 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
22273
23477
|
const resolveSettings$7 = (settings) => {
|
|
22274
23478
|
const reactDoctor = settings?.["react-doctor"];
|
|
22275
23479
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -22302,7 +23506,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
22302
23506
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
22303
23507
|
context.report({
|
|
22304
23508
|
node: node.callee,
|
|
22305
|
-
message: MESSAGE$
|
|
23509
|
+
message: MESSAGE$9
|
|
22306
23510
|
});
|
|
22307
23511
|
} };
|
|
22308
23512
|
}
|
|
@@ -22660,7 +23864,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
22660
23864
|
};
|
|
22661
23865
|
const onlyExportComponents = defineRule({
|
|
22662
23866
|
id: "only-export-components",
|
|
22663
|
-
severity: "
|
|
23867
|
+
severity: "warn",
|
|
22664
23868
|
recommendation: "Move non-component exports out of files that export components.",
|
|
22665
23869
|
category: "Architecture",
|
|
22666
23870
|
create: (context) => {
|
|
@@ -22671,7 +23875,7 @@ const onlyExportComponents = defineRule({
|
|
|
22671
23875
|
allowConstantExport: settings.allowConstantExport
|
|
22672
23876
|
};
|
|
22673
23877
|
return { Program(node) {
|
|
22674
|
-
if (!isFileNameAllowed(
|
|
23878
|
+
if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
|
|
22675
23879
|
const allNodes = collectAllNodes(node);
|
|
22676
23880
|
const exports = [];
|
|
22677
23881
|
let hasReactExport = false;
|
|
@@ -22915,7 +24119,7 @@ const REACT_HOOK_NAMES = new Set([
|
|
|
22915
24119
|
"useSyncExternalStore",
|
|
22916
24120
|
"useTransition"
|
|
22917
24121
|
]);
|
|
22918
|
-
const buildMessage$
|
|
24122
|
+
const buildMessage$4 = (importedNames) => `Import ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`preact/hooks\` (or \`preact/compat\`) — importing hooks from \`react\` in a pure-Preact project loads a second copy of Preact's hook state and triggers \`__H\` undefined errors.`;
|
|
22919
24123
|
const preactNoReactHooksImport = defineRule({
|
|
22920
24124
|
id: "preact-no-react-hooks-import",
|
|
22921
24125
|
requires: ["pure-preact"],
|
|
@@ -22938,7 +24142,7 @@ const preactNoReactHooksImport = defineRule({
|
|
|
22938
24142
|
});
|
|
22939
24143
|
context.report({
|
|
22940
24144
|
node,
|
|
22941
|
-
message: buildMessage$
|
|
24145
|
+
message: buildMessage$4(importedNames)
|
|
22942
24146
|
});
|
|
22943
24147
|
} })
|
|
22944
24148
|
});
|
|
@@ -22995,7 +24199,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
22995
24199
|
});
|
|
22996
24200
|
//#endregion
|
|
22997
24201
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
22998
|
-
const MESSAGE$
|
|
24202
|
+
const MESSAGE$8 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
|
|
22999
24203
|
const preactPreferOndblclick = defineRule({
|
|
23000
24204
|
id: "preact-prefer-ondblclick",
|
|
23001
24205
|
requires: ["pure-preact"],
|
|
@@ -23009,7 +24213,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
23009
24213
|
if (!onDoubleClickAttribute) return;
|
|
23010
24214
|
context.report({
|
|
23011
24215
|
node: onDoubleClickAttribute,
|
|
23012
|
-
message: MESSAGE$
|
|
24216
|
+
message: MESSAGE$8
|
|
23013
24217
|
});
|
|
23014
24218
|
} })
|
|
23015
24219
|
});
|
|
@@ -23117,7 +24321,7 @@ const preferEs6Class = defineRule({
|
|
|
23117
24321
|
});
|
|
23118
24322
|
//#endregion
|
|
23119
24323
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
23120
|
-
const MESSAGE$
|
|
24324
|
+
const MESSAGE$7 = "Class component should be written as a function component — use hooks instead.";
|
|
23121
24325
|
const resolveSettings$4 = (settings) => {
|
|
23122
24326
|
const reactDoctor = settings?.["react-doctor"];
|
|
23123
24327
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -23155,7 +24359,7 @@ const preferFunctionComponent = defineRule({
|
|
|
23155
24359
|
const reportNode = node.id ?? node;
|
|
23156
24360
|
context.report({
|
|
23157
24361
|
node: reportNode,
|
|
23158
|
-
message: MESSAGE$
|
|
24362
|
+
message: MESSAGE$7
|
|
23159
24363
|
});
|
|
23160
24364
|
};
|
|
23161
24365
|
return {
|
|
@@ -23212,6 +24416,276 @@ const preferHtmlDialog = defineRule({
|
|
|
23212
24416
|
} })
|
|
23213
24417
|
});
|
|
23214
24418
|
//#endregion
|
|
24419
|
+
//#region src/plugin/utils/function-returns-object-literal.ts
|
|
24420
|
+
const unwrapExpression = (node) => {
|
|
24421
|
+
let current = node;
|
|
24422
|
+
for (;;) {
|
|
24423
|
+
if ((current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") && "expression" in current && isAstNode(current.expression)) {
|
|
24424
|
+
current = current.expression;
|
|
24425
|
+
continue;
|
|
24426
|
+
}
|
|
24427
|
+
return current;
|
|
24428
|
+
}
|
|
24429
|
+
};
|
|
24430
|
+
const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
24431
|
+
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
24432
|
+
const body = functionNode.body;
|
|
24433
|
+
if (body && body.type !== "BlockStatement") return unwrapExpression(body).type === "ObjectExpression";
|
|
24434
|
+
}
|
|
24435
|
+
const body = functionNode.body;
|
|
24436
|
+
if (!body || body.type !== "BlockStatement") return false;
|
|
24437
|
+
let returnsObject = false;
|
|
24438
|
+
const visit = (node) => {
|
|
24439
|
+
if (returnsObject) return;
|
|
24440
|
+
if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
|
|
24441
|
+
if (unwrapExpression(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
24442
|
+
return;
|
|
24443
|
+
}
|
|
24444
|
+
const nodeRecord = node;
|
|
24445
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
24446
|
+
if (key === "parent") continue;
|
|
24447
|
+
const child = nodeRecord[key];
|
|
24448
|
+
if (Array.isArray(child)) for (const item of child) {
|
|
24449
|
+
if (!isAstNode(item)) continue;
|
|
24450
|
+
if (FUNCTION_LIKE_TYPES$1.has(item.type)) continue;
|
|
24451
|
+
visit(item);
|
|
24452
|
+
if (returnsObject) return;
|
|
24453
|
+
}
|
|
24454
|
+
else if (isAstNode(child)) {
|
|
24455
|
+
if (FUNCTION_LIKE_TYPES$1.has(child.type)) continue;
|
|
24456
|
+
visit(child);
|
|
24457
|
+
}
|
|
24458
|
+
}
|
|
24459
|
+
};
|
|
24460
|
+
visit(body);
|
|
24461
|
+
return returnsObject;
|
|
24462
|
+
};
|
|
24463
|
+
//#endregion
|
|
24464
|
+
//#region src/plugin/utils/enclosing-component-or-hook-scope.ts
|
|
24465
|
+
const enclosingComponentOrHookScope = (startNode, ownScopeFor) => {
|
|
24466
|
+
const functionNode = nearestEnclosingFunction(startNode);
|
|
24467
|
+
if (!functionNode) return null;
|
|
24468
|
+
const displayName = componentOrHookDisplayNameForFunction(functionNode);
|
|
24469
|
+
if (!displayName) return null;
|
|
24470
|
+
if (!isReactHookName(displayName) && doesFunctionReturnsObjectLiteral(functionNode)) return null;
|
|
24471
|
+
const bodyScope = ownScopeFor(functionNode);
|
|
24472
|
+
if (!bodyScope) return null;
|
|
24473
|
+
return {
|
|
24474
|
+
functionNode,
|
|
24475
|
+
bodyScope,
|
|
24476
|
+
displayName
|
|
24477
|
+
};
|
|
24478
|
+
};
|
|
24479
|
+
//#endregion
|
|
24480
|
+
//#region src/plugin/rules/architecture/prefer-module-scope-pure-function.ts
|
|
24481
|
+
const isAssignedToComponentMember = (functionNode) => {
|
|
24482
|
+
const parent = functionNode.parent;
|
|
24483
|
+
if (!parent) return false;
|
|
24484
|
+
return isNodeOfType(parent, "AssignmentExpression") && isNodeOfType(parent.left, "MemberExpression");
|
|
24485
|
+
};
|
|
24486
|
+
const hasComponentLocalCaptures = (functionNode, bodyScope, scopes) => {
|
|
24487
|
+
const captures = closureCaptures(functionNode, scopes);
|
|
24488
|
+
for (const capture of captures) {
|
|
24489
|
+
const symbol = capture.resolvedSymbol;
|
|
24490
|
+
if (!symbol) continue;
|
|
24491
|
+
if (isDescendantScope(symbol.scope, bodyScope)) return true;
|
|
24492
|
+
}
|
|
24493
|
+
return false;
|
|
24494
|
+
};
|
|
24495
|
+
const preferModuleScopePureFunction = defineRule({
|
|
24496
|
+
id: "prefer-module-scope-pure-function",
|
|
24497
|
+
tags: ["test-noise"],
|
|
24498
|
+
severity: "warn",
|
|
24499
|
+
category: "Architecture",
|
|
24500
|
+
recommendation: "Move the function to module scope (above the component). It doesn't reference any local state, so the per-render allocation is wasted.",
|
|
24501
|
+
create: (context) => {
|
|
24502
|
+
const report = (functionNode, name, componentName) => {
|
|
24503
|
+
context.report({
|
|
24504
|
+
node: functionNode,
|
|
24505
|
+
message: `\`${name}\` inside \`${componentName}\` doesn't close over any local state. Move it to module scope so it isn't reallocated every render and the component file stays focused on rendering logic.`
|
|
24506
|
+
});
|
|
24507
|
+
};
|
|
24508
|
+
const checkNamedFunction = (functionNode, bindingName) => {
|
|
24509
|
+
if (isAssignedToComponentMember(functionNode)) return;
|
|
24510
|
+
const component = enclosingComponentOrHookScope(functionNode, context.scopes.ownScopeFor);
|
|
24511
|
+
if (!component) return;
|
|
24512
|
+
const ownScope = context.scopes.ownScopeFor(functionNode);
|
|
24513
|
+
if (!ownScope) return;
|
|
24514
|
+
if (ownScope === component.bodyScope) return;
|
|
24515
|
+
if (!isDescendantScope(ownScope, component.bodyScope)) return;
|
|
24516
|
+
if (hasComponentLocalCaptures(functionNode, component.bodyScope, context.scopes)) return;
|
|
24517
|
+
report(functionNode, bindingName, component.displayName);
|
|
24518
|
+
};
|
|
24519
|
+
return {
|
|
24520
|
+
VariableDeclarator(node) {
|
|
24521
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
24522
|
+
const initializer = node.init;
|
|
24523
|
+
if (!initializer) return;
|
|
24524
|
+
if (!isNodeOfType(initializer, "ArrowFunctionExpression") && !isNodeOfType(initializer, "FunctionExpression")) return;
|
|
24525
|
+
const bindingName = node.id.name;
|
|
24526
|
+
if (/^[A-Z]/.test(bindingName)) return;
|
|
24527
|
+
checkNamedFunction(initializer, bindingName);
|
|
24528
|
+
},
|
|
24529
|
+
FunctionDeclaration(node) {
|
|
24530
|
+
if (!node.id?.name) return;
|
|
24531
|
+
const bindingName = node.id.name;
|
|
24532
|
+
if (/^[A-Z]/.test(bindingName)) return;
|
|
24533
|
+
checkNamedFunction(node, bindingName);
|
|
24534
|
+
}
|
|
24535
|
+
};
|
|
24536
|
+
}
|
|
24537
|
+
});
|
|
24538
|
+
//#endregion
|
|
24539
|
+
//#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
|
|
24540
|
+
const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
24541
|
+
const isMutationContext = (referenceIdentifier) => {
|
|
24542
|
+
const parent = referenceIdentifier.parent;
|
|
24543
|
+
if (!parent) return false;
|
|
24544
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceIdentifier) return true;
|
|
24545
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceIdentifier) return true;
|
|
24546
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === referenceIdentifier) {
|
|
24547
|
+
const grandparent = parent.parent;
|
|
24548
|
+
if (!grandparent) return false;
|
|
24549
|
+
if (isNodeOfType(grandparent, "AssignmentExpression") && grandparent.left === parent) return true;
|
|
24550
|
+
if (isNodeOfType(grandparent, "UpdateExpression") && grandparent.argument === parent) return true;
|
|
24551
|
+
if (isNodeOfType(grandparent, "UnaryExpression") && grandparent.operator === "delete" && grandparent.argument === parent) return true;
|
|
24552
|
+
if (isNodeOfType(grandparent, "CallExpression") && grandparent.callee === parent && !parent.computed && isNodeOfType(parent.property, "Identifier") && MUTATING_RECEIVER_METHOD_NAMES.has(parent.property.name)) return true;
|
|
24553
|
+
}
|
|
24554
|
+
return false;
|
|
24555
|
+
};
|
|
24556
|
+
const isBindingMutatedAfterInit = (declaratorNode, bodyScope, scopes) => {
|
|
24557
|
+
if (!isNodeOfType(declaratorNode.id, "Identifier")) return false;
|
|
24558
|
+
const symbol = scopes.symbolFor(declaratorNode.id);
|
|
24559
|
+
if (!symbol) return false;
|
|
24560
|
+
for (const reference of symbol.references) {
|
|
24561
|
+
if (reference.identifier === declaratorNode.id) continue;
|
|
24562
|
+
if (reference.identifier === declaratorNode.init) continue;
|
|
24563
|
+
if (!isDescendantScope(reference.scope, bodyScope) && reference.scope !== bodyScope) continue;
|
|
24564
|
+
if (isMutationContext(reference.identifier)) return true;
|
|
24565
|
+
}
|
|
24566
|
+
return false;
|
|
24567
|
+
};
|
|
24568
|
+
const hasComponentLocalReferences = (expression, bodyScope, scopes) => {
|
|
24569
|
+
let foundLocal = false;
|
|
24570
|
+
walkAst(expression, (node) => {
|
|
24571
|
+
if (foundLocal) return false;
|
|
24572
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) {
|
|
24573
|
+
foundLocal = true;
|
|
24574
|
+
return false;
|
|
24575
|
+
}
|
|
24576
|
+
const reference = scopes.referenceFor(node);
|
|
24577
|
+
if (reference?.resolvedSymbol && isDescendantScope(reference.resolvedSymbol.scope, bodyScope)) {
|
|
24578
|
+
foundLocal = true;
|
|
24579
|
+
return false;
|
|
24580
|
+
}
|
|
24581
|
+
});
|
|
24582
|
+
return foundLocal;
|
|
24583
|
+
};
|
|
24584
|
+
const isHoistableValueExpression = (expression) => {
|
|
24585
|
+
const stripped = stripParenExpression(expression);
|
|
24586
|
+
return isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ObjectExpression");
|
|
24587
|
+
};
|
|
24588
|
+
const preferModuleScopeStaticValue = defineRule({
|
|
24589
|
+
id: "prefer-module-scope-static-value",
|
|
24590
|
+
tags: ["test-noise"],
|
|
24591
|
+
severity: "warn",
|
|
24592
|
+
category: "Architecture",
|
|
24593
|
+
recommendation: "Move the constant to module scope (above the component). It doesn't reference any local state, so the per-render allocation is wasted and any memoised consumer sees a fresh reference each render.",
|
|
24594
|
+
create: (context) => ({ VariableDeclarator(node) {
|
|
24595
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
24596
|
+
const initializer = node.init;
|
|
24597
|
+
if (!initializer) return;
|
|
24598
|
+
if (!isHoistableValueExpression(initializer)) return;
|
|
24599
|
+
const component = enclosingComponentOrHookScope(node, context.scopes.ownScopeFor);
|
|
24600
|
+
if (!component) return;
|
|
24601
|
+
if (hasComponentLocalReferences(initializer, component.bodyScope, context.scopes)) return;
|
|
24602
|
+
if (isBindingMutatedAfterInit(node, component.bodyScope, context.scopes)) return;
|
|
24603
|
+
const bindingName = node.id.name;
|
|
24604
|
+
context.report({
|
|
24605
|
+
node,
|
|
24606
|
+
message: `\`${bindingName}\` inside \`${component.displayName}\` doesn't depend on any local state. Move it to module scope so the allocation happens once and memoised consumers see a stable reference.`
|
|
24607
|
+
});
|
|
24608
|
+
} })
|
|
24609
|
+
});
|
|
24610
|
+
//#endregion
|
|
24611
|
+
//#region src/plugin/rules/performance/prefer-stable-empty-fallback.ts
|
|
24612
|
+
const isEmptyArrayLiteral$1 = (expression) => {
|
|
24613
|
+
const stripped = stripParenExpression(expression);
|
|
24614
|
+
return isNodeOfType(stripped, "ArrayExpression") && (stripped.elements ?? []).length === 0;
|
|
24615
|
+
};
|
|
24616
|
+
const isEmptyObjectLiteral = (expression) => {
|
|
24617
|
+
const stripped = stripParenExpression(expression);
|
|
24618
|
+
return isNodeOfType(stripped, "ObjectExpression") && (stripped.properties ?? []).length === 0;
|
|
24619
|
+
};
|
|
24620
|
+
const isStableNonEmptyExpression = (expression) => {
|
|
24621
|
+
const stripped = stripParenExpression(expression);
|
|
24622
|
+
if (isNodeOfType(stripped, "Identifier")) return true;
|
|
24623
|
+
if (isNodeOfType(stripped, "ThisExpression")) return true;
|
|
24624
|
+
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
24625
|
+
if (stripped.computed) return false;
|
|
24626
|
+
const object = stripped.object;
|
|
24627
|
+
if (!object) return false;
|
|
24628
|
+
return isStableNonEmptyExpression(object);
|
|
24629
|
+
}
|
|
24630
|
+
return false;
|
|
24631
|
+
};
|
|
24632
|
+
const matchEmptyFallbackInLogicalExpression = (expression) => {
|
|
24633
|
+
const stripped = stripParenExpression(expression);
|
|
24634
|
+
if (!isNodeOfType(stripped, "LogicalExpression")) return null;
|
|
24635
|
+
if (stripped.operator !== "||" && stripped.operator !== "??") return null;
|
|
24636
|
+
const left = stripped.left;
|
|
24637
|
+
const right = stripped.right;
|
|
24638
|
+
if (!left || !right) return null;
|
|
24639
|
+
if (isEmptyArrayLiteral$1(right) && isStableNonEmptyExpression(left)) return {
|
|
24640
|
+
emptyKind: "array",
|
|
24641
|
+
emptyNode: right,
|
|
24642
|
+
nonEmptyExpression: left
|
|
24643
|
+
};
|
|
24644
|
+
if (isEmptyObjectLiteral(right) && isStableNonEmptyExpression(left)) return {
|
|
24645
|
+
emptyKind: "object",
|
|
24646
|
+
emptyNode: right,
|
|
24647
|
+
nonEmptyExpression: left
|
|
24648
|
+
};
|
|
24649
|
+
return null;
|
|
24650
|
+
};
|
|
24651
|
+
const buildMessage$3 = (emptyKind) => {
|
|
24652
|
+
return `Fallback \`${emptyKind === "array" ? "[]" : "{}"}\` allocates a fresh ${emptyKind} on every render where the left-hand value is falsy — the memoised child sees a different reference and re-renders. Hoist a module-level constant (e.g. \`${emptyKind === "array" ? "const EMPTY_ITEMS: Item[] = []" : "const EMPTY_CONFIG: Config = {}"}\`) and use it as the fallback.`;
|
|
24653
|
+
};
|
|
24654
|
+
const preferStableEmptyFallback = defineRule({
|
|
24655
|
+
id: "prefer-stable-empty-fallback",
|
|
24656
|
+
tags: ["react-jsx-only", "test-noise"],
|
|
24657
|
+
severity: "warn",
|
|
24658
|
+
category: "Performance",
|
|
24659
|
+
disabledBy: ["react-compiler"],
|
|
24660
|
+
recommendation: "Hoist a module-level `const EMPTY = []` (or `{}`) and use that as the `||` / `??` fallback so the consumer sees a stable reference.",
|
|
24661
|
+
create: (context) => {
|
|
24662
|
+
let memoRegistry = null;
|
|
24663
|
+
return {
|
|
24664
|
+
Program(node) {
|
|
24665
|
+
memoRegistry = buildSameFileMemoRegistry(node);
|
|
24666
|
+
},
|
|
24667
|
+
JSXAttribute(node) {
|
|
24668
|
+
if (!isInsideFunctionScope(node)) return;
|
|
24669
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
24670
|
+
if (!node.value) return;
|
|
24671
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24672
|
+
const innerExpression = node.value.expression;
|
|
24673
|
+
if (!innerExpression) return;
|
|
24674
|
+
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
24675
|
+
const parentJsxOpening = node.parent;
|
|
24676
|
+
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
24677
|
+
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
24678
|
+
const fallback = matchEmptyFallbackInLogicalExpression(innerExpression);
|
|
24679
|
+
if (!fallback) return;
|
|
24680
|
+
context.report({
|
|
24681
|
+
node: fallback.emptyNode,
|
|
24682
|
+
message: buildMessage$3(fallback.emptyKind)
|
|
24683
|
+
});
|
|
24684
|
+
}
|
|
24685
|
+
};
|
|
24686
|
+
}
|
|
24687
|
+
});
|
|
24688
|
+
//#endregion
|
|
23215
24689
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
23216
24690
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
23217
24691
|
const preferTagOverRole = defineRule({
|
|
@@ -23717,106 +25191,6 @@ const queryStableQueryClient = defineRule({
|
|
|
23717
25191
|
}
|
|
23718
25192
|
});
|
|
23719
25193
|
//#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
25194
|
//#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
|
|
23821
25195
|
const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
23822
25196
|
["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
|
|
@@ -23862,91 +25236,216 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
23862
25236
|
} })
|
|
23863
25237
|
});
|
|
23864
25238
|
//#endregion
|
|
23865
|
-
//#region src/plugin/utils/has-binding-named.ts
|
|
23866
|
-
const hasBindingNamed = (root, bindingName) => {
|
|
23867
|
-
const collected = /* @__PURE__ */ new Set();
|
|
23868
|
-
const visit = (node) => {
|
|
23869
|
-
switch (node.type) {
|
|
23870
|
-
case "VariableDeclarator":
|
|
23871
|
-
if ("id" in node && node.id) collectPatternNames(node.id, collected);
|
|
23872
|
-
break;
|
|
23873
|
-
case "FunctionDeclaration":
|
|
23874
|
-
case "FunctionExpression":
|
|
23875
|
-
case "ClassDeclaration":
|
|
23876
|
-
case "ClassExpression":
|
|
23877
|
-
if ("id" in node && node.id && node.id.type === "Identifier") {
|
|
23878
|
-
const idNode = node.id;
|
|
23879
|
-
if (typeof idNode.name === "string") collected.add(idNode.name);
|
|
23880
|
-
}
|
|
23881
|
-
break;
|
|
23882
|
-
case "ArrowFunctionExpression": break;
|
|
23883
|
-
case "ImportDefaultSpecifier":
|
|
23884
|
-
case "ImportNamespaceSpecifier":
|
|
23885
|
-
case "ImportSpecifier":
|
|
23886
|
-
if ("local" in node && node.local && node.local.type === "Identifier") {
|
|
23887
|
-
const local = node.local;
|
|
23888
|
-
if (typeof local.name === "string") collected.add(local.name);
|
|
23889
|
-
}
|
|
23890
|
-
break;
|
|
23891
|
-
case "TSImportEqualsDeclaration":
|
|
23892
|
-
case "TSEnumDeclaration":
|
|
23893
|
-
case "TSTypeAliasDeclaration":
|
|
23894
|
-
case "TSInterfaceDeclaration":
|
|
23895
|
-
case "TSModuleDeclaration": {
|
|
23896
|
-
const idNode = node.id;
|
|
23897
|
-
if (idNode && idNode.type === "Identifier") {
|
|
23898
|
-
const idObject = idNode;
|
|
23899
|
-
if (typeof idObject.name === "string") collected.add(idObject.name);
|
|
23900
|
-
}
|
|
23901
|
-
break;
|
|
23902
|
-
}
|
|
23903
|
-
default: break;
|
|
23904
|
-
}
|
|
23905
|
-
if ("params" in node && Array.isArray(node.params)) for (const param of node.params) collectPatternNames(param, collected);
|
|
23906
|
-
if (collected.has(bindingName)) return;
|
|
23907
|
-
const nodeRecord = node;
|
|
23908
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
23909
|
-
if (key === "parent") continue;
|
|
23910
|
-
const child = nodeRecord[key];
|
|
23911
|
-
if (Array.isArray(child)) {
|
|
23912
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
23913
|
-
} else if (isAstNode(child)) visit(child);
|
|
23914
|
-
if (collected.has(bindingName)) return;
|
|
23915
|
-
}
|
|
23916
|
-
};
|
|
23917
|
-
visit(root);
|
|
23918
|
-
return collected.has(bindingName);
|
|
23919
|
-
};
|
|
23920
|
-
//#endregion
|
|
23921
25239
|
//#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
|
|
23922
|
-
const MESSAGE$
|
|
25240
|
+
const MESSAGE$6 = "`React` must be in scope when using JSX (the classic JSX transform expands `<a/>` to `React.createElement('a')`).";
|
|
23923
25241
|
const reactInJsxScope = defineRule({
|
|
23924
25242
|
id: "react-in-jsx-scope",
|
|
23925
25243
|
severity: "warn",
|
|
23926
25244
|
defaultEnabled: false,
|
|
23927
25245
|
recommendation: "If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import `React` at the top of the file.",
|
|
23928
|
-
create: (context) => {
|
|
23929
|
-
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
25246
|
+
create: (context) => ({
|
|
25247
|
+
JSXOpeningElement(node) {
|
|
25248
|
+
if (findVariableInitializer(node, "React")) return;
|
|
25249
|
+
context.report({
|
|
25250
|
+
node: node.name,
|
|
25251
|
+
message: MESSAGE$6
|
|
25252
|
+
});
|
|
25253
|
+
},
|
|
25254
|
+
JSXFragment(node) {
|
|
25255
|
+
if (findVariableInitializer(node, "React")) return;
|
|
25256
|
+
context.report({
|
|
25257
|
+
node: node.openingFragment,
|
|
25258
|
+
message: MESSAGE$6
|
|
25259
|
+
});
|
|
25260
|
+
}
|
|
25261
|
+
})
|
|
25262
|
+
});
|
|
25263
|
+
//#endregion
|
|
25264
|
+
//#region src/plugin/utils/collect-react-redux-selector-aliases.ts
|
|
25265
|
+
const REACT_REDUX_MODULE = "react-redux";
|
|
25266
|
+
const collectReactReduxSelectorAliases = (programRoot) => {
|
|
25267
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
25268
|
+
if (!isNodeOfType(programRoot, "Program")) return aliases;
|
|
25269
|
+
for (const topLevel of programRoot.body ?? []) {
|
|
25270
|
+
if (!isNodeOfType(topLevel, "ImportDeclaration")) continue;
|
|
25271
|
+
if (typeof topLevel.source?.value !== "string") continue;
|
|
25272
|
+
if (topLevel.source.value !== REACT_REDUX_MODULE) continue;
|
|
25273
|
+
for (const specifier of topLevel.specifiers ?? []) {
|
|
25274
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25275
|
+
const imported = specifier.imported;
|
|
25276
|
+
if ((imported && "name" in imported && typeof imported.name === "string" ? imported.name : imported && "value" in imported && typeof imported.value === "string" ? imported.value : null) !== "useSelector") continue;
|
|
25277
|
+
const local = specifier.local;
|
|
25278
|
+
if (isNodeOfType(local, "Identifier")) aliases.add(local.name);
|
|
25279
|
+
}
|
|
25280
|
+
}
|
|
25281
|
+
const collectDeclarations = (node) => {
|
|
25282
|
+
if (!isNodeOfType(node, "VariableDeclaration")) return;
|
|
25283
|
+
for (const declarator of node.declarations ?? []) {
|
|
25284
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
25285
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
25286
|
+
if (!declarator.init) continue;
|
|
25287
|
+
const initialiser = stripParenExpression(declarator.init);
|
|
25288
|
+
if (!isNodeOfType(initialiser, "Identifier")) continue;
|
|
25289
|
+
if (!aliases.has(initialiser.name)) continue;
|
|
25290
|
+
aliases.add(declarator.id.name);
|
|
25291
|
+
}
|
|
25292
|
+
};
|
|
25293
|
+
for (const topLevel of programRoot.body ?? []) if (isNodeOfType(topLevel, "VariableDeclaration")) collectDeclarations(topLevel);
|
|
25294
|
+
else if (isNodeOfType(topLevel, "ExportNamedDeclaration") && topLevel.declaration) collectDeclarations(topLevel.declaration);
|
|
25295
|
+
return aliases;
|
|
25296
|
+
};
|
|
25297
|
+
const isUseSelectorIdentifier = (calleeNode, aliases) => {
|
|
25298
|
+
if (!isNodeOfType(calleeNode, "Identifier")) return false;
|
|
25299
|
+
if (aliases.has(calleeNode.name)) return true;
|
|
25300
|
+
if (calleeNode.name !== "useSelector") return false;
|
|
25301
|
+
return isImportedFromModule(calleeNode, calleeNode.name, REACT_REDUX_MODULE);
|
|
25302
|
+
};
|
|
25303
|
+
//#endregion
|
|
25304
|
+
//#region src/plugin/rules/state-and-effects/utils/inline-use-selector-function.ts
|
|
25305
|
+
const inlineUseSelectorFunction = (callNode, aliases) => {
|
|
25306
|
+
if (!isUseSelectorIdentifier(callNode.callee, aliases)) return null;
|
|
25307
|
+
const args = callNode.arguments ?? [];
|
|
25308
|
+
if (args.length === 0 || args.length >= 2) return null;
|
|
25309
|
+
const selectorArgument = stripParenExpression(args[0]);
|
|
25310
|
+
if (isNodeOfType(selectorArgument, "ArrowFunctionExpression") || isNodeOfType(selectorArgument, "FunctionExpression")) return selectorArgument;
|
|
25311
|
+
return null;
|
|
25312
|
+
};
|
|
25313
|
+
//#endregion
|
|
25314
|
+
//#region src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts
|
|
25315
|
+
const ALLOCATING_ARRAY_METHODS = new Set([
|
|
25316
|
+
"filter",
|
|
25317
|
+
"map",
|
|
25318
|
+
"flatMap",
|
|
25319
|
+
"slice",
|
|
25320
|
+
"concat",
|
|
25321
|
+
"toSorted",
|
|
25322
|
+
"toReversed",
|
|
25323
|
+
"toSpliced",
|
|
25324
|
+
"with"
|
|
25325
|
+
]);
|
|
25326
|
+
const ALLOCATING_NAMESPACE_CALLS = new Map([["Object", new Set([
|
|
25327
|
+
"keys",
|
|
25328
|
+
"values",
|
|
25329
|
+
"entries",
|
|
25330
|
+
"fromEntries",
|
|
25331
|
+
"assign"
|
|
25332
|
+
])], ["Array", new Set(["from", "of"])]]);
|
|
25333
|
+
const MESSAGE_DERIVATION = (methodName) => `useSelector callback derives a new array via \`.${methodName}(...)\` on every store update — the default \`===\` equality check always fails on a fresh allocation, re-rendering the component on every dispatched action. Select the raw slice (\`useSelector(s => s.users)\`) and derive with \`useMemo\`, or hoist the derivation into a memoised \`createSelector\` from \`reselect\`.`;
|
|
25334
|
+
const MESSAGE_NAMESPACE = (namespace, methodName) => `useSelector callback returns a fresh collection from \`${namespace}.${methodName}(...)\` on every store update — the default \`===\` equality check always fails, re-rendering on every dispatched action. Select the raw slice and derive with \`useMemo\` or \`reselect\`.`;
|
|
25335
|
+
const getAllocatingCallSiteDescription = (expression) => {
|
|
25336
|
+
const stripped = stripParenExpression(expression);
|
|
25337
|
+
if (!isNodeOfType(stripped, "CallExpression")) return null;
|
|
25338
|
+
const callee = stripped.callee;
|
|
25339
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
25340
|
+
if (callee.computed) return null;
|
|
25341
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
25342
|
+
const methodName = callee.property.name;
|
|
25343
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
25344
|
+
const namespaceName = callee.object.name;
|
|
25345
|
+
if (ALLOCATING_NAMESPACE_CALLS.get(namespaceName)?.has(methodName)) return {
|
|
25346
|
+
kind: "namespace",
|
|
25347
|
+
namespace: namespaceName,
|
|
25348
|
+
method: methodName
|
|
23937
25349
|
};
|
|
25350
|
+
}
|
|
25351
|
+
if (ALLOCATING_ARRAY_METHODS.has(methodName)) return {
|
|
25352
|
+
kind: "method",
|
|
25353
|
+
method: methodName
|
|
25354
|
+
};
|
|
25355
|
+
return null;
|
|
25356
|
+
};
|
|
25357
|
+
const findReturnedAllocatingCall = (expression) => {
|
|
25358
|
+
const stripped = stripParenExpression(expression);
|
|
25359
|
+
const direct = getAllocatingCallSiteDescription(stripped);
|
|
25360
|
+
if (direct) return {
|
|
25361
|
+
...direct,
|
|
25362
|
+
node: stripped
|
|
25363
|
+
};
|
|
25364
|
+
if (isNodeOfType(stripped, "ConditionalExpression")) return findReturnedAllocatingCall(stripped.consequent) ?? findReturnedAllocatingCall(stripped.alternate);
|
|
25365
|
+
if (isNodeOfType(stripped, "LogicalExpression")) return findReturnedAllocatingCall(stripped.left) ?? findReturnedAllocatingCall(stripped.right);
|
|
25366
|
+
if (isNodeOfType(stripped, "SequenceExpression")) {
|
|
25367
|
+
const lastExpression = stripped.expressions[stripped.expressions.length - 1];
|
|
25368
|
+
return lastExpression ? findReturnedAllocatingCall(lastExpression) : null;
|
|
25369
|
+
}
|
|
25370
|
+
return null;
|
|
25371
|
+
};
|
|
25372
|
+
const reduxUseselectorInlineDerivation = defineRule({
|
|
25373
|
+
id: "redux-useselector-inline-derivation",
|
|
25374
|
+
severity: "warn",
|
|
25375
|
+
category: "Performance",
|
|
25376
|
+
disabledBy: ["react-compiler"],
|
|
25377
|
+
recommendation: "Select the raw slice and derive with `useMemo`, or use `createSelector` from `reselect`.",
|
|
25378
|
+
create: (context) => {
|
|
25379
|
+
let aliases = /* @__PURE__ */ new Set();
|
|
23938
25380
|
return {
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
25381
|
+
Program(node) {
|
|
25382
|
+
aliases = collectReactReduxSelectorAliases(node);
|
|
25383
|
+
},
|
|
25384
|
+
CallExpression(node) {
|
|
25385
|
+
const selectorArgument = inlineUseSelectorFunction(node, aliases);
|
|
25386
|
+
if (!selectorArgument) return;
|
|
25387
|
+
const body = selectorArgument.body;
|
|
25388
|
+
if (!body) return;
|
|
25389
|
+
const returnedExpressions = [];
|
|
25390
|
+
if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
|
|
25391
|
+
if (node !== body && isFunctionLike$2(node)) return false;
|
|
25392
|
+
if (isNodeOfType(node, "ReturnStatement")) {
|
|
25393
|
+
if (node.argument) returnedExpressions.push(node.argument);
|
|
25394
|
+
return false;
|
|
25395
|
+
}
|
|
23944
25396
|
});
|
|
25397
|
+
else returnedExpressions.push(body);
|
|
25398
|
+
for (const returnedExpression of returnedExpressions) {
|
|
25399
|
+
const allocatingCall = findReturnedAllocatingCall(returnedExpression);
|
|
25400
|
+
if (!allocatingCall) continue;
|
|
25401
|
+
const reportMessage = allocatingCall.kind === "method" ? MESSAGE_DERIVATION(allocatingCall.method) : MESSAGE_NAMESPACE(allocatingCall.namespace, allocatingCall.method);
|
|
25402
|
+
context.report({
|
|
25403
|
+
node: allocatingCall.node,
|
|
25404
|
+
message: reportMessage
|
|
25405
|
+
});
|
|
25406
|
+
return;
|
|
25407
|
+
}
|
|
25408
|
+
}
|
|
25409
|
+
};
|
|
25410
|
+
}
|
|
25411
|
+
});
|
|
25412
|
+
//#endregion
|
|
25413
|
+
//#region src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts
|
|
25414
|
+
const MESSAGE$5 = "useSelector returning a new object/array re-renders on every dispatched action — the default `===` equality check always fails on a fresh reference. Either return a primitive, split into multiple useSelector calls, or pass `shallowEqual` (or a custom equality fn) as the second argument.";
|
|
25415
|
+
const isConciseBodyReturningCollection = (functionNode) => {
|
|
25416
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
25417
|
+
const rawBody = functionNode.body;
|
|
25418
|
+
if (!rawBody) return false;
|
|
25419
|
+
if (!isNodeOfType(rawBody, "BlockStatement")) {
|
|
25420
|
+
const conciseExpression = stripParenExpression(rawBody);
|
|
25421
|
+
return isNodeOfType(conciseExpression, "ObjectExpression") || isNodeOfType(conciseExpression, "ArrayExpression");
|
|
25422
|
+
}
|
|
25423
|
+
const statements = rawBody.body ?? [];
|
|
25424
|
+
if (statements.length === 0) return false;
|
|
25425
|
+
const lastStatement = statements[statements.length - 1];
|
|
25426
|
+
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
25427
|
+
if (!lastStatement.argument) return false;
|
|
25428
|
+
const returnedExpression = stripParenExpression(lastStatement.argument);
|
|
25429
|
+
return isNodeOfType(returnedExpression, "ObjectExpression") || isNodeOfType(returnedExpression, "ArrayExpression");
|
|
25430
|
+
};
|
|
25431
|
+
const reduxUseselectorReturnsNewCollection = defineRule({
|
|
25432
|
+
id: "redux-useselector-returns-new-collection",
|
|
25433
|
+
severity: "warn",
|
|
25434
|
+
category: "Performance",
|
|
25435
|
+
disabledBy: ["react-compiler"],
|
|
25436
|
+
recommendation: "Return a primitive, split into multiple useSelector calls, or pass `shallowEqual` from `react-redux` as the second argument.",
|
|
25437
|
+
create: (context) => {
|
|
25438
|
+
let aliases = /* @__PURE__ */ new Set();
|
|
25439
|
+
return {
|
|
25440
|
+
Program(node) {
|
|
25441
|
+
aliases = collectReactReduxSelectorAliases(node);
|
|
23945
25442
|
},
|
|
23946
|
-
|
|
23947
|
-
|
|
25443
|
+
CallExpression(node) {
|
|
25444
|
+
const selectorArgument = inlineUseSelectorFunction(node, aliases);
|
|
25445
|
+
if (!selectorArgument) return;
|
|
25446
|
+
if (!isConciseBodyReturningCollection(selectorArgument)) return;
|
|
23948
25447
|
context.report({
|
|
23949
|
-
node
|
|
25448
|
+
node,
|
|
23950
25449
|
message: MESSAGE$5
|
|
23951
25450
|
});
|
|
23952
25451
|
}
|
|
@@ -24192,7 +25691,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
24192
25691
|
category: "Performance",
|
|
24193
25692
|
recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
24194
25693
|
create: (context) => {
|
|
24195
|
-
const filename = context.
|
|
25694
|
+
const filename = context.filename;
|
|
24196
25695
|
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
|
|
24197
25696
|
return { JSXAttribute(node) {
|
|
24198
25697
|
if (isAutoGenerated) return;
|
|
@@ -24224,7 +25723,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
24224
25723
|
let found = false;
|
|
24225
25724
|
walkAst(functionBody, (child) => {
|
|
24226
25725
|
if (found) return;
|
|
24227
|
-
if (child !== functionBody && isFunctionLike$
|
|
25726
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
24228
25727
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
24229
25728
|
});
|
|
24230
25729
|
return found;
|
|
@@ -24243,7 +25742,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
24243
25742
|
let found = false;
|
|
24244
25743
|
walkAst(componentBody, (child) => {
|
|
24245
25744
|
if (found) return;
|
|
24246
|
-
if (!isFunctionLike$
|
|
25745
|
+
if (!isFunctionLike$2(child)) return;
|
|
24247
25746
|
const functionBody = child.body;
|
|
24248
25747
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
24249
25748
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24697,6 +26196,33 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
24697
26196
|
} })
|
|
24698
26197
|
});
|
|
24699
26198
|
//#endregion
|
|
26199
|
+
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
26200
|
+
const rerenderLazyRefInit = defineRule({
|
|
26201
|
+
id: "rerender-lazy-ref-init",
|
|
26202
|
+
tags: ["test-noise"],
|
|
26203
|
+
severity: "warn",
|
|
26204
|
+
category: "Performance",
|
|
26205
|
+
recommendation: "Initialize lazily: `const ref = useRef<T | null>(null); if (ref.current === null) ref.current = expensiveCall();`",
|
|
26206
|
+
create: (context) => ({ CallExpression(node) {
|
|
26207
|
+
if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
|
|
26208
|
+
const initializer = node.arguments[0];
|
|
26209
|
+
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
26210
|
+
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
26211
|
+
if (!isPlainCall && !isNewCall) return;
|
|
26212
|
+
const callee = initializer.callee;
|
|
26213
|
+
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
26214
|
+
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
26215
|
+
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
26216
|
+
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
26217
|
+
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
26218
|
+
const lazyFix = isNewCall ? `ref.current = new ${calleeName}();` : `ref.current = ${calleeName}();`;
|
|
26219
|
+
context.report({
|
|
26220
|
+
node: initializer,
|
|
26221
|
+
message: `useRef(${callShape}) allocates a fresh value on every render — useRef has no lazy-init form, so the allocation is discarded after the first render. Use \`const ref = useRef(null); if (ref.current === null) ${lazyFix}\` or \`useMemo\` instead.`
|
|
26222
|
+
});
|
|
26223
|
+
} })
|
|
26224
|
+
});
|
|
26225
|
+
//#endregion
|
|
24700
26226
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-state-init.ts
|
|
24701
26227
|
const rerenderLazyStateInit = defineRule({
|
|
24702
26228
|
id: "rerender-lazy-state-init",
|
|
@@ -25524,7 +27050,8 @@ const classifyPackagePlatform = (filename) => {
|
|
|
25524
27050
|
//#endregion
|
|
25525
27051
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
25526
27052
|
const isExpoManagedFileActive = (context) => {
|
|
25527
|
-
const
|
|
27053
|
+
const rawFilename = context.filename;
|
|
27054
|
+
const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
|
|
25528
27055
|
if (filename) {
|
|
25529
27056
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
25530
27057
|
if (packagePlatform === "expo") return true;
|
|
@@ -30145,7 +31672,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
30145
31672
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
30146
31673
|
let current = node.parent;
|
|
30147
31674
|
while (current) {
|
|
30148
|
-
if (isFunctionLike$
|
|
31675
|
+
if (isFunctionLike$2(current)) {
|
|
30149
31676
|
const resolvedName = inferFunctionName(current);
|
|
30150
31677
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
30151
31678
|
}
|
|
@@ -30166,7 +31693,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
30166
31693
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
30167
31694
|
let current = node.parent;
|
|
30168
31695
|
while (current) {
|
|
30169
|
-
if (isFunctionLike$
|
|
31696
|
+
if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30170
31697
|
current = current.parent ?? null;
|
|
30171
31698
|
}
|
|
30172
31699
|
return false;
|
|
@@ -30500,7 +32027,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
30500
32027
|
let foundAuthCall = false;
|
|
30501
32028
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
30502
32029
|
if (foundAuthCall) return;
|
|
30503
|
-
if (isFunctionLike$
|
|
32030
|
+
if (isFunctionLike$2(child)) return false;
|
|
30504
32031
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30505
32032
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
30506
32033
|
});
|
|
@@ -30692,7 +32219,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
30692
32219
|
let isServerSideFile = false;
|
|
30693
32220
|
return {
|
|
30694
32221
|
Program(node) {
|
|
30695
|
-
const filename = normalizeFilename$1(context.
|
|
32222
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30696
32223
|
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
30697
32224
|
isServerSideFile = false;
|
|
30698
32225
|
return;
|
|
@@ -30805,7 +32332,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
30805
32332
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
|
|
30806
32333
|
},
|
|
30807
32334
|
ExportDefaultDeclaration(node) {
|
|
30808
|
-
const filename = normalizeFilename$1(context.
|
|
32335
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30809
32336
|
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
30810
32337
|
const declaration = node.declaration;
|
|
30811
32338
|
if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
|
|
@@ -31356,7 +32883,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31356
32883
|
};
|
|
31357
32884
|
return {
|
|
31358
32885
|
Program(node) {
|
|
31359
|
-
const filename = context.
|
|
32886
|
+
const filename = context.filename ?? "";
|
|
31360
32887
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31361
32888
|
const statements = node.body ?? [];
|
|
31362
32889
|
for (const statement of statements) collectImportBindings(statement);
|
|
@@ -31366,17 +32893,17 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31366
32893
|
}
|
|
31367
32894
|
},
|
|
31368
32895
|
ImportDeclaration(node) {
|
|
31369
|
-
const filename = context.
|
|
32896
|
+
const filename = context.filename ?? "";
|
|
31370
32897
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31371
32898
|
collectImportBindings(node);
|
|
31372
32899
|
},
|
|
31373
32900
|
VariableDeclarator(node) {
|
|
31374
|
-
const filename = context.
|
|
32901
|
+
const filename = context.filename ?? "";
|
|
31375
32902
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31376
32903
|
collectVariableAlias(node);
|
|
31377
32904
|
},
|
|
31378
32905
|
JSXOpeningElement(node) {
|
|
31379
|
-
const filename = normalizeFilename$1(context.
|
|
32906
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31380
32907
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31381
32908
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
31382
32909
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -31391,7 +32918,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31391
32918
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
31392
32919
|
},
|
|
31393
32920
|
"Program:exit"(programNode) {
|
|
31394
|
-
const filename = normalizeFilename$1(context.
|
|
32921
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31395
32922
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31396
32923
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
31397
32924
|
node: programNode,
|
|
@@ -31410,7 +32937,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
31410
32937
|
severity: "warn",
|
|
31411
32938
|
recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
31412
32939
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31413
|
-
const filename = normalizeFilename$1(context.
|
|
32940
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31414
32941
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31415
32942
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
31416
32943
|
const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
|
|
@@ -31484,7 +33011,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31484
33011
|
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
33012
|
return {
|
|
31486
33013
|
CallExpression(node) {
|
|
31487
|
-
const filename = normalizeFilename$1(context.
|
|
33014
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31488
33015
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31489
33016
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
31490
33017
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
@@ -31494,17 +33021,17 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31494
33021
|
});
|
|
31495
33022
|
},
|
|
31496
33023
|
"CallExpression:exit"(node) {
|
|
31497
|
-
const filename = normalizeFilename$1(context.
|
|
33024
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31498
33025
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31499
33026
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
31500
33027
|
},
|
|
31501
33028
|
JSXAttribute(node) {
|
|
31502
|
-
const filename = normalizeFilename$1(context.
|
|
33029
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31503
33030
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31504
33031
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
31505
33032
|
},
|
|
31506
33033
|
"JSXAttribute:exit"(node) {
|
|
31507
|
-
const filename = normalizeFilename$1(context.
|
|
33034
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31508
33035
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31509
33036
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
31510
33037
|
}
|
|
@@ -31585,7 +33112,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
31585
33112
|
severity: "warn",
|
|
31586
33113
|
recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
31587
33114
|
create: (context) => ({ CallExpression(node) {
|
|
31588
|
-
const filename = normalizeFilename$1(context.
|
|
33115
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31589
33116
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31590
33117
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
31591
33118
|
const callback = node.arguments?.[0];
|
|
@@ -33087,6 +34614,28 @@ const reactDoctorRules = [
|
|
|
33087
34614
|
category: "Architecture"
|
|
33088
34615
|
}
|
|
33089
34616
|
},
|
|
34617
|
+
{
|
|
34618
|
+
key: "react-doctor/no-create-context-in-render",
|
|
34619
|
+
id: "no-create-context-in-render",
|
|
34620
|
+
source: "react-doctor",
|
|
34621
|
+
originallyExternal: false,
|
|
34622
|
+
rule: {
|
|
34623
|
+
...noCreateContextInRender,
|
|
34624
|
+
framework: "global",
|
|
34625
|
+
category: "Correctness"
|
|
34626
|
+
}
|
|
34627
|
+
},
|
|
34628
|
+
{
|
|
34629
|
+
key: "react-doctor/no-create-store-in-render",
|
|
34630
|
+
id: "no-create-store-in-render",
|
|
34631
|
+
source: "react-doctor",
|
|
34632
|
+
originallyExternal: false,
|
|
34633
|
+
rule: {
|
|
34634
|
+
...noCreateStoreInRender,
|
|
34635
|
+
framework: "global",
|
|
34636
|
+
category: "Correctness"
|
|
34637
|
+
}
|
|
34638
|
+
},
|
|
33090
34639
|
{
|
|
33091
34640
|
key: "react-doctor/no-danger",
|
|
33092
34641
|
id: "no-danger",
|
|
@@ -33285,6 +34834,17 @@ const reactDoctorRules = [
|
|
|
33285
34834
|
category: "State & Effects"
|
|
33286
34835
|
}
|
|
33287
34836
|
},
|
|
34837
|
+
{
|
|
34838
|
+
key: "react-doctor/no-effect-with-fresh-deps",
|
|
34839
|
+
id: "no-effect-with-fresh-deps",
|
|
34840
|
+
source: "react-doctor",
|
|
34841
|
+
originallyExternal: false,
|
|
34842
|
+
rule: {
|
|
34843
|
+
...noEffectWithFreshDeps,
|
|
34844
|
+
framework: "global",
|
|
34845
|
+
category: "State & Effects"
|
|
34846
|
+
}
|
|
34847
|
+
},
|
|
33288
34848
|
{
|
|
33289
34849
|
key: "react-doctor/no-eval",
|
|
33290
34850
|
id: "no-eval",
|
|
@@ -33758,6 +35318,17 @@ const reactDoctorRules = [
|
|
|
33758
35318
|
category: "State & Effects"
|
|
33759
35319
|
}
|
|
33760
35320
|
},
|
|
35321
|
+
{
|
|
35322
|
+
key: "react-doctor/no-prop-types",
|
|
35323
|
+
id: "no-prop-types",
|
|
35324
|
+
source: "react-doctor",
|
|
35325
|
+
originallyExternal: false,
|
|
35326
|
+
rule: {
|
|
35327
|
+
...noPropTypes,
|
|
35328
|
+
framework: "global",
|
|
35329
|
+
category: "Architecture"
|
|
35330
|
+
}
|
|
35331
|
+
},
|
|
33761
35332
|
{
|
|
33762
35333
|
key: "react-doctor/no-pure-black-background",
|
|
33763
35334
|
id: "no-pure-black-background",
|
|
@@ -33769,6 +35340,17 @@ const reactDoctorRules = [
|
|
|
33769
35340
|
category: "Architecture"
|
|
33770
35341
|
}
|
|
33771
35342
|
},
|
|
35343
|
+
{
|
|
35344
|
+
key: "react-doctor/no-random-key",
|
|
35345
|
+
id: "no-random-key",
|
|
35346
|
+
source: "react-doctor",
|
|
35347
|
+
originallyExternal: false,
|
|
35348
|
+
rule: {
|
|
35349
|
+
...noRandomKey,
|
|
35350
|
+
framework: "global",
|
|
35351
|
+
category: "Correctness"
|
|
35352
|
+
}
|
|
35353
|
+
},
|
|
33772
35354
|
{
|
|
33773
35355
|
key: "react-doctor/no-react-children",
|
|
33774
35356
|
id: "no-react-children",
|
|
@@ -33890,6 +35472,17 @@ const reactDoctorRules = [
|
|
|
33890
35472
|
category: "Security"
|
|
33891
35473
|
}
|
|
33892
35474
|
},
|
|
35475
|
+
{
|
|
35476
|
+
key: "react-doctor/no-self-updating-effect",
|
|
35477
|
+
id: "no-self-updating-effect",
|
|
35478
|
+
source: "react-doctor",
|
|
35479
|
+
originallyExternal: false,
|
|
35480
|
+
rule: {
|
|
35481
|
+
...noSelfUpdatingEffect,
|
|
35482
|
+
framework: "global",
|
|
35483
|
+
category: "State & Effects"
|
|
35484
|
+
}
|
|
35485
|
+
},
|
|
33893
35486
|
{
|
|
33894
35487
|
key: "react-doctor/no-set-state",
|
|
33895
35488
|
id: "no-set-state",
|
|
@@ -34198,6 +35791,39 @@ const reactDoctorRules = [
|
|
|
34198
35791
|
category: "Accessibility"
|
|
34199
35792
|
}
|
|
34200
35793
|
},
|
|
35794
|
+
{
|
|
35795
|
+
key: "react-doctor/prefer-module-scope-pure-function",
|
|
35796
|
+
id: "prefer-module-scope-pure-function",
|
|
35797
|
+
source: "react-doctor",
|
|
35798
|
+
originallyExternal: false,
|
|
35799
|
+
rule: {
|
|
35800
|
+
...preferModuleScopePureFunction,
|
|
35801
|
+
framework: "global",
|
|
35802
|
+
category: "Architecture"
|
|
35803
|
+
}
|
|
35804
|
+
},
|
|
35805
|
+
{
|
|
35806
|
+
key: "react-doctor/prefer-module-scope-static-value",
|
|
35807
|
+
id: "prefer-module-scope-static-value",
|
|
35808
|
+
source: "react-doctor",
|
|
35809
|
+
originallyExternal: false,
|
|
35810
|
+
rule: {
|
|
35811
|
+
...preferModuleScopeStaticValue,
|
|
35812
|
+
framework: "global",
|
|
35813
|
+
category: "Architecture"
|
|
35814
|
+
}
|
|
35815
|
+
},
|
|
35816
|
+
{
|
|
35817
|
+
key: "react-doctor/prefer-stable-empty-fallback",
|
|
35818
|
+
id: "prefer-stable-empty-fallback",
|
|
35819
|
+
source: "react-doctor",
|
|
35820
|
+
originallyExternal: false,
|
|
35821
|
+
rule: {
|
|
35822
|
+
...preferStableEmptyFallback,
|
|
35823
|
+
framework: "global",
|
|
35824
|
+
category: "Performance"
|
|
35825
|
+
}
|
|
35826
|
+
},
|
|
34201
35827
|
{
|
|
34202
35828
|
key: "react-doctor/prefer-tag-over-role",
|
|
34203
35829
|
id: "prefer-tag-over-role",
|
|
@@ -34308,17 +35934,6 @@ const reactDoctorRules = [
|
|
|
34308
35934
|
category: "TanStack Query"
|
|
34309
35935
|
}
|
|
34310
35936
|
},
|
|
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
35937
|
{
|
|
34323
35938
|
key: "react-doctor/react-compiler-no-manual-memoization",
|
|
34324
35939
|
id: "react-compiler-no-manual-memoization",
|
|
@@ -34341,6 +35956,28 @@ const reactDoctorRules = [
|
|
|
34341
35956
|
category: "Correctness"
|
|
34342
35957
|
}
|
|
34343
35958
|
},
|
|
35959
|
+
{
|
|
35960
|
+
key: "react-doctor/redux-useselector-inline-derivation",
|
|
35961
|
+
id: "redux-useselector-inline-derivation",
|
|
35962
|
+
source: "react-doctor",
|
|
35963
|
+
originallyExternal: false,
|
|
35964
|
+
rule: {
|
|
35965
|
+
...reduxUseselectorInlineDerivation,
|
|
35966
|
+
framework: "global",
|
|
35967
|
+
category: "Performance"
|
|
35968
|
+
}
|
|
35969
|
+
},
|
|
35970
|
+
{
|
|
35971
|
+
key: "react-doctor/redux-useselector-returns-new-collection",
|
|
35972
|
+
id: "redux-useselector-returns-new-collection",
|
|
35973
|
+
source: "react-doctor",
|
|
35974
|
+
originallyExternal: false,
|
|
35975
|
+
rule: {
|
|
35976
|
+
...reduxUseselectorReturnsNewCollection,
|
|
35977
|
+
framework: "global",
|
|
35978
|
+
category: "Performance"
|
|
35979
|
+
}
|
|
35980
|
+
},
|
|
34344
35981
|
{
|
|
34345
35982
|
key: "react-doctor/rendering-animate-svg-wrapper",
|
|
34346
35983
|
id: "rendering-animate-svg-wrapper",
|
|
@@ -34484,6 +36121,17 @@ const reactDoctorRules = [
|
|
|
34484
36121
|
category: "Performance"
|
|
34485
36122
|
}
|
|
34486
36123
|
},
|
|
36124
|
+
{
|
|
36125
|
+
key: "react-doctor/rerender-lazy-ref-init",
|
|
36126
|
+
id: "rerender-lazy-ref-init",
|
|
36127
|
+
source: "react-doctor",
|
|
36128
|
+
originallyExternal: false,
|
|
36129
|
+
rule: {
|
|
36130
|
+
...rerenderLazyRefInit,
|
|
36131
|
+
framework: "global",
|
|
36132
|
+
category: "Performance"
|
|
36133
|
+
}
|
|
36134
|
+
},
|
|
34487
36135
|
{
|
|
34488
36136
|
key: "react-doctor/rerender-lazy-state-init",
|
|
34489
36137
|
id: "rerender-lazy-state-init",
|
|
@@ -35254,7 +36902,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
|
|
|
35254
36902
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
35255
36903
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
35256
36904
|
const isReactNativeFileActive = (context) => {
|
|
35257
|
-
const rawFilename = context.
|
|
36905
|
+
const rawFilename = context.filename;
|
|
35258
36906
|
if (!rawFilename) return true;
|
|
35259
36907
|
const filename = normalizeFilename$1(rawFilename);
|
|
35260
36908
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
@@ -35308,7 +36956,7 @@ const appendNode = (builder, block, node) => {
|
|
|
35308
36956
|
};
|
|
35309
36957
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
35310
36958
|
builder.nodeBlock.set(node, block);
|
|
35311
|
-
if (isFunctionLike$
|
|
36959
|
+
if (isFunctionLike$2(node)) return;
|
|
35312
36960
|
const record = node;
|
|
35313
36961
|
for (const key of Object.keys(record)) {
|
|
35314
36962
|
if (key === "parent") continue;
|
|
@@ -35646,7 +37294,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35646
37294
|
body: program.body
|
|
35647
37295
|
});
|
|
35648
37296
|
const visit = (node) => {
|
|
35649
|
-
if (isFunctionLike$
|
|
37297
|
+
if (isFunctionLike$2(node)) {
|
|
35650
37298
|
const body = node.body;
|
|
35651
37299
|
if (body) buildFor(node, body);
|
|
35652
37300
|
}
|
|
@@ -35663,7 +37311,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35663
37311
|
const enclosingFunction = (node) => {
|
|
35664
37312
|
let current = node;
|
|
35665
37313
|
while (current) {
|
|
35666
|
-
if (isFunctionLike$
|
|
37314
|
+
if (isFunctionLike$2(current)) return current;
|
|
35667
37315
|
if (isNodeOfType(current, "Program")) return current;
|
|
35668
37316
|
current = current.parent ?? null;
|
|
35669
37317
|
}
|
|
@@ -35742,7 +37390,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
35742
37390
|
};
|
|
35743
37391
|
const enrichedContext = {
|
|
35744
37392
|
report: baseContext.report,
|
|
35745
|
-
|
|
37393
|
+
get filename() {
|
|
37394
|
+
return baseContext.filename ?? baseContext.getFilename?.();
|
|
37395
|
+
},
|
|
35746
37396
|
settings: baseContext.settings,
|
|
35747
37397
|
get scopes() {
|
|
35748
37398
|
return getScopes();
|