oxlint-plugin-react-doctor 0.2.11-dev.f036b0f → 0.2.11-dev.f4035fc
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 +408 -0
- package/dist/index.js +2142 -391
- 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/",
|
|
@@ -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",
|
|
@@ -1026,7 +1034,7 @@ const altText = defineRule({
|
|
|
1026
1034
|
});
|
|
1027
1035
|
//#endregion
|
|
1028
1036
|
//#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
|
|
1029
|
-
const buildMessage$
|
|
1037
|
+
const buildMessage$30 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
|
|
1030
1038
|
const DEFAULT_AMBIGUOUS = [
|
|
1031
1039
|
"click here",
|
|
1032
1040
|
"here",
|
|
@@ -1083,14 +1091,14 @@ const anchorAmbiguousText = defineRule({
|
|
|
1083
1091
|
const normalized = normalizeText(accessibleText);
|
|
1084
1092
|
if (ambiguousSet.has(normalized)) context.report({
|
|
1085
1093
|
node: node.openingElement.name,
|
|
1086
|
-
message: buildMessage$
|
|
1094
|
+
message: buildMessage$30(normalized)
|
|
1087
1095
|
});
|
|
1088
1096
|
} };
|
|
1089
1097
|
}
|
|
1090
1098
|
});
|
|
1091
1099
|
//#endregion
|
|
1092
1100
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1093
|
-
const MESSAGE$
|
|
1101
|
+
const MESSAGE$49 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
|
|
1094
1102
|
const anchorHasContent = defineRule({
|
|
1095
1103
|
id: "anchor-has-content",
|
|
1096
1104
|
tags: ["react-jsx-only"],
|
|
@@ -1105,7 +1113,7 @@ const anchorHasContent = defineRule({
|
|
|
1105
1113
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1106
1114
|
context.report({
|
|
1107
1115
|
node: opening.name,
|
|
1108
|
-
message: MESSAGE$
|
|
1116
|
+
message: MESSAGE$49
|
|
1109
1117
|
});
|
|
1110
1118
|
} })
|
|
1111
1119
|
});
|
|
@@ -1498,7 +1506,7 @@ const parseJsxValue = (value) => {
|
|
|
1498
1506
|
};
|
|
1499
1507
|
//#endregion
|
|
1500
1508
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
1501
|
-
const MESSAGE$
|
|
1509
|
+
const MESSAGE$48 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
|
|
1502
1510
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
1503
1511
|
id: "aria-activedescendant-has-tabindex",
|
|
1504
1512
|
tags: ["react-jsx-only"],
|
|
@@ -1515,14 +1523,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
1515
1523
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
1516
1524
|
context.report({
|
|
1517
1525
|
node: node.name,
|
|
1518
|
-
message: MESSAGE$
|
|
1526
|
+
message: MESSAGE$48
|
|
1519
1527
|
});
|
|
1520
1528
|
return;
|
|
1521
1529
|
}
|
|
1522
1530
|
if (isInteractiveElement(tag, node)) return;
|
|
1523
1531
|
context.report({
|
|
1524
1532
|
node: node.name,
|
|
1525
|
-
message: MESSAGE$
|
|
1533
|
+
message: MESSAGE$48
|
|
1526
1534
|
});
|
|
1527
1535
|
} })
|
|
1528
1536
|
});
|
|
@@ -1662,7 +1670,7 @@ const ARIA_PROPERTIES = new Map([
|
|
|
1662
1670
|
const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
|
|
1663
1671
|
//#endregion
|
|
1664
1672
|
//#region src/plugin/rules/a11y/aria-props.ts
|
|
1665
|
-
const buildMessage$
|
|
1673
|
+
const buildMessage$29 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
|
|
1666
1674
|
const ariaProps = defineRule({
|
|
1667
1675
|
id: "aria-props",
|
|
1668
1676
|
tags: ["react-jsx-only"],
|
|
@@ -1675,7 +1683,7 @@ const ariaProps = defineRule({
|
|
|
1675
1683
|
if (!name || !name.startsWith("aria-")) return;
|
|
1676
1684
|
if (!isValidAriaProperty(name)) context.report({
|
|
1677
1685
|
node: node.name,
|
|
1678
|
-
message: buildMessage$
|
|
1686
|
+
message: buildMessage$29(name)
|
|
1679
1687
|
});
|
|
1680
1688
|
} })
|
|
1681
1689
|
});
|
|
@@ -1826,7 +1834,7 @@ const buildExpectedDescription = (propType) => {
|
|
|
1826
1834
|
case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
|
|
1827
1835
|
}
|
|
1828
1836
|
};
|
|
1829
|
-
const buildMessage$
|
|
1837
|
+
const buildMessage$28 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
|
|
1830
1838
|
const allowNoneValue = (propType) => {
|
|
1831
1839
|
switch (propType.kind) {
|
|
1832
1840
|
case "boolean":
|
|
@@ -1959,13 +1967,13 @@ const ariaProptypes = defineRule({
|
|
|
1959
1967
|
if (!node.value) {
|
|
1960
1968
|
if (!allowNoneValue(propType)) context.report({
|
|
1961
1969
|
node,
|
|
1962
|
-
message: buildMessage$
|
|
1970
|
+
message: buildMessage$28(propName, propType)
|
|
1963
1971
|
});
|
|
1964
1972
|
return;
|
|
1965
1973
|
}
|
|
1966
1974
|
if (!isValidValueForType(propType, node.value)) context.report({
|
|
1967
1975
|
node,
|
|
1968
|
-
message: buildMessage$
|
|
1976
|
+
message: buildMessage$28(propName, propType)
|
|
1969
1977
|
});
|
|
1970
1978
|
} })
|
|
1971
1979
|
});
|
|
@@ -2277,7 +2285,7 @@ const ariaRole = defineRule({
|
|
|
2277
2285
|
});
|
|
2278
2286
|
//#endregion
|
|
2279
2287
|
//#region src/plugin/rules/a11y/aria-unsupported-elements.ts
|
|
2280
|
-
const buildMessage$
|
|
2288
|
+
const buildMessage$27 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
|
|
2281
2289
|
const ariaUnsupportedElements = defineRule({
|
|
2282
2290
|
id: "aria-unsupported-elements",
|
|
2283
2291
|
tags: ["react-jsx-only"],
|
|
@@ -2294,7 +2302,7 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2294
2302
|
if (!attrName) continue;
|
|
2295
2303
|
if (attrName.startsWith("aria-") || attrName === "role") context.report({
|
|
2296
2304
|
node: attribute,
|
|
2297
|
-
message: buildMessage$
|
|
2305
|
+
message: buildMessage$27(tag, attrName)
|
|
2298
2306
|
});
|
|
2299
2307
|
}
|
|
2300
2308
|
} })
|
|
@@ -2327,7 +2335,7 @@ const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
|
|
|
2327
2335
|
"BigInt",
|
|
2328
2336
|
"Reflect"
|
|
2329
2337
|
]);
|
|
2330
|
-
const MUTATING_ARRAY_METHODS
|
|
2338
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
2331
2339
|
"push",
|
|
2332
2340
|
"pop",
|
|
2333
2341
|
"shift",
|
|
@@ -2338,6 +2346,12 @@ const MUTATING_ARRAY_METHODS$1 = new Set([
|
|
|
2338
2346
|
"fill",
|
|
2339
2347
|
"copyWithin"
|
|
2340
2348
|
]);
|
|
2349
|
+
const MUTATING_COLLECTION_METHODS = new Set([
|
|
2350
|
+
"add",
|
|
2351
|
+
"clear",
|
|
2352
|
+
"delete",
|
|
2353
|
+
"set"
|
|
2354
|
+
]);
|
|
2341
2355
|
const CHAINABLE_ITERATION_METHODS = new Set([
|
|
2342
2356
|
"map",
|
|
2343
2357
|
"filter",
|
|
@@ -2549,7 +2563,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2549
2563
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2550
2564
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2551
2565
|
*/
|
|
2552
|
-
const isFunctionLike$
|
|
2566
|
+
const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2553
2567
|
//#endregion
|
|
2554
2568
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2555
2569
|
/**
|
|
@@ -2572,7 +2586,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2572
2586
|
let firstAwait = null;
|
|
2573
2587
|
walkAst(block, (child) => {
|
|
2574
2588
|
if (firstAwait) return false;
|
|
2575
|
-
if (child !== block && isFunctionLike$
|
|
2589
|
+
if (child !== block && isFunctionLike$2(child)) return false;
|
|
2576
2590
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2577
2591
|
});
|
|
2578
2592
|
return firstAwait;
|
|
@@ -3027,13 +3041,13 @@ const asyncDeferAwait = defineRule({
|
|
|
3027
3041
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
3028
3042
|
if (!functionBody) return;
|
|
3029
3043
|
walkAst(functionBody, (descendant) => {
|
|
3030
|
-
if (isFunctionLike$
|
|
3044
|
+
if (isFunctionLike$2(descendant)) return false;
|
|
3031
3045
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
3032
3046
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
3033
3047
|
});
|
|
3034
3048
|
};
|
|
3035
3049
|
const enterFunction = (node) => {
|
|
3036
|
-
if (!isFunctionLike$
|
|
3050
|
+
if (!isFunctionLike$2(node)) return;
|
|
3037
3051
|
if (!node.async) return;
|
|
3038
3052
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
3039
3053
|
inspectAllStatementBlocks(node.body);
|
|
@@ -3164,7 +3178,7 @@ const asyncParallel = defineRule({
|
|
|
3164
3178
|
});
|
|
3165
3179
|
//#endregion
|
|
3166
3180
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
3167
|
-
const buildMessage$
|
|
3181
|
+
const buildMessage$26 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
|
|
3168
3182
|
const AUTOFILL_TOKENS = new Set([
|
|
3169
3183
|
"off",
|
|
3170
3184
|
"on",
|
|
@@ -3252,7 +3266,7 @@ const autocompleteValid = defineRule({
|
|
|
3252
3266
|
if (!AUTOFILL_TOKENS.has(token)) {
|
|
3253
3267
|
context.report({
|
|
3254
3268
|
node: attribute,
|
|
3255
|
-
message: buildMessage$
|
|
3269
|
+
message: buildMessage$26(value)
|
|
3256
3270
|
});
|
|
3257
3271
|
return;
|
|
3258
3272
|
}
|
|
@@ -3527,7 +3541,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
3527
3541
|
//#endregion
|
|
3528
3542
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
3529
3543
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3530
|
-
const MESSAGE$
|
|
3544
|
+
const MESSAGE$47 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
|
|
3531
3545
|
const KEY_HANDLERS = [
|
|
3532
3546
|
"onKeyUp",
|
|
3533
3547
|
"onKeyDown",
|
|
@@ -3558,7 +3572,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3558
3572
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
3559
3573
|
context.report({
|
|
3560
3574
|
node: node.name,
|
|
3561
|
-
message: MESSAGE$
|
|
3575
|
+
message: MESSAGE$47
|
|
3562
3576
|
});
|
|
3563
3577
|
} };
|
|
3564
3578
|
}
|
|
@@ -3669,7 +3683,7 @@ const stripParenExpression = (node) => {
|
|
|
3669
3683
|
};
|
|
3670
3684
|
//#endregion
|
|
3671
3685
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3672
|
-
const MESSAGE$
|
|
3686
|
+
const MESSAGE$46 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3673
3687
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
3674
3688
|
const DEFAULT_LABELLING_PROPS = [
|
|
3675
3689
|
"alt",
|
|
@@ -3829,7 +3843,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3829
3843
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
3830
3844
|
context.report({
|
|
3831
3845
|
node: opening,
|
|
3832
|
-
message: MESSAGE$
|
|
3846
|
+
message: MESSAGE$46
|
|
3833
3847
|
});
|
|
3834
3848
|
} };
|
|
3835
3849
|
}
|
|
@@ -4153,14 +4167,21 @@ const isEs6Component = (node) => {
|
|
|
4153
4167
|
};
|
|
4154
4168
|
//#endregion
|
|
4155
4169
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
4156
|
-
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
|
+
];
|
|
4157
4176
|
const resolveSettings$45 = (settings) => {
|
|
4158
4177
|
const reactDoctor = settings?.["react-doctor"];
|
|
4159
4178
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
4179
|
+
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
4160
4180
|
return {
|
|
4161
4181
|
ignoreTranspilerName: ruleSettings.ignoreTranspilerName ?? false,
|
|
4162
4182
|
checkContextObjects: ruleSettings.checkContextObjects ?? false,
|
|
4163
|
-
reactVersion: ruleSettings.reactVersion ?? ""
|
|
4183
|
+
reactVersion: ruleSettings.reactVersion ?? "",
|
|
4184
|
+
additionalHoCs
|
|
4164
4185
|
};
|
|
4165
4186
|
};
|
|
4166
4187
|
const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
@@ -4233,29 +4254,28 @@ const isCreateContextCall = (node) => {
|
|
|
4233
4254
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
4234
4255
|
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
|
|
4235
4256
|
};
|
|
4236
|
-
const isObserverCall = (node) => {
|
|
4237
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
4238
|
-
const callee = node.callee;
|
|
4239
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name === "observer";
|
|
4240
|
-
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "observer";
|
|
4241
|
-
};
|
|
4242
|
-
const getCallName = (node) => {
|
|
4243
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
4244
|
-
const callee = node.callee;
|
|
4245
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
4246
|
-
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee);
|
|
4247
|
-
return null;
|
|
4248
|
-
};
|
|
4249
4257
|
const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
|
|
4250
4258
|
const firstCallArgument = (node) => {
|
|
4251
4259
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
4252
4260
|
const first = node.arguments[0];
|
|
4253
4261
|
return first ? first : null;
|
|
4254
4262
|
};
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
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;
|
|
4258
4277
|
};
|
|
4278
|
+
const isDisplayNameHoC = (node, additionalHoCs) => resolveHoCCalleeName(node, additionalHoCs) !== null;
|
|
4259
4279
|
const supportsComposedForwardRefDisplayName = (version) => {
|
|
4260
4280
|
if (!version) return false;
|
|
4261
4281
|
if (isReactVersionAtLeast$1(version, 15, 7)) return true;
|
|
@@ -4263,17 +4283,17 @@ const supportsComposedForwardRefDisplayName = (version) => {
|
|
|
4263
4283
|
return Boolean(match && Number(match[1]) >= 11);
|
|
4264
4284
|
};
|
|
4265
4285
|
const shouldReportHoCDisplayName = (node, settings) => {
|
|
4266
|
-
if (!isDisplayNameHoC(node)) return false;
|
|
4286
|
+
if (!isDisplayNameHoC(node, settings.additionalHoCs)) return false;
|
|
4267
4287
|
if (!containsJsx$1(node)) return false;
|
|
4268
4288
|
const assignedName = getAssignedName(node);
|
|
4269
4289
|
const programRoot = findProgramRoot(node);
|
|
4270
4290
|
if (assignedName && programRoot && hasDisplayNameAssignment(assignedName, programRoot)) return false;
|
|
4271
|
-
const callName =
|
|
4291
|
+
const callName = resolveHoCCalleeName(node, settings.additionalHoCs);
|
|
4272
4292
|
const firstArgument = firstCallArgument(node);
|
|
4273
4293
|
if (!firstArgument) return false;
|
|
4274
|
-
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;
|
|
4275
4295
|
if (callName === "memo" && isNodeOfType(firstArgument, "CallExpression")) {
|
|
4276
|
-
if (
|
|
4296
|
+
if (resolveHoCCalleeName(firstArgument, settings.additionalHoCs) !== "forwardRef") return false;
|
|
4277
4297
|
return !supportsComposedForwardRefDisplayName(settings.reactVersion);
|
|
4278
4298
|
}
|
|
4279
4299
|
if (isNamedFunctionLike(firstArgument)) return false;
|
|
@@ -4349,7 +4369,7 @@ const displayName = defineRule({
|
|
|
4349
4369
|
const reportAt = (node) => {
|
|
4350
4370
|
context.report({
|
|
4351
4371
|
node,
|
|
4352
|
-
message: MESSAGE$
|
|
4372
|
+
message: MESSAGE$45
|
|
4353
4373
|
});
|
|
4354
4374
|
};
|
|
4355
4375
|
return {
|
|
@@ -4427,10 +4447,6 @@ const displayName = defineRule({
|
|
|
4427
4447
|
reportAt(node);
|
|
4428
4448
|
return;
|
|
4429
4449
|
}
|
|
4430
|
-
if (isObserverCall(node) && containsJsx$1(node)) {
|
|
4431
|
-
reportAt(node);
|
|
4432
|
-
return;
|
|
4433
|
-
}
|
|
4434
4450
|
if (shouldReportHoCDisplayName(node, settings)) {
|
|
4435
4451
|
reportAt(node);
|
|
4436
4452
|
return;
|
|
@@ -4472,7 +4488,7 @@ const displayName = defineRule({
|
|
|
4472
4488
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4473
4489
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4474
4490
|
if (!node || typeof node !== "object") return;
|
|
4475
|
-
if (isFunctionLike$
|
|
4491
|
+
if (isFunctionLike$2(node)) return;
|
|
4476
4492
|
visitor(node);
|
|
4477
4493
|
const nodeRecord = node;
|
|
4478
4494
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4560,7 +4576,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4560
4576
|
let didFindRelease = false;
|
|
4561
4577
|
walkAst(node, (child) => {
|
|
4562
4578
|
if (didFindRelease) return false;
|
|
4563
|
-
if (child !== node && isFunctionLike$
|
|
4579
|
+
if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4564
4580
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4565
4581
|
didFindRelease = true;
|
|
4566
4582
|
return false;
|
|
@@ -4569,7 +4585,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4569
4585
|
return didFindRelease;
|
|
4570
4586
|
};
|
|
4571
4587
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4572
|
-
if (!isFunctionLike$
|
|
4588
|
+
if (!isFunctionLike$2(node)) return false;
|
|
4573
4589
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4574
4590
|
};
|
|
4575
4591
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
@@ -4811,7 +4827,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4811
4827
|
};
|
|
4812
4828
|
const isFunctionBodyBlock = (block) => {
|
|
4813
4829
|
if (!block.parent) return false;
|
|
4814
|
-
return isFunctionLike$
|
|
4830
|
+
return isFunctionLike$2(block.parent);
|
|
4815
4831
|
};
|
|
4816
4832
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4817
4833
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -4966,7 +4982,7 @@ const walkParameterReferences = (pattern, state) => {
|
|
|
4966
4982
|
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
4967
4983
|
};
|
|
4968
4984
|
const walk = (node, state) => {
|
|
4969
|
-
if (isFunctionLike$
|
|
4985
|
+
if (isFunctionLike$2(node)) {
|
|
4970
4986
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
4971
4987
|
setNodeScope(node, state);
|
|
4972
4988
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -5212,7 +5228,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5212
5228
|
const out = [];
|
|
5213
5229
|
const seen = /* @__PURE__ */ new Set();
|
|
5214
5230
|
const visit = (node) => {
|
|
5215
|
-
if (node !== functionNode && isFunctionLike$
|
|
5231
|
+
if (node !== functionNode && isFunctionLike$2(node)) {
|
|
5216
5232
|
const innerCaptures = closureCaptures(node, scopes);
|
|
5217
5233
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
5218
5234
|
if (!seen.has(reference.id)) {
|
|
@@ -5284,7 +5300,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
5284
5300
|
"ParenthesizedExpression",
|
|
5285
5301
|
"ChainExpression"
|
|
5286
5302
|
]);
|
|
5287
|
-
const unwrapExpression = (node) => {
|
|
5303
|
+
const unwrapExpression$1 = (node) => {
|
|
5288
5304
|
let current = node;
|
|
5289
5305
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
5290
5306
|
const inner = current.expression;
|
|
@@ -5391,7 +5407,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
5391
5407
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
5392
5408
|
const initializerRaw = declarator.init;
|
|
5393
5409
|
if (!initializerRaw) return false;
|
|
5394
|
-
const initializer = unwrapExpression(initializerRaw);
|
|
5410
|
+
const initializer = unwrapExpression$1(initializerRaw);
|
|
5395
5411
|
if (symbol.kind === "const") {
|
|
5396
5412
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
5397
5413
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -5411,13 +5427,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
5411
5427
|
return false;
|
|
5412
5428
|
};
|
|
5413
5429
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
5414
|
-
const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
|
|
5430
|
+
const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
|
|
5415
5431
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
5416
5432
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
5417
5433
|
};
|
|
5418
5434
|
const getFunctionValueNode = (symbol) => {
|
|
5419
5435
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
5420
|
-
const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
|
|
5436
|
+
const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
|
|
5421
5437
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
5422
5438
|
return null;
|
|
5423
5439
|
};
|
|
@@ -5552,23 +5568,23 @@ const computeDepKey = (reference) => {
|
|
|
5552
5568
|
return fullName;
|
|
5553
5569
|
};
|
|
5554
5570
|
const computeDeclaredDepKey = (entry) => {
|
|
5555
|
-
const stripped = unwrapExpression(entry);
|
|
5571
|
+
const stripped = unwrapExpression$1(entry);
|
|
5556
5572
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
5557
5573
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
5558
5574
|
return null;
|
|
5559
5575
|
};
|
|
5560
5576
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
5561
5577
|
if (!depsArgument) return false;
|
|
5562
|
-
const strippedDepsArgument = unwrapExpression(depsArgument);
|
|
5578
|
+
const strippedDepsArgument = unwrapExpression$1(depsArgument);
|
|
5563
5579
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
5564
5580
|
return strippedDepsArgument.elements.some((element) => {
|
|
5565
5581
|
if (!element) return false;
|
|
5566
|
-
const strippedElement = unwrapExpression(element);
|
|
5582
|
+
const strippedElement = unwrapExpression$1(element);
|
|
5567
5583
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
5568
5584
|
});
|
|
5569
5585
|
};
|
|
5570
5586
|
const stringifyMemberChain = (node) => {
|
|
5571
|
-
const stripped = unwrapExpression(node);
|
|
5587
|
+
const stripped = unwrapExpression$1(node);
|
|
5572
5588
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
5573
5589
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
5574
5590
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -5610,13 +5626,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
5610
5626
|
return false;
|
|
5611
5627
|
};
|
|
5612
5628
|
const getMemberRootIdentifier = (node) => {
|
|
5613
|
-
const stripped = unwrapExpression(node);
|
|
5629
|
+
const stripped = unwrapExpression$1(node);
|
|
5614
5630
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
5615
5631
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
5616
5632
|
return null;
|
|
5617
5633
|
};
|
|
5618
5634
|
const hasComputedMemberExpression = (node) => {
|
|
5619
|
-
const stripped = unwrapExpression(node);
|
|
5635
|
+
const stripped = unwrapExpression$1(node);
|
|
5620
5636
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
5621
5637
|
if (stripped.computed) return true;
|
|
5622
5638
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -5637,7 +5653,7 @@ const isRegExpLiteral = (node) => {
|
|
|
5637
5653
|
};
|
|
5638
5654
|
const isUnstableInitializer = (node) => {
|
|
5639
5655
|
if (!node) return false;
|
|
5640
|
-
const stripped = unwrapExpression(node);
|
|
5656
|
+
const stripped = unwrapExpression$1(node);
|
|
5641
5657
|
if (isRegExpLiteral(stripped)) return true;
|
|
5642
5658
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
5643
5659
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -5793,7 +5809,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
5793
5809
|
const visit = (current) => {
|
|
5794
5810
|
if (didFindMemberCall) return;
|
|
5795
5811
|
if (isNodeOfType(current, "CallExpression")) {
|
|
5796
|
-
if (getMemberRootIdentifier(unwrapExpression(current.callee))?.name === rootName) {
|
|
5812
|
+
if (getMemberRootIdentifier(unwrapExpression$1(current.callee))?.name === rootName) {
|
|
5797
5813
|
didFindMemberCall = true;
|
|
5798
5814
|
return;
|
|
5799
5815
|
}
|
|
@@ -5855,7 +5871,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5855
5871
|
return;
|
|
5856
5872
|
}
|
|
5857
5873
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
5858
|
-
const callbackExpression = unwrapExpression(callbackArgument);
|
|
5874
|
+
const callbackExpression = unwrapExpression$1(callbackArgument);
|
|
5859
5875
|
let callbackToAnalyze = null;
|
|
5860
5876
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
5861
5877
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -5863,7 +5879,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5863
5879
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
5864
5880
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
5865
5881
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
5866
|
-
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);
|
|
5867
5883
|
else if (depsArgumentRaw) {
|
|
5868
5884
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
5869
5885
|
context.report({
|
|
@@ -5920,7 +5936,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5920
5936
|
});
|
|
5921
5937
|
return;
|
|
5922
5938
|
}
|
|
5923
|
-
const depsArgument = unwrapExpression(depsArgumentRaw);
|
|
5939
|
+
const depsArgument = unwrapExpression$1(depsArgumentRaw);
|
|
5924
5940
|
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
5925
5941
|
if (isAutoDependenciesHook(hookName)) return;
|
|
5926
5942
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
@@ -5961,11 +5977,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5961
5977
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
5962
5978
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
5963
5979
|
if (!element) return false;
|
|
5964
|
-
return isLiteralOrEmptyTemplate(unwrapExpression(element));
|
|
5980
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$1(element));
|
|
5965
5981
|
});
|
|
5966
5982
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
5967
5983
|
if (!element) return false;
|
|
5968
|
-
return isNonStringLiteral(unwrapExpression(element));
|
|
5984
|
+
return isNonStringLiteral(unwrapExpression$1(element));
|
|
5969
5985
|
});
|
|
5970
5986
|
if (hasNonStringLiteralDep) context.report({
|
|
5971
5987
|
node: depsArgument,
|
|
@@ -5986,7 +6002,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
5986
6002
|
});
|
|
5987
6003
|
continue;
|
|
5988
6004
|
}
|
|
5989
|
-
const stripped = unwrapExpression(elementNode);
|
|
6005
|
+
const stripped = unwrapExpression$1(elementNode);
|
|
5990
6006
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
5991
6007
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
5992
6008
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -6206,7 +6222,7 @@ const flattenJsxName$1 = (name) => {
|
|
|
6206
6222
|
return "";
|
|
6207
6223
|
};
|
|
6208
6224
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
6209
|
-
const buildMessage$
|
|
6225
|
+
const buildMessage$25 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
|
|
6210
6226
|
const forbidComponentProps = defineRule({
|
|
6211
6227
|
id: "forbid-component-props",
|
|
6212
6228
|
severity: "warn",
|
|
@@ -6232,7 +6248,7 @@ const forbidComponentProps = defineRule({
|
|
|
6232
6248
|
if (!isForbiddenForTag(entry, tag)) continue;
|
|
6233
6249
|
context.report({
|
|
6234
6250
|
node: attribute,
|
|
6235
|
-
message: buildMessage$
|
|
6251
|
+
message: buildMessage$25(propName, entry.message)
|
|
6236
6252
|
});
|
|
6237
6253
|
break;
|
|
6238
6254
|
}
|
|
@@ -6242,7 +6258,7 @@ const forbidComponentProps = defineRule({
|
|
|
6242
6258
|
});
|
|
6243
6259
|
//#endregion
|
|
6244
6260
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6245
|
-
const buildMessage$
|
|
6261
|
+
const buildMessage$24 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
|
|
6246
6262
|
const resolveSettings$43 = (settings) => {
|
|
6247
6263
|
const reactDoctor = settings?.["react-doctor"];
|
|
6248
6264
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
@@ -6280,7 +6296,7 @@ const forbidDomProps = defineRule({
|
|
|
6280
6296
|
if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
|
|
6281
6297
|
context.report({
|
|
6282
6298
|
node: attribute.name,
|
|
6283
|
-
message: buildMessage$
|
|
6299
|
+
message: buildMessage$24(propName, descriptor.message)
|
|
6284
6300
|
});
|
|
6285
6301
|
}
|
|
6286
6302
|
} };
|
|
@@ -6350,7 +6366,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6350
6366
|
};
|
|
6351
6367
|
//#endregion
|
|
6352
6368
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6353
|
-
const buildMessage$
|
|
6369
|
+
const buildMessage$23 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
|
|
6354
6370
|
const resolveSettings$42 = (settings) => {
|
|
6355
6371
|
const reactDoctor = settings?.["react-doctor"];
|
|
6356
6372
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
@@ -6374,7 +6390,7 @@ const forbidElements = defineRule({
|
|
|
6374
6390
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6375
6391
|
context.report({
|
|
6376
6392
|
node: node.name,
|
|
6377
|
-
message: buildMessage$
|
|
6393
|
+
message: buildMessage$23(fullName, forbidMap.get(fullName))
|
|
6378
6394
|
});
|
|
6379
6395
|
},
|
|
6380
6396
|
CallExpression(node) {
|
|
@@ -6394,7 +6410,7 @@ const forbidElements = defineRule({
|
|
|
6394
6410
|
if (!elementName || !forbidMap.has(elementName)) return;
|
|
6395
6411
|
context.report({
|
|
6396
6412
|
node: firstArgument,
|
|
6397
|
-
message: buildMessage$
|
|
6413
|
+
message: buildMessage$23(elementName, forbidMap.get(elementName))
|
|
6398
6414
|
});
|
|
6399
6415
|
}
|
|
6400
6416
|
};
|
|
@@ -6402,7 +6418,7 @@ const forbidElements = defineRule({
|
|
|
6402
6418
|
});
|
|
6403
6419
|
//#endregion
|
|
6404
6420
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
6405
|
-
const MESSAGE$
|
|
6421
|
+
const MESSAGE$44 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
|
|
6406
6422
|
const forwardRefUsesRef = defineRule({
|
|
6407
6423
|
id: "forward-ref-uses-ref",
|
|
6408
6424
|
severity: "warn",
|
|
@@ -6421,13 +6437,13 @@ const forwardRefUsesRef = defineRule({
|
|
|
6421
6437
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
6422
6438
|
context.report({
|
|
6423
6439
|
node: inner,
|
|
6424
|
-
message: MESSAGE$
|
|
6440
|
+
message: MESSAGE$44
|
|
6425
6441
|
});
|
|
6426
6442
|
} })
|
|
6427
6443
|
});
|
|
6428
6444
|
//#endregion
|
|
6429
6445
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
6430
|
-
const MESSAGE$
|
|
6446
|
+
const MESSAGE$43 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
|
|
6431
6447
|
const DEFAULT_HEADING_TAGS = [
|
|
6432
6448
|
"h1",
|
|
6433
6449
|
"h2",
|
|
@@ -6459,7 +6475,7 @@ const headingHasContent = defineRule({
|
|
|
6459
6475
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6460
6476
|
context.report({
|
|
6461
6477
|
node,
|
|
6462
|
-
message: MESSAGE$
|
|
6478
|
+
message: MESSAGE$43
|
|
6463
6479
|
});
|
|
6464
6480
|
} };
|
|
6465
6481
|
}
|
|
@@ -6595,7 +6611,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
6595
6611
|
});
|
|
6596
6612
|
//#endregion
|
|
6597
6613
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6598
|
-
const MESSAGE$
|
|
6614
|
+
const MESSAGE$42 = "`<html>` element must have a non-empty `lang` attribute.";
|
|
6599
6615
|
const resolveSettings$39 = (settings) => {
|
|
6600
6616
|
const reactDoctor = settings?.["react-doctor"];
|
|
6601
6617
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -6642,7 +6658,7 @@ const htmlHasLang = defineRule({
|
|
|
6642
6658
|
if (!lang) {
|
|
6643
6659
|
context.report({
|
|
6644
6660
|
node: node.name,
|
|
6645
|
-
message: MESSAGE$
|
|
6661
|
+
message: MESSAGE$42
|
|
6646
6662
|
});
|
|
6647
6663
|
return;
|
|
6648
6664
|
}
|
|
@@ -6650,13 +6666,13 @@ const htmlHasLang = defineRule({
|
|
|
6650
6666
|
if (verdict === "missing" || verdict === "empty") {
|
|
6651
6667
|
context.report({
|
|
6652
6668
|
node: lang,
|
|
6653
|
-
message: MESSAGE$
|
|
6669
|
+
message: MESSAGE$42
|
|
6654
6670
|
});
|
|
6655
6671
|
return;
|
|
6656
6672
|
}
|
|
6657
6673
|
if (hasSpread && !lang) context.report({
|
|
6658
6674
|
node: node.name,
|
|
6659
|
-
message: MESSAGE$
|
|
6675
|
+
message: MESSAGE$42
|
|
6660
6676
|
});
|
|
6661
6677
|
} };
|
|
6662
6678
|
}
|
|
@@ -6696,7 +6712,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
|
|
|
6696
6712
|
"table",
|
|
6697
6713
|
"ul"
|
|
6698
6714
|
]);
|
|
6699
|
-
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.`;
|
|
6700
6716
|
const isParagraphElement = (candidate) => {
|
|
6701
6717
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6702
6718
|
const opening = candidate.openingElement;
|
|
@@ -6724,7 +6740,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
6724
6740
|
if (!findEnclosingParagraph(node)) return;
|
|
6725
6741
|
context.report({
|
|
6726
6742
|
node: node.name,
|
|
6727
|
-
message: buildMessage$
|
|
6743
|
+
message: buildMessage$22(childTagName)
|
|
6728
6744
|
});
|
|
6729
6745
|
} })
|
|
6730
6746
|
});
|
|
@@ -6744,7 +6760,7 @@ const ROW_GROUPS = new Set([
|
|
|
6744
6760
|
"tbody",
|
|
6745
6761
|
"tfoot"
|
|
6746
6762
|
]);
|
|
6747
|
-
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).`;
|
|
6748
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.";
|
|
6749
6765
|
const getHostTagName = (jsxElement) => {
|
|
6750
6766
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
@@ -6812,28 +6828,28 @@ const htmlNoInvalidTableNesting = defineRule({
|
|
|
6812
6828
|
if (ROW_GROUPS.has(tagName)) {
|
|
6813
6829
|
if (actualParent !== "table") context.report({
|
|
6814
6830
|
node: node.openingElement.name,
|
|
6815
|
-
message: buildMessage$
|
|
6831
|
+
message: buildMessage$21(tagName, "`<table>`", actualParent)
|
|
6816
6832
|
});
|
|
6817
6833
|
return;
|
|
6818
6834
|
}
|
|
6819
6835
|
if (tagName === "tr") {
|
|
6820
6836
|
if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
|
|
6821
6837
|
node: node.openingElement.name,
|
|
6822
|
-
message: buildMessage$
|
|
6838
|
+
message: buildMessage$21(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
|
|
6823
6839
|
});
|
|
6824
6840
|
return;
|
|
6825
6841
|
}
|
|
6826
6842
|
if (tagName === "td" || tagName === "th") {
|
|
6827
6843
|
if (actualParent !== "tr") context.report({
|
|
6828
6844
|
node: node.openingElement.name,
|
|
6829
|
-
message: buildMessage$
|
|
6845
|
+
message: buildMessage$21(tagName, "`<tr>`", actualParent)
|
|
6830
6846
|
});
|
|
6831
6847
|
}
|
|
6832
6848
|
} })
|
|
6833
6849
|
});
|
|
6834
6850
|
//#endregion
|
|
6835
6851
|
//#region src/plugin/rules/correctness/html-no-nested-interactive.ts
|
|
6836
|
-
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.`;
|
|
6837
6853
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
6838
6854
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6839
6855
|
const opening = candidate.openingElement;
|
|
@@ -6861,13 +6877,13 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
6861
6877
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
6862
6878
|
context.report({
|
|
6863
6879
|
node: node.name,
|
|
6864
|
-
message: buildMessage$
|
|
6880
|
+
message: buildMessage$20(tagName)
|
|
6865
6881
|
});
|
|
6866
6882
|
} })
|
|
6867
6883
|
});
|
|
6868
6884
|
//#endregion
|
|
6869
6885
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
6870
|
-
const MESSAGE$
|
|
6886
|
+
const MESSAGE$41 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
|
|
6871
6887
|
const evaluateTitleValue = (value) => {
|
|
6872
6888
|
if (!value) return "missing";
|
|
6873
6889
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -6906,14 +6922,14 @@ const iframeHasTitle = defineRule({
|
|
|
6906
6922
|
if (!titleAttr) {
|
|
6907
6923
|
if (hasSpread || tag === "iframe") context.report({
|
|
6908
6924
|
node: node.name,
|
|
6909
|
-
message: MESSAGE$
|
|
6925
|
+
message: MESSAGE$41
|
|
6910
6926
|
});
|
|
6911
6927
|
return;
|
|
6912
6928
|
}
|
|
6913
6929
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
6914
6930
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
6915
6931
|
node: titleAttr,
|
|
6916
|
-
message: MESSAGE$
|
|
6932
|
+
message: MESSAGE$41
|
|
6917
6933
|
});
|
|
6918
6934
|
} })
|
|
6919
6935
|
});
|
|
@@ -7016,7 +7032,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
7016
7032
|
});
|
|
7017
7033
|
//#endregion
|
|
7018
7034
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
7019
|
-
const MESSAGE$
|
|
7035
|
+
const MESSAGE$40 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
|
|
7020
7036
|
const DEFAULT_COMPONENTS = ["img"];
|
|
7021
7037
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
7022
7038
|
"image",
|
|
@@ -7078,7 +7094,7 @@ const imgRedundantAlt = defineRule({
|
|
|
7078
7094
|
if (!altAttribute) return;
|
|
7079
7095
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
7080
7096
|
node: altAttribute,
|
|
7081
|
-
message: MESSAGE$
|
|
7097
|
+
message: MESSAGE$40
|
|
7082
7098
|
});
|
|
7083
7099
|
} };
|
|
7084
7100
|
}
|
|
@@ -7273,7 +7289,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
7273
7289
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7274
7290
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7275
7291
|
};
|
|
7276
|
-
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7292
|
+
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7277
7293
|
const getFirstParameterName = (fn) => {
|
|
7278
7294
|
const parameters = fn.params ?? [];
|
|
7279
7295
|
if (parameters.length !== 1) return null;
|
|
@@ -7391,7 +7407,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7391
7407
|
const args = node.arguments ?? [];
|
|
7392
7408
|
if (args.length === 0) return;
|
|
7393
7409
|
const reader = args[0];
|
|
7394
|
-
if (!isFunctionLike(reader)) return;
|
|
7410
|
+
if (!isFunctionLike$1(reader)) return;
|
|
7395
7411
|
const getParameterName = getFirstParameterName(reader);
|
|
7396
7412
|
if (!getParameterName) return;
|
|
7397
7413
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -7407,7 +7423,6 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7407
7423
|
//#endregion
|
|
7408
7424
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
7409
7425
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
7410
|
-
const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
|
|
7411
7426
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
7412
7427
|
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7413
7428
|
const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
@@ -7645,7 +7660,7 @@ const isInsideLoopContext = (node) => {
|
|
|
7645
7660
|
let current = node.parent;
|
|
7646
7661
|
while (current) {
|
|
7647
7662
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
7648
|
-
if (isFunctionLike$
|
|
7663
|
+
if (isFunctionLike$2(current)) {
|
|
7649
7664
|
if (isIteratorCallback(current)) return true;
|
|
7650
7665
|
return false;
|
|
7651
7666
|
}
|
|
@@ -7999,7 +8014,7 @@ const jsHoistIntl = defineRule({
|
|
|
7999
8014
|
let cursor = node.parent ?? null;
|
|
8000
8015
|
let inFunctionBody = false;
|
|
8001
8016
|
while (cursor) {
|
|
8002
|
-
if (isFunctionLike$
|
|
8017
|
+
if (isFunctionLike$2(cursor)) {
|
|
8003
8018
|
inFunctionBody = true;
|
|
8004
8019
|
const fnParent = cursor.parent;
|
|
8005
8020
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -9344,7 +9359,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
9344
9359
|
};
|
|
9345
9360
|
//#endregion
|
|
9346
9361
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9347
|
-
const buildMessage$
|
|
9362
|
+
const buildMessage$19 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
9348
9363
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9349
9364
|
const resolveSettings$30 = (settings) => {
|
|
9350
9365
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -9411,7 +9426,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9411
9426
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
9412
9427
|
if (total > max) context.report({
|
|
9413
9428
|
node,
|
|
9414
|
-
message: buildMessage$
|
|
9429
|
+
message: buildMessage$19(total, max)
|
|
9415
9430
|
});
|
|
9416
9431
|
};
|
|
9417
9432
|
return {
|
|
@@ -9426,7 +9441,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9426
9441
|
});
|
|
9427
9442
|
//#endregion
|
|
9428
9443
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
9429
|
-
const MESSAGE$
|
|
9444
|
+
const MESSAGE$39 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
|
|
9430
9445
|
const LITERAL_TEXT_TAGS = new Set([
|
|
9431
9446
|
"code",
|
|
9432
9447
|
"pre",
|
|
@@ -9461,11 +9476,20 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
9461
9476
|
if (isInsideLiteralTextTag(node)) return;
|
|
9462
9477
|
context.report({
|
|
9463
9478
|
node,
|
|
9464
|
-
message: MESSAGE$
|
|
9479
|
+
message: MESSAGE$39
|
|
9465
9480
|
});
|
|
9466
9481
|
} })
|
|
9467
9482
|
});
|
|
9468
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
|
|
9469
9493
|
//#region src/plugin/utils/is-inside-function-scope.ts
|
|
9470
9494
|
const FUNCTION_NODE_TYPES$1 = new Set([
|
|
9471
9495
|
"FunctionDeclaration",
|
|
@@ -9483,7 +9507,12 @@ const isInsideFunctionScope = (node) => {
|
|
|
9483
9507
|
};
|
|
9484
9508
|
//#endregion
|
|
9485
9509
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
9486
|
-
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
|
+
];
|
|
9487
9516
|
const isConstructedValue = (expression) => {
|
|
9488
9517
|
const stripped = stripParenExpression(expression);
|
|
9489
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;
|
|
@@ -9491,10 +9520,57 @@ const isConstructedValue = (expression) => {
|
|
|
9491
9520
|
if (isNodeOfType(stripped, "LogicalExpression")) return isConstructedValue(stripped.left) || isConstructedValue(stripped.right);
|
|
9492
9521
|
return false;
|
|
9493
9522
|
};
|
|
9494
|
-
const
|
|
9523
|
+
const isProviderMemberName = (node) => {
|
|
9495
9524
|
if (!isNodeOfType(node, "JSXMemberExpression")) return false;
|
|
9496
9525
|
return node.property.name === "Provider";
|
|
9497
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
|
+
};
|
|
9498
9574
|
const jsxNoConstructedContextValues = defineRule({
|
|
9499
9575
|
id: "jsx-no-constructed-context-values",
|
|
9500
9576
|
tags: ["react-jsx-only"],
|
|
@@ -9503,26 +9579,35 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9503
9579
|
category: "Performance",
|
|
9504
9580
|
create: (context) => {
|
|
9505
9581
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
if (
|
|
9513
|
-
|
|
9514
|
-
const
|
|
9515
|
-
|
|
9516
|
-
if (!
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
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
|
+
}
|
|
9524
9609
|
}
|
|
9525
|
-
}
|
|
9610
|
+
};
|
|
9526
9611
|
}
|
|
9527
9612
|
});
|
|
9528
9613
|
//#endregion
|
|
@@ -9548,7 +9633,8 @@ const jsxNoDuplicateProps = defineRule({
|
|
|
9548
9633
|
//#endregion
|
|
9549
9634
|
//#region src/plugin/utils/build-same-file-memo-registry.ts
|
|
9550
9635
|
const HOC_NAMES_FOR_MEMOISATION = new Set([
|
|
9551
|
-
|
|
9636
|
+
"memo",
|
|
9637
|
+
"React.memo",
|
|
9552
9638
|
"observer",
|
|
9553
9639
|
"observable",
|
|
9554
9640
|
"lazy",
|
|
@@ -9602,7 +9688,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
9602
9688
|
};
|
|
9603
9689
|
//#endregion
|
|
9604
9690
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
9605
|
-
const MESSAGE$
|
|
9691
|
+
const MESSAGE$37 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
|
|
9606
9692
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
9607
9693
|
"icon",
|
|
9608
9694
|
"Icon",
|
|
@@ -9870,7 +9956,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
9870
9956
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
9871
9957
|
context.report({
|
|
9872
9958
|
node,
|
|
9873
|
-
message: MESSAGE$
|
|
9959
|
+
message: MESSAGE$37
|
|
9874
9960
|
});
|
|
9875
9961
|
}
|
|
9876
9962
|
};
|
|
@@ -10158,7 +10244,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
10158
10244
|
];
|
|
10159
10245
|
//#endregion
|
|
10160
10246
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
10161
|
-
const MESSAGE$
|
|
10247
|
+
const MESSAGE$36 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
|
|
10162
10248
|
const isDataArrayPropName = (propName) => {
|
|
10163
10249
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
10164
10250
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10241,7 +10327,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10241
10327
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
10242
10328
|
context.report({
|
|
10243
10329
|
node,
|
|
10244
|
-
message: MESSAGE$
|
|
10330
|
+
message: MESSAGE$36
|
|
10245
10331
|
});
|
|
10246
10332
|
}
|
|
10247
10333
|
};
|
|
@@ -10499,7 +10585,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
10499
10585
|
]);
|
|
10500
10586
|
//#endregion
|
|
10501
10587
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
10502
|
-
const MESSAGE$
|
|
10588
|
+
const MESSAGE$35 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
|
|
10503
10589
|
const isAccessorPredicateName = (propName) => {
|
|
10504
10590
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
10505
10591
|
if (propName.length <= prefix.length) continue;
|
|
@@ -10704,7 +10790,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10704
10790
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
10705
10791
|
context.report({
|
|
10706
10792
|
node,
|
|
10707
|
-
message: MESSAGE$
|
|
10793
|
+
message: MESSAGE$35
|
|
10708
10794
|
});
|
|
10709
10795
|
}
|
|
10710
10796
|
};
|
|
@@ -10924,7 +11010,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
10924
11010
|
];
|
|
10925
11011
|
//#endregion
|
|
10926
11012
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
10927
|
-
const MESSAGE$
|
|
11013
|
+
const MESSAGE$34 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
|
|
10928
11014
|
const isConfigObjectPropName = (propName) => {
|
|
10929
11015
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
10930
11016
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11011,7 +11097,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11011
11097
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
11012
11098
|
context.report({
|
|
11013
11099
|
node,
|
|
11014
|
-
message: MESSAGE$
|
|
11100
|
+
message: MESSAGE$34
|
|
11015
11101
|
});
|
|
11016
11102
|
}
|
|
11017
11103
|
};
|
|
@@ -11019,7 +11105,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11019
11105
|
});
|
|
11020
11106
|
//#endregion
|
|
11021
11107
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
11022
|
-
const MESSAGE$
|
|
11108
|
+
const MESSAGE$33 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
|
|
11023
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;
|
|
11024
11110
|
const resolveSettings$29 = (settings) => {
|
|
11025
11111
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -11059,7 +11145,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11059
11145
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
11060
11146
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
11061
11147
|
node: attribute,
|
|
11062
|
-
message: MESSAGE$
|
|
11148
|
+
message: MESSAGE$33
|
|
11063
11149
|
});
|
|
11064
11150
|
}
|
|
11065
11151
|
} };
|
|
@@ -11347,7 +11433,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
11347
11433
|
});
|
|
11348
11434
|
//#endregion
|
|
11349
11435
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11350
|
-
const buildMessage$
|
|
11436
|
+
const buildMessage$18 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
11351
11437
|
const KNOWN_GLOBALS = new Set([
|
|
11352
11438
|
"globalThis",
|
|
11353
11439
|
"window",
|
|
@@ -11382,7 +11468,7 @@ const jsxNoUndef = defineRule({
|
|
|
11382
11468
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
11383
11469
|
context.report({
|
|
11384
11470
|
node: node.name,
|
|
11385
|
-
message: buildMessage$
|
|
11471
|
+
message: buildMessage$18(rootIdentifier)
|
|
11386
11472
|
});
|
|
11387
11473
|
} })
|
|
11388
11474
|
});
|
|
@@ -11481,7 +11567,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
11481
11567
|
});
|
|
11482
11568
|
//#endregion
|
|
11483
11569
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
11484
|
-
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.`;
|
|
11485
11571
|
const resolveSettings$26 = (settings) => {
|
|
11486
11572
|
const reactDoctor = settings?.["react-doctor"];
|
|
11487
11573
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -11597,7 +11683,7 @@ const jsxPascalCase = defineRule({
|
|
|
11597
11683
|
if (!isPascal && !isAllCaps) {
|
|
11598
11684
|
context.report({
|
|
11599
11685
|
node,
|
|
11600
|
-
message: buildMessage$
|
|
11686
|
+
message: buildMessage$17(segment, settings.allowAllCaps)
|
|
11601
11687
|
});
|
|
11602
11688
|
return;
|
|
11603
11689
|
}
|
|
@@ -11649,7 +11735,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
11649
11735
|
});
|
|
11650
11736
|
//#endregion
|
|
11651
11737
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
11652
|
-
const MESSAGE$
|
|
11738
|
+
const MESSAGE$32 = "JSX prop spreading is forbidden — list each prop explicitly.";
|
|
11653
11739
|
const resolveSettings$25 = (settings) => {
|
|
11654
11740
|
const reactDoctor = settings?.["react-doctor"];
|
|
11655
11741
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -11689,7 +11775,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11689
11775
|
}
|
|
11690
11776
|
context.report({
|
|
11691
11777
|
node: attribute,
|
|
11692
|
-
message: MESSAGE$
|
|
11778
|
+
message: MESSAGE$32
|
|
11693
11779
|
});
|
|
11694
11780
|
}
|
|
11695
11781
|
} };
|
|
@@ -11844,7 +11930,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
11844
11930
|
});
|
|
11845
11931
|
//#endregion
|
|
11846
11932
|
//#region src/plugin/rules/a11y/lang.ts
|
|
11847
|
-
const MESSAGE$
|
|
11933
|
+
const MESSAGE$31 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
|
|
11848
11934
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
11849
11935
|
"aa",
|
|
11850
11936
|
"ab",
|
|
@@ -12055,7 +12141,7 @@ const lang = defineRule({
|
|
|
12055
12141
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
12056
12142
|
context.report({
|
|
12057
12143
|
node: langAttr,
|
|
12058
|
-
message: MESSAGE$
|
|
12144
|
+
message: MESSAGE$31
|
|
12059
12145
|
});
|
|
12060
12146
|
return;
|
|
12061
12147
|
}
|
|
@@ -12064,13 +12150,13 @@ const lang = defineRule({
|
|
|
12064
12150
|
if (value === null) return;
|
|
12065
12151
|
if (!isValidLangTag(value)) context.report({
|
|
12066
12152
|
node: langAttr,
|
|
12067
|
-
message: MESSAGE$
|
|
12153
|
+
message: MESSAGE$31
|
|
12068
12154
|
});
|
|
12069
12155
|
} })
|
|
12070
12156
|
});
|
|
12071
12157
|
//#endregion
|
|
12072
12158
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
12073
|
-
const MESSAGE$
|
|
12159
|
+
const MESSAGE$30 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
|
|
12074
12160
|
const DEFAULT_AUDIO = ["audio"];
|
|
12075
12161
|
const DEFAULT_VIDEO = ["video"];
|
|
12076
12162
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -12110,7 +12196,7 @@ const mediaHasCaption = defineRule({
|
|
|
12110
12196
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
12111
12197
|
context.report({
|
|
12112
12198
|
node: node.name,
|
|
12113
|
-
message: MESSAGE$
|
|
12199
|
+
message: MESSAGE$30
|
|
12114
12200
|
});
|
|
12115
12201
|
return;
|
|
12116
12202
|
}
|
|
@@ -12127,7 +12213,7 @@ const mediaHasCaption = defineRule({
|
|
|
12127
12213
|
return kindValue.value.toLowerCase() === "captions";
|
|
12128
12214
|
})) context.report({
|
|
12129
12215
|
node: node.name,
|
|
12130
|
-
message: MESSAGE$
|
|
12216
|
+
message: MESSAGE$30
|
|
12131
12217
|
});
|
|
12132
12218
|
} };
|
|
12133
12219
|
}
|
|
@@ -12818,7 +12904,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
12818
12904
|
};
|
|
12819
12905
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
12820
12906
|
if (remainingDepth <= 0) return [];
|
|
12821
|
-
if (isFunctionLike$
|
|
12907
|
+
if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
|
|
12822
12908
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
12823
12909
|
for (const callArgument of expression.arguments ?? []) {
|
|
12824
12910
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -12941,7 +13027,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
12941
13027
|
});
|
|
12942
13028
|
//#endregion
|
|
12943
13029
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
12944
|
-
const MESSAGE$
|
|
13030
|
+
const MESSAGE$29 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
|
|
12945
13031
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
12946
13032
|
const noAccessKey = defineRule({
|
|
12947
13033
|
id: "no-access-key",
|
|
@@ -12957,7 +13043,7 @@ const noAccessKey = defineRule({
|
|
|
12957
13043
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
12958
13044
|
context.report({
|
|
12959
13045
|
node: accessKey,
|
|
12960
|
-
message: MESSAGE$
|
|
13046
|
+
message: MESSAGE$29
|
|
12961
13047
|
});
|
|
12962
13048
|
return;
|
|
12963
13049
|
}
|
|
@@ -12967,7 +13053,7 @@ const noAccessKey = defineRule({
|
|
|
12967
13053
|
if (isUndefinedIdentifier(expression)) return;
|
|
12968
13054
|
context.report({
|
|
12969
13055
|
node: accessKey,
|
|
12970
|
-
message: MESSAGE$
|
|
13056
|
+
message: MESSAGE$29
|
|
12971
13057
|
});
|
|
12972
13058
|
}
|
|
12973
13059
|
} })
|
|
@@ -13276,7 +13362,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
13276
13362
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
13277
13363
|
if (isNodeOfType(fn, "Identifier")) {
|
|
13278
13364
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
13279
|
-
if (definitionNode && isFunctionLike$
|
|
13365
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
|
|
13280
13366
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13281
13367
|
const initializer = definitionNode.init;
|
|
13282
13368
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -13369,14 +13455,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
13369
13455
|
return node ?? null;
|
|
13370
13456
|
};
|
|
13371
13457
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
13372
|
-
if (isFunctionLike$
|
|
13458
|
+
if (isFunctionLike$2(node)) return true;
|
|
13373
13459
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
13374
13460
|
if (isNodeOfType(node, "Identifier")) {
|
|
13375
13461
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
13376
|
-
if (definitionNode && isFunctionLike$
|
|
13462
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return true;
|
|
13377
13463
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13378
13464
|
const initializer = definitionNode.init;
|
|
13379
|
-
return isFunctionLike$
|
|
13465
|
+
return isFunctionLike$2(initializer);
|
|
13380
13466
|
}
|
|
13381
13467
|
}
|
|
13382
13468
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -13386,7 +13472,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13386
13472
|
if (visited.has(node)) return false;
|
|
13387
13473
|
visited.add(node);
|
|
13388
13474
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
13389
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$
|
|
13475
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
|
|
13390
13476
|
const record = node;
|
|
13391
13477
|
for (const [key, value] of Object.entries(record)) {
|
|
13392
13478
|
if (key === "parent") continue;
|
|
@@ -13398,7 +13484,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13398
13484
|
};
|
|
13399
13485
|
const hasCleanup = (analysis, node) => {
|
|
13400
13486
|
const fn = getEffectFn(analysis, node);
|
|
13401
|
-
if (!isFunctionLike$
|
|
13487
|
+
if (!isFunctionLike$2(fn)) return false;
|
|
13402
13488
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
13403
13489
|
return hasCleanupReturn(analysis, fn.body);
|
|
13404
13490
|
};
|
|
@@ -13440,7 +13526,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13440
13526
|
});
|
|
13441
13527
|
//#endregion
|
|
13442
13528
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
13443
|
-
const MESSAGE$
|
|
13529
|
+
const MESSAGE$28 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
|
|
13444
13530
|
const noAriaHiddenOnFocusable = defineRule({
|
|
13445
13531
|
id: "no-aria-hidden-on-focusable",
|
|
13446
13532
|
tags: ["react-jsx-only"],
|
|
@@ -13466,7 +13552,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
13466
13552
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
13467
13553
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
13468
13554
|
node: ariaHidden,
|
|
13469
|
-
message: MESSAGE$
|
|
13555
|
+
message: MESSAGE$28
|
|
13470
13556
|
});
|
|
13471
13557
|
} })
|
|
13472
13558
|
});
|
|
@@ -13746,7 +13832,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
13746
13832
|
let current = node;
|
|
13747
13833
|
while (current.parent) {
|
|
13748
13834
|
const parent = current.parent;
|
|
13749
|
-
if (isFunctionLike$
|
|
13835
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13750
13836
|
const callee = parent.callee;
|
|
13751
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);
|
|
13752
13838
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -13765,7 +13851,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
13765
13851
|
let current = node;
|
|
13766
13852
|
while (current.parent) {
|
|
13767
13853
|
const parent = current.parent;
|
|
13768
|
-
if (isFunctionLike$
|
|
13854
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13769
13855
|
const callee = parent.callee;
|
|
13770
13856
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
13771
13857
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -13833,7 +13919,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
13833
13919
|
});
|
|
13834
13920
|
//#endregion
|
|
13835
13921
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
13836
|
-
const MESSAGE$
|
|
13922
|
+
const MESSAGE$27 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
|
|
13837
13923
|
const SECOND_INDEX_METHODS = new Set([
|
|
13838
13924
|
"every",
|
|
13839
13925
|
"filter",
|
|
@@ -14035,7 +14121,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14035
14121
|
}
|
|
14036
14122
|
context.report({
|
|
14037
14123
|
node: keyAttribute,
|
|
14038
|
-
message: MESSAGE$
|
|
14124
|
+
message: MESSAGE$27
|
|
14039
14125
|
});
|
|
14040
14126
|
},
|
|
14041
14127
|
CallExpression(node) {
|
|
@@ -14055,7 +14141,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14055
14141
|
if (propName !== "key") continue;
|
|
14056
14142
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
14057
14143
|
node: property,
|
|
14058
|
-
message: MESSAGE$
|
|
14144
|
+
message: MESSAGE$27
|
|
14059
14145
|
});
|
|
14060
14146
|
}
|
|
14061
14147
|
}
|
|
@@ -14063,7 +14149,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14063
14149
|
});
|
|
14064
14150
|
//#endregion
|
|
14065
14151
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
14066
|
-
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.";
|
|
14067
14153
|
const resolveSettings$21 = (settings) => {
|
|
14068
14154
|
const reactDoctor = settings?.["react-doctor"];
|
|
14069
14155
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -14118,7 +14204,7 @@ const noAutofocus = defineRule({
|
|
|
14118
14204
|
}
|
|
14119
14205
|
context.report({
|
|
14120
14206
|
node: autoFocusAttribute,
|
|
14121
|
-
message: MESSAGE$
|
|
14207
|
+
message: MESSAGE$26
|
|
14122
14208
|
});
|
|
14123
14209
|
} };
|
|
14124
14210
|
}
|
|
@@ -14609,7 +14695,7 @@ const noChainStateUpdates = defineRule({
|
|
|
14609
14695
|
});
|
|
14610
14696
|
//#endregion
|
|
14611
14697
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
14612
|
-
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.";
|
|
14613
14699
|
const noChildrenProp = defineRule({
|
|
14614
14700
|
id: "no-children-prop",
|
|
14615
14701
|
severity: "warn",
|
|
@@ -14620,7 +14706,7 @@ const noChildrenProp = defineRule({
|
|
|
14620
14706
|
if (node.name.name !== "children") return;
|
|
14621
14707
|
context.report({
|
|
14622
14708
|
node: node.name,
|
|
14623
|
-
message: MESSAGE$
|
|
14709
|
+
message: MESSAGE$25
|
|
14624
14710
|
});
|
|
14625
14711
|
},
|
|
14626
14712
|
CallExpression(node) {
|
|
@@ -14633,7 +14719,7 @@ const noChildrenProp = defineRule({
|
|
|
14633
14719
|
const propertyKey = property.key;
|
|
14634
14720
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
14635
14721
|
node: propertyKey,
|
|
14636
|
-
message: MESSAGE$
|
|
14722
|
+
message: MESSAGE$25
|
|
14637
14723
|
});
|
|
14638
14724
|
}
|
|
14639
14725
|
}
|
|
@@ -14641,7 +14727,7 @@ const noChildrenProp = defineRule({
|
|
|
14641
14727
|
});
|
|
14642
14728
|
//#endregion
|
|
14643
14729
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
14644
|
-
const MESSAGE$
|
|
14730
|
+
const MESSAGE$24 = "`React.cloneElement` is uncommon and leads to fragile components.";
|
|
14645
14731
|
const noCloneElement = defineRule({
|
|
14646
14732
|
id: "no-clone-element",
|
|
14647
14733
|
severity: "warn",
|
|
@@ -14653,7 +14739,7 @@ const noCloneElement = defineRule({
|
|
|
14653
14739
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
14654
14740
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
14655
14741
|
node: callee,
|
|
14656
|
-
message: MESSAGE$
|
|
14742
|
+
message: MESSAGE$24
|
|
14657
14743
|
});
|
|
14658
14744
|
return;
|
|
14659
14745
|
}
|
|
@@ -14666,14 +14752,231 @@ const noCloneElement = defineRule({
|
|
|
14666
14752
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
14667
14753
|
context.report({
|
|
14668
14754
|
node: callee,
|
|
14669
|
-
message: MESSAGE$
|
|
14755
|
+
message: MESSAGE$24
|
|
14670
14756
|
});
|
|
14671
14757
|
}
|
|
14672
14758
|
} })
|
|
14673
14759
|
});
|
|
14674
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
|
|
14675
14978
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
14676
|
-
const MESSAGE$
|
|
14979
|
+
const MESSAGE$22 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
|
|
14677
14980
|
const noDanger = defineRule({
|
|
14678
14981
|
id: "no-danger",
|
|
14679
14982
|
severity: "warn",
|
|
@@ -14684,7 +14987,7 @@ const noDanger = defineRule({
|
|
|
14684
14987
|
if (!propAttribute) return;
|
|
14685
14988
|
context.report({
|
|
14686
14989
|
node: propAttribute.name,
|
|
14687
|
-
message: MESSAGE$
|
|
14990
|
+
message: MESSAGE$22
|
|
14688
14991
|
});
|
|
14689
14992
|
},
|
|
14690
14993
|
CallExpression(node) {
|
|
@@ -14696,7 +14999,7 @@ const noDanger = defineRule({
|
|
|
14696
14999
|
const propertyKey = property.key;
|
|
14697
15000
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
14698
15001
|
node: propertyKey,
|
|
14699
|
-
message: MESSAGE$
|
|
15002
|
+
message: MESSAGE$22
|
|
14700
15003
|
});
|
|
14701
15004
|
}
|
|
14702
15005
|
}
|
|
@@ -14704,7 +15007,7 @@ const noDanger = defineRule({
|
|
|
14704
15007
|
});
|
|
14705
15008
|
//#endregion
|
|
14706
15009
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
14707
|
-
const MESSAGE$
|
|
15010
|
+
const MESSAGE$21 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
|
|
14708
15011
|
const isLineBreak = (child) => {
|
|
14709
15012
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
14710
15013
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -14773,7 +15076,7 @@ const noDangerWithChildren = defineRule({
|
|
|
14773
15076
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
14774
15077
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
14775
15078
|
node: opening,
|
|
14776
|
-
message: MESSAGE$
|
|
15079
|
+
message: MESSAGE$21
|
|
14777
15080
|
});
|
|
14778
15081
|
},
|
|
14779
15082
|
CallExpression(node) {
|
|
@@ -14785,7 +15088,7 @@ const noDangerWithChildren = defineRule({
|
|
|
14785
15088
|
if (!propsShape.hasDangerously) return;
|
|
14786
15089
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
14787
15090
|
node,
|
|
14788
|
-
message: MESSAGE$
|
|
15091
|
+
message: MESSAGE$21
|
|
14789
15092
|
});
|
|
14790
15093
|
}
|
|
14791
15094
|
})
|
|
@@ -15160,7 +15463,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
15160
15463
|
};
|
|
15161
15464
|
const getInlineFunctionNode = (node) => {
|
|
15162
15465
|
if (!node) return null;
|
|
15163
|
-
if (isFunctionLike$
|
|
15466
|
+
if (isFunctionLike$2(node)) return node;
|
|
15164
15467
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
15165
15468
|
for (const argument of node.arguments ?? []) {
|
|
15166
15469
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -15171,7 +15474,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
15171
15474
|
const getNearestComponentFunction = (node) => {
|
|
15172
15475
|
let cursor = node.parent ?? null;
|
|
15173
15476
|
while (cursor) {
|
|
15174
|
-
if (isFunctionLike$
|
|
15477
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
15175
15478
|
cursor = cursor.parent ?? null;
|
|
15176
15479
|
}
|
|
15177
15480
|
return null;
|
|
@@ -15352,7 +15655,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
15352
15655
|
//#endregion
|
|
15353
15656
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
15354
15657
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
15355
|
-
const MESSAGE$
|
|
15658
|
+
const MESSAGE$20 = "Do not use `this.setState` in `componentDidMount`.";
|
|
15356
15659
|
const resolveSettings$20 = (settings) => {
|
|
15357
15660
|
const reactDoctor = settings?.["react-doctor"];
|
|
15358
15661
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15370,7 +15673,7 @@ const noDidMountSetState = defineRule({
|
|
|
15370
15673
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15371
15674
|
context.report({
|
|
15372
15675
|
node: node.callee,
|
|
15373
|
-
message: MESSAGE$
|
|
15676
|
+
message: MESSAGE$20
|
|
15374
15677
|
});
|
|
15375
15678
|
} };
|
|
15376
15679
|
}
|
|
@@ -15378,7 +15681,7 @@ const noDidMountSetState = defineRule({
|
|
|
15378
15681
|
//#endregion
|
|
15379
15682
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
15380
15683
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
15381
|
-
const MESSAGE$
|
|
15684
|
+
const MESSAGE$19 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
|
|
15382
15685
|
const resolveSettings$19 = (settings) => {
|
|
15383
15686
|
const reactDoctor = settings?.["react-doctor"];
|
|
15384
15687
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -15396,7 +15699,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
15396
15699
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
15397
15700
|
context.report({
|
|
15398
15701
|
node: node.callee,
|
|
15399
|
-
message: MESSAGE$
|
|
15702
|
+
message: MESSAGE$19
|
|
15400
15703
|
});
|
|
15401
15704
|
} };
|
|
15402
15705
|
}
|
|
@@ -15419,7 +15722,7 @@ const isStateMemberExpression = (node) => {
|
|
|
15419
15722
|
};
|
|
15420
15723
|
//#endregion
|
|
15421
15724
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
15422
|
-
const MESSAGE$
|
|
15725
|
+
const MESSAGE$18 = "Never mutate `this.state` directly.";
|
|
15423
15726
|
const shouldIgnoreMutation = (node) => {
|
|
15424
15727
|
let isConstructor = false;
|
|
15425
15728
|
let isInsideCallExpression = false;
|
|
@@ -15441,7 +15744,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
15441
15744
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
15442
15745
|
context.report({
|
|
15443
15746
|
node: reportNode,
|
|
15444
|
-
message: MESSAGE$
|
|
15747
|
+
message: MESSAGE$18
|
|
15445
15748
|
});
|
|
15446
15749
|
};
|
|
15447
15750
|
const noDirectMutationState = defineRule({
|
|
@@ -15497,7 +15800,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
15497
15800
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
15498
15801
|
if (!node || typeof node !== "object") return;
|
|
15499
15802
|
let nextShadowedStateNames = shadowedStateNames;
|
|
15500
|
-
if (isFunctionLike$
|
|
15803
|
+
if (isFunctionLike$2(node)) {
|
|
15501
15804
|
const localBindings = collectFunctionLocalBindings(node);
|
|
15502
15805
|
if (localBindings.size > 0) {
|
|
15503
15806
|
const merged = new Set(shadowedStateNames);
|
|
@@ -15543,7 +15846,7 @@ const noDirectStateMutation = defineRule({
|
|
|
15543
15846
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
15544
15847
|
if (!isNodeOfType(callee.property, "Identifier")) return;
|
|
15545
15848
|
const methodName = callee.property.name;
|
|
15546
|
-
if (!MUTATING_ARRAY_METHODS
|
|
15849
|
+
if (!MUTATING_ARRAY_METHODS.has(methodName)) return;
|
|
15547
15850
|
const rootName = getRootIdentifierName(callee.object);
|
|
15548
15851
|
if (!rootName || !stateValueToSetter.has(rootName)) return;
|
|
15549
15852
|
if (currentlyShadowed.has(rootName)) return;
|
|
@@ -15604,7 +15907,7 @@ const noDisabledZoom = defineRule({
|
|
|
15604
15907
|
});
|
|
15605
15908
|
//#endregion
|
|
15606
15909
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
15607
|
-
const buildMessage$
|
|
15910
|
+
const buildMessage$16 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
15608
15911
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
15609
15912
|
const resolveSettings$18 = (settings) => {
|
|
15610
15913
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -15624,7 +15927,7 @@ const noDistractingElements = defineRule({
|
|
|
15624
15927
|
const tag = getElementType(node, context.settings);
|
|
15625
15928
|
if (distractingTags.has(tag)) context.report({
|
|
15626
15929
|
node: node.name,
|
|
15627
|
-
message: buildMessage$
|
|
15930
|
+
message: buildMessage$16(tag)
|
|
15628
15931
|
});
|
|
15629
15932
|
} };
|
|
15630
15933
|
}
|
|
@@ -16064,6 +16367,69 @@ const noEffectEventInDeps = defineRule({
|
|
|
16064
16367
|
}
|
|
16065
16368
|
});
|
|
16066
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
|
|
16067
16433
|
//#region src/plugin/rules/security/no-eval.ts
|
|
16068
16434
|
const noEval = defineRule({
|
|
16069
16435
|
id: "no-eval",
|
|
@@ -16954,7 +17320,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
16954
17320
|
"ReactDOM",
|
|
16955
17321
|
"ReactDom"
|
|
16956
17322
|
]);
|
|
16957
|
-
const MESSAGE$
|
|
17323
|
+
const MESSAGE$17 = "Unexpected call to `findDOMNode` — removed in React 19.";
|
|
16958
17324
|
const noFindDomNode = defineRule({
|
|
16959
17325
|
id: "no-find-dom-node",
|
|
16960
17326
|
severity: "warn",
|
|
@@ -16964,7 +17330,7 @@ const noFindDomNode = defineRule({
|
|
|
16964
17330
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
16965
17331
|
context.report({
|
|
16966
17332
|
node: callee,
|
|
16967
|
-
message: MESSAGE$
|
|
17333
|
+
message: MESSAGE$17
|
|
16968
17334
|
});
|
|
16969
17335
|
return;
|
|
16970
17336
|
}
|
|
@@ -16975,7 +17341,7 @@ const noFindDomNode = defineRule({
|
|
|
16975
17341
|
if (callee.property.name !== "findDOMNode") return;
|
|
16976
17342
|
context.report({
|
|
16977
17343
|
node: callee.property,
|
|
16978
|
-
message: MESSAGE$
|
|
17344
|
+
message: MESSAGE$17
|
|
16979
17345
|
});
|
|
16980
17346
|
}
|
|
16981
17347
|
} })
|
|
@@ -17465,7 +17831,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
17465
17831
|
});
|
|
17466
17832
|
//#endregion
|
|
17467
17833
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
17468
|
-
const buildMessage$
|
|
17834
|
+
const buildMessage$15 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
17469
17835
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
17470
17836
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
17471
17837
|
tr: ["none", "presentation"],
|
|
@@ -17509,7 +17875,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
17509
17875
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
17510
17876
|
context.report({
|
|
17511
17877
|
node: roleAttribute,
|
|
17512
|
-
message: buildMessage$
|
|
17878
|
+
message: buildMessage$15(elementType, firstRole)
|
|
17513
17879
|
});
|
|
17514
17880
|
} };
|
|
17515
17881
|
}
|
|
@@ -17710,7 +18076,7 @@ const isInsideClassBody = (node) => {
|
|
|
17710
18076
|
let current = node.parent;
|
|
17711
18077
|
while (current) {
|
|
17712
18078
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
17713
|
-
if (isFunctionLike$
|
|
18079
|
+
if (isFunctionLike$2(current)) return false;
|
|
17714
18080
|
current = current.parent;
|
|
17715
18081
|
}
|
|
17716
18082
|
return false;
|
|
@@ -17953,7 +18319,7 @@ const noMoment = defineRule({
|
|
|
17953
18319
|
});
|
|
17954
18320
|
//#endregion
|
|
17955
18321
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17956
|
-
const buildMessage$
|
|
18322
|
+
const buildMessage$14 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17957
18323
|
const resolveSettings$16 = (settings) => {
|
|
17958
18324
|
const reactDoctor = settings?.["react-doctor"];
|
|
17959
18325
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -18274,7 +18640,7 @@ const noMultiComp = defineRule({
|
|
|
18274
18640
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
18275
18641
|
for (const component of flagged.slice(1)) context.report({
|
|
18276
18642
|
node: component.reportNode,
|
|
18277
|
-
message: buildMessage$
|
|
18643
|
+
message: buildMessage$14(component.name)
|
|
18278
18644
|
});
|
|
18279
18645
|
} };
|
|
18280
18646
|
}
|
|
@@ -18351,25 +18717,315 @@ const noMutableInDeps = defineRule({
|
|
|
18351
18717
|
}
|
|
18352
18718
|
});
|
|
18353
18719
|
//#endregion
|
|
18354
|
-
//#region src/plugin/rules/state-and-effects/
|
|
18355
|
-
const
|
|
18356
|
-
|
|
18357
|
-
"
|
|
18358
|
-
"
|
|
18359
|
-
"
|
|
18360
|
-
"
|
|
18361
|
-
"
|
|
18362
|
-
"
|
|
18363
|
-
"
|
|
18364
|
-
"
|
|
18365
|
-
"
|
|
18366
|
-
|
|
18367
|
-
|
|
18368
|
-
"
|
|
18369
|
-
"
|
|
18370
|
-
"delete",
|
|
18371
|
-
"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"
|
|
18372
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.";
|
|
18373
19029
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
18374
19030
|
"copyWithin",
|
|
18375
19031
|
"fill",
|
|
@@ -18388,7 +19044,6 @@ const cloneReducerPathState = (state) => ({
|
|
|
18388
19044
|
mutableStateSourceNames: new Set(state.mutableStateSourceNames),
|
|
18389
19045
|
mutations: [...state.mutations]
|
|
18390
19046
|
});
|
|
18391
|
-
const isFunctionLikeAstNode = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
|
|
18392
19047
|
const isSpecifierImportedFromReact = (node) => {
|
|
18393
19048
|
const parent = node.parent ?? null;
|
|
18394
19049
|
return parent !== null && isNodeOfType(parent, "ImportDeclaration") && parent.source.value === "react";
|
|
@@ -18415,19 +19070,16 @@ const isCallToImportedReactUseReducer = (node) => {
|
|
|
18415
19070
|
const binding = findVariableInitializer(callee.object, callee.object.name);
|
|
18416
19071
|
return Boolean(binding?.initializer && isReactNamespaceOrDefaultImportSpecifier(binding.initializer));
|
|
18417
19072
|
};
|
|
18418
|
-
const resolveSameFileReducerFunction = (node) => {
|
|
18419
|
-
if (!node) return null;
|
|
18420
|
-
const unwrappedNode = stripParenExpression(node);
|
|
18421
|
-
if (isFunctionLikeAstNode(unwrappedNode)) return unwrappedNode;
|
|
18422
|
-
if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
|
|
18423
|
-
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
18424
|
-
if (!initializer) return null;
|
|
18425
|
-
const unwrappedInitializer = stripParenExpression(initializer);
|
|
18426
|
-
return isFunctionLikeAstNode(unwrappedInitializer) ? unwrappedInitializer : null;
|
|
18427
|
-
};
|
|
18428
19073
|
const isStaticMethodCallOnNamedObject = (node, objectName, methodNames) => {
|
|
18429
19074
|
const unwrappedNode = stripParenExpression(node);
|
|
18430
|
-
|
|
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;
|
|
18431
19083
|
};
|
|
18432
19084
|
const isExpressionRootedInMutableReducerStateSource = (node, state) => {
|
|
18433
19085
|
let current = stripParenExpression(node);
|
|
@@ -18463,11 +19115,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
18463
19115
|
return false;
|
|
18464
19116
|
};
|
|
18465
19117
|
const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
18466
|
-
if (
|
|
19118
|
+
if (isFunctionLike$2(node)) return [];
|
|
18467
19119
|
const mutations = [];
|
|
18468
19120
|
walkAst(node, (child) => {
|
|
18469
19121
|
const unwrappedChild = stripParenExpression(child);
|
|
18470
|
-
if (child !== node &&
|
|
19122
|
+
if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
|
|
18471
19123
|
if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
|
|
18472
19124
|
if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
|
|
18473
19125
|
return;
|
|
@@ -18486,6 +19138,10 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
18486
19138
|
mutations.push({ node: unwrappedChild });
|
|
18487
19139
|
return;
|
|
18488
19140
|
}
|
|
19141
|
+
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && isLodashMutatorCall(unwrappedChild)) {
|
|
19142
|
+
mutations.push({ node: unwrappedChild });
|
|
19143
|
+
return;
|
|
19144
|
+
}
|
|
18489
19145
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
18490
19146
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
18491
19147
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
@@ -18512,18 +19168,47 @@ const restoreOuterIdentityForBlockScopedNames = (blockState, outerState, blockSc
|
|
|
18512
19168
|
}
|
|
18513
19169
|
return nextState;
|
|
18514
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
|
+
};
|
|
18515
19197
|
const updateReducerStateIdentityForVariableDeclaration = (declaration, state) => {
|
|
18516
19198
|
for (const declarator of declaration.declarations ?? []) {
|
|
18517
|
-
if (
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18521
|
-
|
|
18522
|
-
|
|
18523
|
-
|
|
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);
|
|
18524
19209
|
continue;
|
|
18525
19210
|
}
|
|
18526
|
-
if (isExpressionReachableFromOriginalReducerState(declarator.init, state)) state
|
|
19211
|
+
if ((isNodeOfType(declarator.id, "ObjectPattern") || isNodeOfType(declarator.id, "ArrayPattern")) && isExpressionReachableFromOriginalReducerState(declarator.init, state)) recordDestructuredAliasNames(declarator.id, state);
|
|
18527
19212
|
}
|
|
18528
19213
|
};
|
|
18529
19214
|
const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) => {
|
|
@@ -18538,24 +19223,40 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
|
|
|
18538
19223
|
}
|
|
18539
19224
|
if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
|
|
18540
19225
|
};
|
|
18541
|
-
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes) => {
|
|
18542
|
-
if (!
|
|
19226
|
+
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
|
|
19227
|
+
if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
|
|
18543
19228
|
const firstParam = functionNode.params?.[0];
|
|
18544
19229
|
const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
|
|
18545
19230
|
if (!stateName) return;
|
|
18546
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
|
+
}
|
|
18547
19242
|
for (const mutation of mutations) {
|
|
18548
19243
|
if (reportedNodes.has(mutation.node)) continue;
|
|
18549
19244
|
reportedNodes.add(mutation.node);
|
|
18550
19245
|
context.report({
|
|
18551
19246
|
node: mutation.node,
|
|
18552
|
-
message: MESSAGE$
|
|
19247
|
+
message: MESSAGE$16
|
|
18553
19248
|
});
|
|
18554
19249
|
}
|
|
18555
19250
|
};
|
|
19251
|
+
let pathBudgetExceeded = false;
|
|
18556
19252
|
const analyzeReducerStatementListByPath = (statements, initialState) => {
|
|
19253
|
+
if (pathBudgetExceeded) return [cloneReducerPathState(initialState)];
|
|
18557
19254
|
let activeStates = [cloneReducerPathState(initialState)];
|
|
18558
19255
|
for (const statement of statements) {
|
|
19256
|
+
if (activeStates.length > 1e3) {
|
|
19257
|
+
pathBudgetExceeded = true;
|
|
19258
|
+
break;
|
|
19259
|
+
}
|
|
18559
19260
|
const nextStates = [];
|
|
18560
19261
|
for (const activeState of activeStates) {
|
|
18561
19262
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
@@ -18624,18 +19325,23 @@ const noMutatingReducerState = defineRule({
|
|
|
18624
19325
|
create: (context) => {
|
|
18625
19326
|
const analyzedReducers = /* @__PURE__ */ new WeakSet();
|
|
18626
19327
|
const reportedNodes = /* @__PURE__ */ new WeakSet();
|
|
19328
|
+
const currentFilename = context.filename;
|
|
18627
19329
|
return { CallExpression(node) {
|
|
18628
19330
|
if (!isCallToImportedReactUseReducer(node)) return;
|
|
18629
|
-
const
|
|
18630
|
-
if (!
|
|
18631
|
-
analyzedReducers.
|
|
18632
|
-
|
|
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
|
+
});
|
|
18633
19339
|
} };
|
|
18634
19340
|
}
|
|
18635
19341
|
});
|
|
18636
19342
|
//#endregion
|
|
18637
19343
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
18638
|
-
const buildMessage$
|
|
19344
|
+
const buildMessage$13 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
18639
19345
|
const noNamespace = defineRule({
|
|
18640
19346
|
id: "no-namespace",
|
|
18641
19347
|
severity: "warn",
|
|
@@ -18647,7 +19353,7 @@ const noNamespace = defineRule({
|
|
|
18647
19353
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
18648
19354
|
context.report({
|
|
18649
19355
|
node: namespaced,
|
|
18650
|
-
message: buildMessage$
|
|
19356
|
+
message: buildMessage$13(fullName)
|
|
18651
19357
|
});
|
|
18652
19358
|
},
|
|
18653
19359
|
CallExpression(node) {
|
|
@@ -18658,7 +19364,7 @@ const noNamespace = defineRule({
|
|
|
18658
19364
|
if (!firstArgument.value.includes(":")) return;
|
|
18659
19365
|
context.report({
|
|
18660
19366
|
node: firstArgument,
|
|
18661
|
-
message: buildMessage$
|
|
19367
|
+
message: buildMessage$13(firstArgument.value)
|
|
18662
19368
|
});
|
|
18663
19369
|
}
|
|
18664
19370
|
})
|
|
@@ -18702,7 +19408,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
18702
19408
|
});
|
|
18703
19409
|
//#endregion
|
|
18704
19410
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
18705
|
-
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.`;
|
|
18706
19412
|
const INTERACTIVE_HANDLERS = [
|
|
18707
19413
|
"onClick",
|
|
18708
19414
|
"onMouseDown",
|
|
@@ -18728,13 +19434,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
18728
19434
|
}
|
|
18729
19435
|
context.report({
|
|
18730
19436
|
node: node.name,
|
|
18731
|
-
message: buildMessage$
|
|
19437
|
+
message: buildMessage$12(tag)
|
|
18732
19438
|
});
|
|
18733
19439
|
} })
|
|
18734
19440
|
});
|
|
18735
19441
|
//#endregion
|
|
18736
19442
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
18737
|
-
const buildMessage$
|
|
19443
|
+
const buildMessage$11 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
18738
19444
|
const DEFAULT_ALLOWED_ROLES = {
|
|
18739
19445
|
ul: [
|
|
18740
19446
|
"menu",
|
|
@@ -18798,14 +19504,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
18798
19504
|
if (!isInteractiveRole(firstRole)) return;
|
|
18799
19505
|
context.report({
|
|
18800
19506
|
node: roleAttribute,
|
|
18801
|
-
message: buildMessage$
|
|
19507
|
+
message: buildMessage$11(elementType, firstRole)
|
|
18802
19508
|
});
|
|
18803
19509
|
} };
|
|
18804
19510
|
}
|
|
18805
19511
|
});
|
|
18806
19512
|
//#endregion
|
|
18807
19513
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
18808
|
-
const MESSAGE$
|
|
19514
|
+
const MESSAGE$15 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
|
|
18809
19515
|
const resolveSettings$14 = (settings) => {
|
|
18810
19516
|
const reactDoctor = settings?.["react-doctor"];
|
|
18811
19517
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -18832,7 +19538,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18832
19538
|
if (numeric === null) {
|
|
18833
19539
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
18834
19540
|
node: tabIndex,
|
|
18835
|
-
message: MESSAGE$
|
|
19541
|
+
message: MESSAGE$15
|
|
18836
19542
|
});
|
|
18837
19543
|
return;
|
|
18838
19544
|
}
|
|
@@ -18845,7 +19551,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18845
19551
|
if (!roleAttribute) {
|
|
18846
19552
|
context.report({
|
|
18847
19553
|
node: tabIndex,
|
|
18848
|
-
message: MESSAGE$
|
|
19554
|
+
message: MESSAGE$15
|
|
18849
19555
|
});
|
|
18850
19556
|
return;
|
|
18851
19557
|
}
|
|
@@ -18859,7 +19565,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
18859
19565
|
}
|
|
18860
19566
|
context.report({
|
|
18861
19567
|
node: tabIndex,
|
|
18862
|
-
message: MESSAGE$
|
|
19568
|
+
message: MESSAGE$15
|
|
18863
19569
|
});
|
|
18864
19570
|
} };
|
|
18865
19571
|
}
|
|
@@ -19388,6 +20094,63 @@ const noPropCallbackInEffect = defineRule({
|
|
|
19388
20094
|
}
|
|
19389
20095
|
});
|
|
19390
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
|
|
19391
20154
|
//#region src/plugin/rules/design/no-pure-black-background.ts
|
|
19392
20155
|
const noPureBlackBackground = defineRule({
|
|
19393
20156
|
id: "no-pure-black-background",
|
|
@@ -19419,8 +20182,94 @@ const noPureBlackBackground = defineRule({
|
|
|
19419
20182
|
})
|
|
19420
20183
|
});
|
|
19421
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
|
|
19422
20271
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
19423
|
-
const MESSAGE$
|
|
20272
|
+
const MESSAGE$14 = "`React.Children` is uncommon and leads to fragile components.";
|
|
19424
20273
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
19425
20274
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
19426
20275
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -19445,13 +20294,13 @@ const noReactChildren = defineRule({
|
|
|
19445
20294
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
19446
20295
|
context.report({
|
|
19447
20296
|
node: calleeOuter,
|
|
19448
|
-
message: MESSAGE$
|
|
20297
|
+
message: MESSAGE$14
|
|
19449
20298
|
});
|
|
19450
20299
|
return;
|
|
19451
20300
|
}
|
|
19452
20301
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
19453
20302
|
node: calleeOuter,
|
|
19454
|
-
message: MESSAGE$
|
|
20303
|
+
message: MESSAGE$14
|
|
19455
20304
|
});
|
|
19456
20305
|
} })
|
|
19457
20306
|
});
|
|
@@ -19647,7 +20496,7 @@ const getTagsForRole = (role) => {
|
|
|
19647
20496
|
};
|
|
19648
20497
|
//#endregion
|
|
19649
20498
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
19650
|
-
const buildMessage$
|
|
20499
|
+
const buildMessage$9 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
19651
20500
|
const resolveSettings$13 = (settings) => {
|
|
19652
20501
|
const reactDoctor = settings?.["react-doctor"];
|
|
19653
20502
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -19670,14 +20519,14 @@ const noRedundantRoles = defineRule({
|
|
|
19670
20519
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
19671
20520
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
19672
20521
|
node: roleAttr,
|
|
19673
|
-
message: buildMessage$
|
|
20522
|
+
message: buildMessage$9(tag, role)
|
|
19674
20523
|
});
|
|
19675
20524
|
} };
|
|
19676
20525
|
}
|
|
19677
20526
|
});
|
|
19678
20527
|
//#endregion
|
|
19679
20528
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
19680
|
-
const buildMessage$
|
|
20529
|
+
const buildMessage$8 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
19681
20530
|
const isPureComponentSuper = (superClass) => {
|
|
19682
20531
|
if (!superClass) return false;
|
|
19683
20532
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -19709,7 +20558,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
19709
20558
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
19710
20559
|
context.report({
|
|
19711
20560
|
node: reportNode,
|
|
19712
|
-
message: buildMessage$
|
|
20561
|
+
message: buildMessage$8(className)
|
|
19713
20562
|
});
|
|
19714
20563
|
};
|
|
19715
20564
|
return {
|
|
@@ -19768,7 +20617,7 @@ const noRenderPropChildren = defineRule({
|
|
|
19768
20617
|
});
|
|
19769
20618
|
//#endregion
|
|
19770
20619
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
19771
|
-
const MESSAGE$
|
|
20620
|
+
const MESSAGE$13 = "Do not use the return value from `ReactDOM.render`.";
|
|
19772
20621
|
const isReactDomRenderCall = (node) => {
|
|
19773
20622
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19774
20623
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -19791,7 +20640,7 @@ const noRenderReturnValue = defineRule({
|
|
|
19791
20640
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
19792
20641
|
context.report({
|
|
19793
20642
|
node: node.callee,
|
|
19794
|
-
message: MESSAGE$
|
|
20643
|
+
message: MESSAGE$13
|
|
19795
20644
|
});
|
|
19796
20645
|
} })
|
|
19797
20646
|
});
|
|
@@ -20186,7 +21035,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
20186
21035
|
const isInsideServerOnlyScope = (node) => {
|
|
20187
21036
|
let currentNode = node.parent ?? null;
|
|
20188
21037
|
while (currentNode) {
|
|
20189
|
-
if (isFunctionLike$
|
|
21038
|
+
if (isFunctionLike$2(currentNode)) {
|
|
20190
21039
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
20191
21040
|
}
|
|
20192
21041
|
currentNode = currentNode.parent ?? null;
|
|
@@ -20240,6 +21089,363 @@ const noSecretsInClientCode = defineRule({
|
|
|
20240
21089
|
}
|
|
20241
21090
|
});
|
|
20242
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
|
|
20243
21449
|
//#region src/plugin/utils/get-parent-component.ts
|
|
20244
21450
|
const getParentComponent = (node) => {
|
|
20245
21451
|
let ancestor = node.parent;
|
|
@@ -20251,7 +21457,7 @@ const getParentComponent = (node) => {
|
|
|
20251
21457
|
};
|
|
20252
21458
|
//#endregion
|
|
20253
21459
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
20254
|
-
const MESSAGE$
|
|
21460
|
+
const MESSAGE$12 = "Do not use `this.setState` in components.";
|
|
20255
21461
|
const noSetState = defineRule({
|
|
20256
21462
|
id: "no-set-state",
|
|
20257
21463
|
severity: "warn",
|
|
@@ -20265,7 +21471,7 @@ const noSetState = defineRule({
|
|
|
20265
21471
|
if (!getParentComponent(node)) return;
|
|
20266
21472
|
context.report({
|
|
20267
21473
|
node: node.callee,
|
|
20268
|
-
message: MESSAGE$
|
|
21474
|
+
message: MESSAGE$12
|
|
20269
21475
|
});
|
|
20270
21476
|
} })
|
|
20271
21477
|
});
|
|
@@ -20424,7 +21630,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
20424
21630
|
};
|
|
20425
21631
|
//#endregion
|
|
20426
21632
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
20427
|
-
const MESSAGE$
|
|
21633
|
+
const MESSAGE$11 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
20428
21634
|
const DEFAULT_HANDLERS = [
|
|
20429
21635
|
"onClick",
|
|
20430
21636
|
"onMouseDown",
|
|
@@ -20483,7 +21689,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20483
21689
|
if (!roleAttribute || !roleAttribute.value) {
|
|
20484
21690
|
context.report({
|
|
20485
21691
|
node: node.name,
|
|
20486
|
-
message: MESSAGE$
|
|
21692
|
+
message: MESSAGE$11
|
|
20487
21693
|
});
|
|
20488
21694
|
return;
|
|
20489
21695
|
}
|
|
@@ -20493,14 +21699,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
20493
21699
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
20494
21700
|
context.report({
|
|
20495
21701
|
node: node.name,
|
|
20496
|
-
message: MESSAGE$
|
|
21702
|
+
message: MESSAGE$11
|
|
20497
21703
|
});
|
|
20498
21704
|
return;
|
|
20499
21705
|
}
|
|
20500
21706
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
20501
21707
|
context.report({
|
|
20502
21708
|
node: node.name,
|
|
20503
|
-
message: MESSAGE$
|
|
21709
|
+
message: MESSAGE$11
|
|
20504
21710
|
});
|
|
20505
21711
|
} };
|
|
20506
21712
|
}
|
|
@@ -20556,7 +21762,7 @@ const noStringRefs = defineRule({
|
|
|
20556
21762
|
});
|
|
20557
21763
|
//#endregion
|
|
20558
21764
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
20559
|
-
const MESSAGE$
|
|
21765
|
+
const MESSAGE$10 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
20560
21766
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
20561
21767
|
let ancestor = node.parent;
|
|
20562
21768
|
while (ancestor) {
|
|
@@ -20624,7 +21830,7 @@ const noThisInSfc = defineRule({
|
|
|
20624
21830
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
20625
21831
|
context.report({
|
|
20626
21832
|
node,
|
|
20627
|
-
message: MESSAGE$
|
|
21833
|
+
message: MESSAGE$10
|
|
20628
21834
|
});
|
|
20629
21835
|
} };
|
|
20630
21836
|
}
|
|
@@ -20806,7 +22012,7 @@ const ESCAPED_VERSIONS = {
|
|
|
20806
22012
|
">": "`>` / `>`",
|
|
20807
22013
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
20808
22014
|
};
|
|
20809
|
-
const buildMessage$
|
|
22015
|
+
const buildMessage$7 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
20810
22016
|
const noUnescapedEntities = defineRule({
|
|
20811
22017
|
id: "no-unescaped-entities",
|
|
20812
22018
|
severity: "warn",
|
|
@@ -20817,7 +22023,7 @@ const noUnescapedEntities = defineRule({
|
|
|
20817
22023
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
20818
22024
|
context.report({
|
|
20819
22025
|
node,
|
|
20820
|
-
message: buildMessage$
|
|
22026
|
+
message: buildMessage$7(character)
|
|
20821
22027
|
});
|
|
20822
22028
|
return;
|
|
20823
22029
|
}
|
|
@@ -21838,7 +23044,7 @@ const SAFER_REPLACEMENT = {
|
|
|
21838
23044
|
componentWillUpdate: "componentDidUpdate",
|
|
21839
23045
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
21840
23046
|
};
|
|
21841
|
-
const buildMessage$
|
|
23047
|
+
const buildMessage$6 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
21842
23048
|
const resolveSettings$9 = (settings) => {
|
|
21843
23049
|
const reactDoctor = settings?.["react-doctor"];
|
|
21844
23050
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21886,7 +23092,7 @@ const noUnsafe = defineRule({
|
|
|
21886
23092
|
if (!getParentComponent(node)) return;
|
|
21887
23093
|
context.report({
|
|
21888
23094
|
node: node.key,
|
|
21889
|
-
message: buildMessage$
|
|
23095
|
+
message: buildMessage$6(name)
|
|
21890
23096
|
});
|
|
21891
23097
|
},
|
|
21892
23098
|
Property(node) {
|
|
@@ -21897,7 +23103,7 @@ const noUnsafe = defineRule({
|
|
|
21897
23103
|
if (isEs5Component(ancestor)) {
|
|
21898
23104
|
context.report({
|
|
21899
23105
|
node: node.key,
|
|
21900
|
-
message: buildMessage$
|
|
23106
|
+
message: buildMessage$6(name)
|
|
21901
23107
|
});
|
|
21902
23108
|
return;
|
|
21903
23109
|
}
|
|
@@ -21909,7 +23115,7 @@ const noUnsafe = defineRule({
|
|
|
21909
23115
|
});
|
|
21910
23116
|
//#endregion
|
|
21911
23117
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21912
|
-
const buildMessage$
|
|
23118
|
+
const buildMessage$5 = (parentName, isInProp, allowAsProps) => {
|
|
21913
23119
|
let message = "Don't define components inside another component";
|
|
21914
23120
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21915
23121
|
message += " — extract it to module scope.";
|
|
@@ -21978,7 +23184,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
21978
23184
|
const findEnclosingComponent = (node) => {
|
|
21979
23185
|
let walker = node.parent;
|
|
21980
23186
|
while (walker) {
|
|
21981
|
-
if (isFunctionLike$
|
|
23187
|
+
if (isFunctionLike$2(walker)) {
|
|
21982
23188
|
const componentName = inferFunctionLikeName(walker);
|
|
21983
23189
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
21984
23190
|
component: walker,
|
|
@@ -22144,7 +23350,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22144
23350
|
if (!enclosing) return;
|
|
22145
23351
|
context.report({
|
|
22146
23352
|
node: reportNode,
|
|
22147
|
-
message: buildMessage$
|
|
23353
|
+
message: buildMessage$5(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
22148
23354
|
});
|
|
22149
23355
|
};
|
|
22150
23356
|
const checkFunctionLike = (node) => {
|
|
@@ -22179,15 +23385,6 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22179
23385
|
}
|
|
22180
23386
|
});
|
|
22181
23387
|
//#endregion
|
|
22182
|
-
//#region src/plugin/utils/is-canonical-react-namespace-name.ts
|
|
22183
|
-
const isCanonicalReactNamespaceName = (namespaceName) => {
|
|
22184
|
-
if (namespaceName === "React") return true;
|
|
22185
|
-
if (namespaceName === "react") return true;
|
|
22186
|
-
if (namespaceName.startsWith("_react")) return true;
|
|
22187
|
-
if (namespaceName.startsWith("_React")) return true;
|
|
22188
|
-
return false;
|
|
22189
|
-
};
|
|
22190
|
-
//#endregion
|
|
22191
23388
|
//#region src/plugin/rules/performance/no-usememo-simple-expression.ts
|
|
22192
23389
|
const isSimpleExpression = (node) => {
|
|
22193
23390
|
if (!node) return false;
|
|
@@ -22276,7 +23473,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
22276
23473
|
//#endregion
|
|
22277
23474
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
22278
23475
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
22279
|
-
const MESSAGE$
|
|
23476
|
+
const MESSAGE$9 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
22280
23477
|
const resolveSettings$7 = (settings) => {
|
|
22281
23478
|
const reactDoctor = settings?.["react-doctor"];
|
|
22282
23479
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -22309,7 +23506,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
22309
23506
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
22310
23507
|
context.report({
|
|
22311
23508
|
node: node.callee,
|
|
22312
|
-
message: MESSAGE$
|
|
23509
|
+
message: MESSAGE$9
|
|
22313
23510
|
});
|
|
22314
23511
|
} };
|
|
22315
23512
|
}
|
|
@@ -22922,7 +24119,7 @@ const REACT_HOOK_NAMES = new Set([
|
|
|
22922
24119
|
"useSyncExternalStore",
|
|
22923
24120
|
"useTransition"
|
|
22924
24121
|
]);
|
|
22925
|
-
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.`;
|
|
22926
24123
|
const preactNoReactHooksImport = defineRule({
|
|
22927
24124
|
id: "preact-no-react-hooks-import",
|
|
22928
24125
|
requires: ["pure-preact"],
|
|
@@ -22945,7 +24142,7 @@ const preactNoReactHooksImport = defineRule({
|
|
|
22945
24142
|
});
|
|
22946
24143
|
context.report({
|
|
22947
24144
|
node,
|
|
22948
|
-
message: buildMessage$
|
|
24145
|
+
message: buildMessage$4(importedNames)
|
|
22949
24146
|
});
|
|
22950
24147
|
} })
|
|
22951
24148
|
});
|
|
@@ -23002,7 +24199,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
23002
24199
|
});
|
|
23003
24200
|
//#endregion
|
|
23004
24201
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
23005
|
-
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.";
|
|
23006
24203
|
const preactPreferOndblclick = defineRule({
|
|
23007
24204
|
id: "preact-prefer-ondblclick",
|
|
23008
24205
|
requires: ["pure-preact"],
|
|
@@ -23016,7 +24213,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
23016
24213
|
if (!onDoubleClickAttribute) return;
|
|
23017
24214
|
context.report({
|
|
23018
24215
|
node: onDoubleClickAttribute,
|
|
23019
|
-
message: MESSAGE$
|
|
24216
|
+
message: MESSAGE$8
|
|
23020
24217
|
});
|
|
23021
24218
|
} })
|
|
23022
24219
|
});
|
|
@@ -23124,7 +24321,7 @@ const preferEs6Class = defineRule({
|
|
|
23124
24321
|
});
|
|
23125
24322
|
//#endregion
|
|
23126
24323
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
23127
|
-
const MESSAGE$
|
|
24324
|
+
const MESSAGE$7 = "Class component should be written as a function component — use hooks instead.";
|
|
23128
24325
|
const resolveSettings$4 = (settings) => {
|
|
23129
24326
|
const reactDoctor = settings?.["react-doctor"];
|
|
23130
24327
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -23162,7 +24359,7 @@ const preferFunctionComponent = defineRule({
|
|
|
23162
24359
|
const reportNode = node.id ?? node;
|
|
23163
24360
|
context.report({
|
|
23164
24361
|
node: reportNode,
|
|
23165
|
-
message: MESSAGE$
|
|
24362
|
+
message: MESSAGE$7
|
|
23166
24363
|
});
|
|
23167
24364
|
};
|
|
23168
24365
|
return {
|
|
@@ -23219,6 +24416,276 @@ const preferHtmlDialog = defineRule({
|
|
|
23219
24416
|
} })
|
|
23220
24417
|
});
|
|
23221
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
|
|
23222
24689
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
23223
24690
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
23224
24691
|
const preferTagOverRole = defineRule({
|
|
@@ -23769,91 +25236,216 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
23769
25236
|
} })
|
|
23770
25237
|
});
|
|
23771
25238
|
//#endregion
|
|
23772
|
-
//#region src/plugin/utils/has-binding-named.ts
|
|
23773
|
-
const hasBindingNamed = (root, bindingName) => {
|
|
23774
|
-
const collected = /* @__PURE__ */ new Set();
|
|
23775
|
-
const visit = (node) => {
|
|
23776
|
-
switch (node.type) {
|
|
23777
|
-
case "VariableDeclarator":
|
|
23778
|
-
if ("id" in node && node.id) collectPatternNames(node.id, collected);
|
|
23779
|
-
break;
|
|
23780
|
-
case "FunctionDeclaration":
|
|
23781
|
-
case "FunctionExpression":
|
|
23782
|
-
case "ClassDeclaration":
|
|
23783
|
-
case "ClassExpression":
|
|
23784
|
-
if ("id" in node && node.id && node.id.type === "Identifier") {
|
|
23785
|
-
const idNode = node.id;
|
|
23786
|
-
if (typeof idNode.name === "string") collected.add(idNode.name);
|
|
23787
|
-
}
|
|
23788
|
-
break;
|
|
23789
|
-
case "ArrowFunctionExpression": break;
|
|
23790
|
-
case "ImportDefaultSpecifier":
|
|
23791
|
-
case "ImportNamespaceSpecifier":
|
|
23792
|
-
case "ImportSpecifier":
|
|
23793
|
-
if ("local" in node && node.local && node.local.type === "Identifier") {
|
|
23794
|
-
const local = node.local;
|
|
23795
|
-
if (typeof local.name === "string") collected.add(local.name);
|
|
23796
|
-
}
|
|
23797
|
-
break;
|
|
23798
|
-
case "TSImportEqualsDeclaration":
|
|
23799
|
-
case "TSEnumDeclaration":
|
|
23800
|
-
case "TSTypeAliasDeclaration":
|
|
23801
|
-
case "TSInterfaceDeclaration":
|
|
23802
|
-
case "TSModuleDeclaration": {
|
|
23803
|
-
const idNode = node.id;
|
|
23804
|
-
if (idNode && idNode.type === "Identifier") {
|
|
23805
|
-
const idObject = idNode;
|
|
23806
|
-
if (typeof idObject.name === "string") collected.add(idObject.name);
|
|
23807
|
-
}
|
|
23808
|
-
break;
|
|
23809
|
-
}
|
|
23810
|
-
default: break;
|
|
23811
|
-
}
|
|
23812
|
-
if ("params" in node && Array.isArray(node.params)) for (const param of node.params) collectPatternNames(param, collected);
|
|
23813
|
-
if (collected.has(bindingName)) return;
|
|
23814
|
-
const nodeRecord = node;
|
|
23815
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
23816
|
-
if (key === "parent") continue;
|
|
23817
|
-
const child = nodeRecord[key];
|
|
23818
|
-
if (Array.isArray(child)) {
|
|
23819
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
23820
|
-
} else if (isAstNode(child)) visit(child);
|
|
23821
|
-
if (collected.has(bindingName)) return;
|
|
23822
|
-
}
|
|
23823
|
-
};
|
|
23824
|
-
visit(root);
|
|
23825
|
-
return collected.has(bindingName);
|
|
23826
|
-
};
|
|
23827
|
-
//#endregion
|
|
23828
25239
|
//#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
|
|
23829
|
-
const MESSAGE$
|
|
25240
|
+
const MESSAGE$6 = "`React` must be in scope when using JSX (the classic JSX transform expands `<a/>` to `React.createElement('a')`).";
|
|
23830
25241
|
const reactInJsxScope = defineRule({
|
|
23831
25242
|
id: "react-in-jsx-scope",
|
|
23832
25243
|
severity: "warn",
|
|
23833
25244
|
defaultEnabled: false,
|
|
23834
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.",
|
|
23835
|
-
create: (context) => {
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
|
|
23841
|
-
|
|
23842
|
-
|
|
23843
|
-
|
|
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
|
|
23844
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();
|
|
23845
25380
|
return {
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
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
|
+
}
|
|
23851
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);
|
|
23852
25442
|
},
|
|
23853
|
-
|
|
23854
|
-
|
|
25443
|
+
CallExpression(node) {
|
|
25444
|
+
const selectorArgument = inlineUseSelectorFunction(node, aliases);
|
|
25445
|
+
if (!selectorArgument) return;
|
|
25446
|
+
if (!isConciseBodyReturningCollection(selectorArgument)) return;
|
|
23855
25447
|
context.report({
|
|
23856
|
-
node
|
|
25448
|
+
node,
|
|
23857
25449
|
message: MESSAGE$5
|
|
23858
25450
|
});
|
|
23859
25451
|
}
|
|
@@ -24131,7 +25723,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
24131
25723
|
let found = false;
|
|
24132
25724
|
walkAst(functionBody, (child) => {
|
|
24133
25725
|
if (found) return;
|
|
24134
|
-
if (child !== functionBody && isFunctionLike$
|
|
25726
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
24135
25727
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
24136
25728
|
});
|
|
24137
25729
|
return found;
|
|
@@ -24150,7 +25742,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
24150
25742
|
let found = false;
|
|
24151
25743
|
walkAst(componentBody, (child) => {
|
|
24152
25744
|
if (found) return;
|
|
24153
|
-
if (!isFunctionLike$
|
|
25745
|
+
if (!isFunctionLike$2(child)) return;
|
|
24154
25746
|
const functionBody = child.body;
|
|
24155
25747
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
24156
25748
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24604,6 +26196,33 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
24604
26196
|
} })
|
|
24605
26197
|
});
|
|
24606
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
|
|
24607
26226
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-state-init.ts
|
|
24608
26227
|
const rerenderLazyStateInit = defineRule({
|
|
24609
26228
|
id: "rerender-lazy-state-init",
|
|
@@ -30053,7 +31672,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
30053
31672
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
30054
31673
|
let current = node.parent;
|
|
30055
31674
|
while (current) {
|
|
30056
|
-
if (isFunctionLike$
|
|
31675
|
+
if (isFunctionLike$2(current)) {
|
|
30057
31676
|
const resolvedName = inferFunctionName(current);
|
|
30058
31677
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
30059
31678
|
}
|
|
@@ -30074,7 +31693,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
30074
31693
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
30075
31694
|
let current = node.parent;
|
|
30076
31695
|
while (current) {
|
|
30077
|
-
if (isFunctionLike$
|
|
31696
|
+
if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30078
31697
|
current = current.parent ?? null;
|
|
30079
31698
|
}
|
|
30080
31699
|
return false;
|
|
@@ -30408,7 +32027,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
30408
32027
|
let foundAuthCall = false;
|
|
30409
32028
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
30410
32029
|
if (foundAuthCall) return;
|
|
30411
|
-
if (isFunctionLike$
|
|
32030
|
+
if (isFunctionLike$2(child)) return false;
|
|
30412
32031
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30413
32032
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
30414
32033
|
});
|
|
@@ -32995,6 +34614,28 @@ const reactDoctorRules = [
|
|
|
32995
34614
|
category: "Architecture"
|
|
32996
34615
|
}
|
|
32997
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
|
+
},
|
|
32998
34639
|
{
|
|
32999
34640
|
key: "react-doctor/no-danger",
|
|
33000
34641
|
id: "no-danger",
|
|
@@ -33193,6 +34834,17 @@ const reactDoctorRules = [
|
|
|
33193
34834
|
category: "State & Effects"
|
|
33194
34835
|
}
|
|
33195
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
|
+
},
|
|
33196
34848
|
{
|
|
33197
34849
|
key: "react-doctor/no-eval",
|
|
33198
34850
|
id: "no-eval",
|
|
@@ -33666,6 +35318,17 @@ const reactDoctorRules = [
|
|
|
33666
35318
|
category: "State & Effects"
|
|
33667
35319
|
}
|
|
33668
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
|
+
},
|
|
33669
35332
|
{
|
|
33670
35333
|
key: "react-doctor/no-pure-black-background",
|
|
33671
35334
|
id: "no-pure-black-background",
|
|
@@ -33677,6 +35340,17 @@ const reactDoctorRules = [
|
|
|
33677
35340
|
category: "Architecture"
|
|
33678
35341
|
}
|
|
33679
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
|
+
},
|
|
33680
35354
|
{
|
|
33681
35355
|
key: "react-doctor/no-react-children",
|
|
33682
35356
|
id: "no-react-children",
|
|
@@ -33798,6 +35472,17 @@ const reactDoctorRules = [
|
|
|
33798
35472
|
category: "Security"
|
|
33799
35473
|
}
|
|
33800
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
|
+
},
|
|
33801
35486
|
{
|
|
33802
35487
|
key: "react-doctor/no-set-state",
|
|
33803
35488
|
id: "no-set-state",
|
|
@@ -34106,6 +35791,39 @@ const reactDoctorRules = [
|
|
|
34106
35791
|
category: "Accessibility"
|
|
34107
35792
|
}
|
|
34108
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
|
+
},
|
|
34109
35827
|
{
|
|
34110
35828
|
key: "react-doctor/prefer-tag-over-role",
|
|
34111
35829
|
id: "prefer-tag-over-role",
|
|
@@ -34238,6 +35956,28 @@ const reactDoctorRules = [
|
|
|
34238
35956
|
category: "Correctness"
|
|
34239
35957
|
}
|
|
34240
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
|
+
},
|
|
34241
35981
|
{
|
|
34242
35982
|
key: "react-doctor/rendering-animate-svg-wrapper",
|
|
34243
35983
|
id: "rendering-animate-svg-wrapper",
|
|
@@ -34381,6 +36121,17 @@ const reactDoctorRules = [
|
|
|
34381
36121
|
category: "Performance"
|
|
34382
36122
|
}
|
|
34383
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
|
+
},
|
|
34384
36135
|
{
|
|
34385
36136
|
key: "react-doctor/rerender-lazy-state-init",
|
|
34386
36137
|
id: "rerender-lazy-state-init",
|
|
@@ -35205,7 +36956,7 @@ const appendNode = (builder, block, node) => {
|
|
|
35205
36956
|
};
|
|
35206
36957
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
35207
36958
|
builder.nodeBlock.set(node, block);
|
|
35208
|
-
if (isFunctionLike$
|
|
36959
|
+
if (isFunctionLike$2(node)) return;
|
|
35209
36960
|
const record = node;
|
|
35210
36961
|
for (const key of Object.keys(record)) {
|
|
35211
36962
|
if (key === "parent") continue;
|
|
@@ -35543,7 +37294,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35543
37294
|
body: program.body
|
|
35544
37295
|
});
|
|
35545
37296
|
const visit = (node) => {
|
|
35546
|
-
if (isFunctionLike$
|
|
37297
|
+
if (isFunctionLike$2(node)) {
|
|
35547
37298
|
const body = node.body;
|
|
35548
37299
|
if (body) buildFor(node, body);
|
|
35549
37300
|
}
|
|
@@ -35560,7 +37311,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35560
37311
|
const enclosingFunction = (node) => {
|
|
35561
37312
|
let current = node;
|
|
35562
37313
|
while (current) {
|
|
35563
|
-
if (isFunctionLike$
|
|
37314
|
+
if (isFunctionLike$2(current)) return current;
|
|
35564
37315
|
if (isNodeOfType(current, "Program")) return current;
|
|
35565
37316
|
current = current.parent ?? null;
|
|
35566
37317
|
}
|