oxlint-plugin-react-doctor 0.2.11-dev.d917f62 → 0.2.11-dev.e7a998a
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 +340 -0
- package/dist/index.js +1709 -394
- 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
|
}
|
|
@@ -19417,7 +20123,7 @@ const getComponentNameFromClassProperty = (node) => {
|
|
|
19417
20123
|
if (!isUppercaseName(declarator.id.name)) return null;
|
|
19418
20124
|
return declarator.id.name;
|
|
19419
20125
|
};
|
|
19420
|
-
const buildMessage$
|
|
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`;
|
|
19421
20127
|
const noPropTypes = defineRule({
|
|
19422
20128
|
id: "no-prop-types",
|
|
19423
20129
|
requires: ["react:19"],
|
|
@@ -19431,7 +20137,7 @@ const noPropTypes = defineRule({
|
|
|
19431
20137
|
if (!componentName) return;
|
|
19432
20138
|
context.report({
|
|
19433
20139
|
node: node.left,
|
|
19434
|
-
message: buildMessage$
|
|
20140
|
+
message: buildMessage$10(componentName)
|
|
19435
20141
|
});
|
|
19436
20142
|
},
|
|
19437
20143
|
PropertyDefinition(node) {
|
|
@@ -19439,7 +20145,7 @@ const noPropTypes = defineRule({
|
|
|
19439
20145
|
if (!componentName) return;
|
|
19440
20146
|
context.report({
|
|
19441
20147
|
node: node.key,
|
|
19442
|
-
message: buildMessage$
|
|
20148
|
+
message: buildMessage$10(componentName)
|
|
19443
20149
|
});
|
|
19444
20150
|
}
|
|
19445
20151
|
})
|
|
@@ -19476,8 +20182,94 @@ const noPureBlackBackground = defineRule({
|
|
|
19476
20182
|
})
|
|
19477
20183
|
});
|
|
19478
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
|
|
19479
20271
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
19480
|
-
const MESSAGE$
|
|
20272
|
+
const MESSAGE$14 = "`React.Children` is uncommon and leads to fragile components.";
|
|
19481
20273
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
19482
20274
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
19483
20275
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -19502,13 +20294,13 @@ const noReactChildren = defineRule({
|
|
|
19502
20294
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
19503
20295
|
context.report({
|
|
19504
20296
|
node: calleeOuter,
|
|
19505
|
-
message: MESSAGE$
|
|
20297
|
+
message: MESSAGE$14
|
|
19506
20298
|
});
|
|
19507
20299
|
return;
|
|
19508
20300
|
}
|
|
19509
20301
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
19510
20302
|
node: calleeOuter,
|
|
19511
|
-
message: MESSAGE$
|
|
20303
|
+
message: MESSAGE$14
|
|
19512
20304
|
});
|
|
19513
20305
|
} })
|
|
19514
20306
|
});
|
|
@@ -19704,7 +20496,7 @@ const getTagsForRole = (role) => {
|
|
|
19704
20496
|
};
|
|
19705
20497
|
//#endregion
|
|
19706
20498
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
19707
|
-
const buildMessage$
|
|
20499
|
+
const buildMessage$9 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
19708
20500
|
const resolveSettings$13 = (settings) => {
|
|
19709
20501
|
const reactDoctor = settings?.["react-doctor"];
|
|
19710
20502
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -19727,14 +20519,14 @@ const noRedundantRoles = defineRule({
|
|
|
19727
20519
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
19728
20520
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
19729
20521
|
node: roleAttr,
|
|
19730
|
-
message: buildMessage$
|
|
20522
|
+
message: buildMessage$9(tag, role)
|
|
19731
20523
|
});
|
|
19732
20524
|
} };
|
|
19733
20525
|
}
|
|
19734
20526
|
});
|
|
19735
20527
|
//#endregion
|
|
19736
20528
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
19737
|
-
const buildMessage$
|
|
20529
|
+
const buildMessage$8 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
19738
20530
|
const isPureComponentSuper = (superClass) => {
|
|
19739
20531
|
if (!superClass) return false;
|
|
19740
20532
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -19766,7 +20558,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
19766
20558
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
19767
20559
|
context.report({
|
|
19768
20560
|
node: reportNode,
|
|
19769
|
-
message: buildMessage$
|
|
20561
|
+
message: buildMessage$8(className)
|
|
19770
20562
|
});
|
|
19771
20563
|
};
|
|
19772
20564
|
return {
|
|
@@ -19825,7 +20617,7 @@ const noRenderPropChildren = defineRule({
|
|
|
19825
20617
|
});
|
|
19826
20618
|
//#endregion
|
|
19827
20619
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
19828
|
-
const MESSAGE$
|
|
20620
|
+
const MESSAGE$13 = "Do not use the return value from `ReactDOM.render`.";
|
|
19829
20621
|
const isReactDomRenderCall = (node) => {
|
|
19830
20622
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19831
20623
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -19848,7 +20640,7 @@ const noRenderReturnValue = defineRule({
|
|
|
19848
20640
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
19849
20641
|
context.report({
|
|
19850
20642
|
node: node.callee,
|
|
19851
|
-
message: MESSAGE$
|
|
20643
|
+
message: MESSAGE$13
|
|
19852
20644
|
});
|
|
19853
20645
|
} })
|
|
19854
20646
|
});
|
|
@@ -20243,7 +21035,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
20243
21035
|
const isInsideServerOnlyScope = (node) => {
|
|
20244
21036
|
let currentNode = node.parent ?? null;
|
|
20245
21037
|
while (currentNode) {
|
|
20246
|
-
if (isFunctionLike$
|
|
21038
|
+
if (isFunctionLike$2(currentNode)) {
|
|
20247
21039
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
20248
21040
|
}
|
|
20249
21041
|
currentNode = currentNode.parent ?? null;
|
|
@@ -20308,7 +21100,7 @@ const getParentComponent = (node) => {
|
|
|
20308
21100
|
};
|
|
20309
21101
|
//#endregion
|
|
20310
21102
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
20311
|
-
const MESSAGE$
|
|
21103
|
+
const MESSAGE$12 = "Do not use `this.setState` in components.";
|
|
20312
21104
|
const noSetState = defineRule({
|
|
20313
21105
|
id: "no-set-state",
|
|
20314
21106
|
severity: "warn",
|
|
@@ -20322,7 +21114,7 @@ const noSetState = defineRule({
|
|
|
20322
21114
|
if (!getParentComponent(node)) return;
|
|
20323
21115
|
context.report({
|
|
20324
21116
|
node: node.callee,
|
|
20325
|
-
message: MESSAGE$
|
|
21117
|
+
message: MESSAGE$12
|
|
20326
21118
|
});
|
|
20327
21119
|
} })
|
|
20328
21120
|
});
|
|
@@ -20481,7 +21273,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
20481
21273
|
};
|
|
20482
21274
|
//#endregion
|
|
20483
21275
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
20484
|
-
const MESSAGE$
|
|
21276
|
+
const MESSAGE$11 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
20485
21277
|
const DEFAULT_HANDLERS = [
|
|
20486
21278
|
"onClick",
|
|
20487
21279
|
"onMouseDown",
|
|
@@ -20540,7 +21332,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20540
21332
|
if (!roleAttribute || !roleAttribute.value) {
|
|
20541
21333
|
context.report({
|
|
20542
21334
|
node: node.name,
|
|
20543
|
-
message: MESSAGE$
|
|
21335
|
+
message: MESSAGE$11
|
|
20544
21336
|
});
|
|
20545
21337
|
return;
|
|
20546
21338
|
}
|
|
@@ -20550,14 +21342,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
20550
21342
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
20551
21343
|
context.report({
|
|
20552
21344
|
node: node.name,
|
|
20553
|
-
message: MESSAGE$
|
|
21345
|
+
message: MESSAGE$11
|
|
20554
21346
|
});
|
|
20555
21347
|
return;
|
|
20556
21348
|
}
|
|
20557
21349
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
20558
21350
|
context.report({
|
|
20559
21351
|
node: node.name,
|
|
20560
|
-
message: MESSAGE$
|
|
21352
|
+
message: MESSAGE$11
|
|
20561
21353
|
});
|
|
20562
21354
|
} };
|
|
20563
21355
|
}
|
|
@@ -20613,7 +21405,7 @@ const noStringRefs = defineRule({
|
|
|
20613
21405
|
});
|
|
20614
21406
|
//#endregion
|
|
20615
21407
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
20616
|
-
const MESSAGE$
|
|
21408
|
+
const MESSAGE$10 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
20617
21409
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
20618
21410
|
let ancestor = node.parent;
|
|
20619
21411
|
while (ancestor) {
|
|
@@ -20681,7 +21473,7 @@ const noThisInSfc = defineRule({
|
|
|
20681
21473
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
20682
21474
|
context.report({
|
|
20683
21475
|
node,
|
|
20684
|
-
message: MESSAGE$
|
|
21476
|
+
message: MESSAGE$10
|
|
20685
21477
|
});
|
|
20686
21478
|
} };
|
|
20687
21479
|
}
|
|
@@ -20863,7 +21655,7 @@ const ESCAPED_VERSIONS = {
|
|
|
20863
21655
|
">": "`>` / `>`",
|
|
20864
21656
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
20865
21657
|
};
|
|
20866
|
-
const buildMessage$
|
|
21658
|
+
const buildMessage$7 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
20867
21659
|
const noUnescapedEntities = defineRule({
|
|
20868
21660
|
id: "no-unescaped-entities",
|
|
20869
21661
|
severity: "warn",
|
|
@@ -20874,7 +21666,7 @@ const noUnescapedEntities = defineRule({
|
|
|
20874
21666
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
20875
21667
|
context.report({
|
|
20876
21668
|
node,
|
|
20877
|
-
message: buildMessage$
|
|
21669
|
+
message: buildMessage$7(character)
|
|
20878
21670
|
});
|
|
20879
21671
|
return;
|
|
20880
21672
|
}
|
|
@@ -21895,7 +22687,7 @@ const SAFER_REPLACEMENT = {
|
|
|
21895
22687
|
componentWillUpdate: "componentDidUpdate",
|
|
21896
22688
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
21897
22689
|
};
|
|
21898
|
-
const buildMessage$
|
|
22690
|
+
const buildMessage$6 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
21899
22691
|
const resolveSettings$9 = (settings) => {
|
|
21900
22692
|
const reactDoctor = settings?.["react-doctor"];
|
|
21901
22693
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21943,7 +22735,7 @@ const noUnsafe = defineRule({
|
|
|
21943
22735
|
if (!getParentComponent(node)) return;
|
|
21944
22736
|
context.report({
|
|
21945
22737
|
node: node.key,
|
|
21946
|
-
message: buildMessage$
|
|
22738
|
+
message: buildMessage$6(name)
|
|
21947
22739
|
});
|
|
21948
22740
|
},
|
|
21949
22741
|
Property(node) {
|
|
@@ -21954,7 +22746,7 @@ const noUnsafe = defineRule({
|
|
|
21954
22746
|
if (isEs5Component(ancestor)) {
|
|
21955
22747
|
context.report({
|
|
21956
22748
|
node: node.key,
|
|
21957
|
-
message: buildMessage$
|
|
22749
|
+
message: buildMessage$6(name)
|
|
21958
22750
|
});
|
|
21959
22751
|
return;
|
|
21960
22752
|
}
|
|
@@ -21966,7 +22758,7 @@ const noUnsafe = defineRule({
|
|
|
21966
22758
|
});
|
|
21967
22759
|
//#endregion
|
|
21968
22760
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21969
|
-
const buildMessage$
|
|
22761
|
+
const buildMessage$5 = (parentName, isInProp, allowAsProps) => {
|
|
21970
22762
|
let message = "Don't define components inside another component";
|
|
21971
22763
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21972
22764
|
message += " — extract it to module scope.";
|
|
@@ -22035,7 +22827,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
22035
22827
|
const findEnclosingComponent = (node) => {
|
|
22036
22828
|
let walker = node.parent;
|
|
22037
22829
|
while (walker) {
|
|
22038
|
-
if (isFunctionLike$
|
|
22830
|
+
if (isFunctionLike$2(walker)) {
|
|
22039
22831
|
const componentName = inferFunctionLikeName(walker);
|
|
22040
22832
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
22041
22833
|
component: walker,
|
|
@@ -22201,7 +22993,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22201
22993
|
if (!enclosing) return;
|
|
22202
22994
|
context.report({
|
|
22203
22995
|
node: reportNode,
|
|
22204
|
-
message: buildMessage$
|
|
22996
|
+
message: buildMessage$5(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
22205
22997
|
});
|
|
22206
22998
|
};
|
|
22207
22999
|
const checkFunctionLike = (node) => {
|
|
@@ -22236,15 +23028,6 @@ const noUnstableNestedComponents = defineRule({
|
|
|
22236
23028
|
}
|
|
22237
23029
|
});
|
|
22238
23030
|
//#endregion
|
|
22239
|
-
//#region src/plugin/utils/is-canonical-react-namespace-name.ts
|
|
22240
|
-
const isCanonicalReactNamespaceName = (namespaceName) => {
|
|
22241
|
-
if (namespaceName === "React") return true;
|
|
22242
|
-
if (namespaceName === "react") return true;
|
|
22243
|
-
if (namespaceName.startsWith("_react")) return true;
|
|
22244
|
-
if (namespaceName.startsWith("_React")) return true;
|
|
22245
|
-
return false;
|
|
22246
|
-
};
|
|
22247
|
-
//#endregion
|
|
22248
23031
|
//#region src/plugin/rules/performance/no-usememo-simple-expression.ts
|
|
22249
23032
|
const isSimpleExpression = (node) => {
|
|
22250
23033
|
if (!node) return false;
|
|
@@ -22333,7 +23116,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
22333
23116
|
//#endregion
|
|
22334
23117
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
22335
23118
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
22336
|
-
const MESSAGE$
|
|
23119
|
+
const MESSAGE$9 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
22337
23120
|
const resolveSettings$7 = (settings) => {
|
|
22338
23121
|
const reactDoctor = settings?.["react-doctor"];
|
|
22339
23122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -22366,7 +23149,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
22366
23149
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
22367
23150
|
context.report({
|
|
22368
23151
|
node: node.callee,
|
|
22369
|
-
message: MESSAGE$
|
|
23152
|
+
message: MESSAGE$9
|
|
22370
23153
|
});
|
|
22371
23154
|
} };
|
|
22372
23155
|
}
|
|
@@ -22979,7 +23762,7 @@ const REACT_HOOK_NAMES = new Set([
|
|
|
22979
23762
|
"useSyncExternalStore",
|
|
22980
23763
|
"useTransition"
|
|
22981
23764
|
]);
|
|
22982
|
-
const buildMessage$
|
|
23765
|
+
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.`;
|
|
22983
23766
|
const preactNoReactHooksImport = defineRule({
|
|
22984
23767
|
id: "preact-no-react-hooks-import",
|
|
22985
23768
|
requires: ["pure-preact"],
|
|
@@ -23002,7 +23785,7 @@ const preactNoReactHooksImport = defineRule({
|
|
|
23002
23785
|
});
|
|
23003
23786
|
context.report({
|
|
23004
23787
|
node,
|
|
23005
|
-
message: buildMessage$
|
|
23788
|
+
message: buildMessage$4(importedNames)
|
|
23006
23789
|
});
|
|
23007
23790
|
} })
|
|
23008
23791
|
});
|
|
@@ -23059,7 +23842,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
23059
23842
|
});
|
|
23060
23843
|
//#endregion
|
|
23061
23844
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
23062
|
-
const MESSAGE$
|
|
23845
|
+
const MESSAGE$8 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
|
|
23063
23846
|
const preactPreferOndblclick = defineRule({
|
|
23064
23847
|
id: "preact-prefer-ondblclick",
|
|
23065
23848
|
requires: ["pure-preact"],
|
|
@@ -23073,7 +23856,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
23073
23856
|
if (!onDoubleClickAttribute) return;
|
|
23074
23857
|
context.report({
|
|
23075
23858
|
node: onDoubleClickAttribute,
|
|
23076
|
-
message: MESSAGE$
|
|
23859
|
+
message: MESSAGE$8
|
|
23077
23860
|
});
|
|
23078
23861
|
} })
|
|
23079
23862
|
});
|
|
@@ -23181,7 +23964,7 @@ const preferEs6Class = defineRule({
|
|
|
23181
23964
|
});
|
|
23182
23965
|
//#endregion
|
|
23183
23966
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
23184
|
-
const MESSAGE$
|
|
23967
|
+
const MESSAGE$7 = "Class component should be written as a function component — use hooks instead.";
|
|
23185
23968
|
const resolveSettings$4 = (settings) => {
|
|
23186
23969
|
const reactDoctor = settings?.["react-doctor"];
|
|
23187
23970
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -23219,7 +24002,7 @@ const preferFunctionComponent = defineRule({
|
|
|
23219
24002
|
const reportNode = node.id ?? node;
|
|
23220
24003
|
context.report({
|
|
23221
24004
|
node: reportNode,
|
|
23222
|
-
message: MESSAGE$
|
|
24005
|
+
message: MESSAGE$7
|
|
23223
24006
|
});
|
|
23224
24007
|
};
|
|
23225
24008
|
return {
|
|
@@ -23276,6 +24059,276 @@ const preferHtmlDialog = defineRule({
|
|
|
23276
24059
|
} })
|
|
23277
24060
|
});
|
|
23278
24061
|
//#endregion
|
|
24062
|
+
//#region src/plugin/utils/function-returns-object-literal.ts
|
|
24063
|
+
const unwrapExpression = (node) => {
|
|
24064
|
+
let current = node;
|
|
24065
|
+
for (;;) {
|
|
24066
|
+
if ((current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") && "expression" in current && isAstNode(current.expression)) {
|
|
24067
|
+
current = current.expression;
|
|
24068
|
+
continue;
|
|
24069
|
+
}
|
|
24070
|
+
return current;
|
|
24071
|
+
}
|
|
24072
|
+
};
|
|
24073
|
+
const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
24074
|
+
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
24075
|
+
const body = functionNode.body;
|
|
24076
|
+
if (body && body.type !== "BlockStatement") return unwrapExpression(body).type === "ObjectExpression";
|
|
24077
|
+
}
|
|
24078
|
+
const body = functionNode.body;
|
|
24079
|
+
if (!body || body.type !== "BlockStatement") return false;
|
|
24080
|
+
let returnsObject = false;
|
|
24081
|
+
const visit = (node) => {
|
|
24082
|
+
if (returnsObject) return;
|
|
24083
|
+
if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
|
|
24084
|
+
if (unwrapExpression(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
24085
|
+
return;
|
|
24086
|
+
}
|
|
24087
|
+
const nodeRecord = node;
|
|
24088
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
24089
|
+
if (key === "parent") continue;
|
|
24090
|
+
const child = nodeRecord[key];
|
|
24091
|
+
if (Array.isArray(child)) for (const item of child) {
|
|
24092
|
+
if (!isAstNode(item)) continue;
|
|
24093
|
+
if (FUNCTION_LIKE_TYPES$1.has(item.type)) continue;
|
|
24094
|
+
visit(item);
|
|
24095
|
+
if (returnsObject) return;
|
|
24096
|
+
}
|
|
24097
|
+
else if (isAstNode(child)) {
|
|
24098
|
+
if (FUNCTION_LIKE_TYPES$1.has(child.type)) continue;
|
|
24099
|
+
visit(child);
|
|
24100
|
+
}
|
|
24101
|
+
}
|
|
24102
|
+
};
|
|
24103
|
+
visit(body);
|
|
24104
|
+
return returnsObject;
|
|
24105
|
+
};
|
|
24106
|
+
//#endregion
|
|
24107
|
+
//#region src/plugin/utils/enclosing-component-or-hook-scope.ts
|
|
24108
|
+
const enclosingComponentOrHookScope = (startNode, ownScopeFor) => {
|
|
24109
|
+
const functionNode = nearestEnclosingFunction(startNode);
|
|
24110
|
+
if (!functionNode) return null;
|
|
24111
|
+
const displayName = componentOrHookDisplayNameForFunction(functionNode);
|
|
24112
|
+
if (!displayName) return null;
|
|
24113
|
+
if (!isReactHookName(displayName) && doesFunctionReturnsObjectLiteral(functionNode)) return null;
|
|
24114
|
+
const bodyScope = ownScopeFor(functionNode);
|
|
24115
|
+
if (!bodyScope) return null;
|
|
24116
|
+
return {
|
|
24117
|
+
functionNode,
|
|
24118
|
+
bodyScope,
|
|
24119
|
+
displayName
|
|
24120
|
+
};
|
|
24121
|
+
};
|
|
24122
|
+
//#endregion
|
|
24123
|
+
//#region src/plugin/rules/architecture/prefer-module-scope-pure-function.ts
|
|
24124
|
+
const isAssignedToComponentMember = (functionNode) => {
|
|
24125
|
+
const parent = functionNode.parent;
|
|
24126
|
+
if (!parent) return false;
|
|
24127
|
+
return isNodeOfType(parent, "AssignmentExpression") && isNodeOfType(parent.left, "MemberExpression");
|
|
24128
|
+
};
|
|
24129
|
+
const hasComponentLocalCaptures = (functionNode, bodyScope, scopes) => {
|
|
24130
|
+
const captures = closureCaptures(functionNode, scopes);
|
|
24131
|
+
for (const capture of captures) {
|
|
24132
|
+
const symbol = capture.resolvedSymbol;
|
|
24133
|
+
if (!symbol) continue;
|
|
24134
|
+
if (isDescendantScope(symbol.scope, bodyScope)) return true;
|
|
24135
|
+
}
|
|
24136
|
+
return false;
|
|
24137
|
+
};
|
|
24138
|
+
const preferModuleScopePureFunction = defineRule({
|
|
24139
|
+
id: "prefer-module-scope-pure-function",
|
|
24140
|
+
tags: ["test-noise"],
|
|
24141
|
+
severity: "warn",
|
|
24142
|
+
category: "Architecture",
|
|
24143
|
+
recommendation: "Move the function to module scope (above the component). It doesn't reference any local state, so the per-render allocation is wasted.",
|
|
24144
|
+
create: (context) => {
|
|
24145
|
+
const report = (functionNode, name, componentName) => {
|
|
24146
|
+
context.report({
|
|
24147
|
+
node: functionNode,
|
|
24148
|
+
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.`
|
|
24149
|
+
});
|
|
24150
|
+
};
|
|
24151
|
+
const checkNamedFunction = (functionNode, bindingName) => {
|
|
24152
|
+
if (isAssignedToComponentMember(functionNode)) return;
|
|
24153
|
+
const component = enclosingComponentOrHookScope(functionNode, context.scopes.ownScopeFor);
|
|
24154
|
+
if (!component) return;
|
|
24155
|
+
const ownScope = context.scopes.ownScopeFor(functionNode);
|
|
24156
|
+
if (!ownScope) return;
|
|
24157
|
+
if (ownScope === component.bodyScope) return;
|
|
24158
|
+
if (!isDescendantScope(ownScope, component.bodyScope)) return;
|
|
24159
|
+
if (hasComponentLocalCaptures(functionNode, component.bodyScope, context.scopes)) return;
|
|
24160
|
+
report(functionNode, bindingName, component.displayName);
|
|
24161
|
+
};
|
|
24162
|
+
return {
|
|
24163
|
+
VariableDeclarator(node) {
|
|
24164
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
24165
|
+
const initializer = node.init;
|
|
24166
|
+
if (!initializer) return;
|
|
24167
|
+
if (!isNodeOfType(initializer, "ArrowFunctionExpression") && !isNodeOfType(initializer, "FunctionExpression")) return;
|
|
24168
|
+
const bindingName = node.id.name;
|
|
24169
|
+
if (/^[A-Z]/.test(bindingName)) return;
|
|
24170
|
+
checkNamedFunction(initializer, bindingName);
|
|
24171
|
+
},
|
|
24172
|
+
FunctionDeclaration(node) {
|
|
24173
|
+
if (!node.id?.name) return;
|
|
24174
|
+
const bindingName = node.id.name;
|
|
24175
|
+
if (/^[A-Z]/.test(bindingName)) return;
|
|
24176
|
+
checkNamedFunction(node, bindingName);
|
|
24177
|
+
}
|
|
24178
|
+
};
|
|
24179
|
+
}
|
|
24180
|
+
});
|
|
24181
|
+
//#endregion
|
|
24182
|
+
//#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
|
|
24183
|
+
const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
24184
|
+
const isMutationContext = (referenceIdentifier) => {
|
|
24185
|
+
const parent = referenceIdentifier.parent;
|
|
24186
|
+
if (!parent) return false;
|
|
24187
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceIdentifier) return true;
|
|
24188
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceIdentifier) return true;
|
|
24189
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === referenceIdentifier) {
|
|
24190
|
+
const grandparent = parent.parent;
|
|
24191
|
+
if (!grandparent) return false;
|
|
24192
|
+
if (isNodeOfType(grandparent, "AssignmentExpression") && grandparent.left === parent) return true;
|
|
24193
|
+
if (isNodeOfType(grandparent, "UpdateExpression") && grandparent.argument === parent) return true;
|
|
24194
|
+
if (isNodeOfType(grandparent, "UnaryExpression") && grandparent.operator === "delete" && grandparent.argument === parent) return true;
|
|
24195
|
+
if (isNodeOfType(grandparent, "CallExpression") && grandparent.callee === parent && !parent.computed && isNodeOfType(parent.property, "Identifier") && MUTATING_RECEIVER_METHOD_NAMES.has(parent.property.name)) return true;
|
|
24196
|
+
}
|
|
24197
|
+
return false;
|
|
24198
|
+
};
|
|
24199
|
+
const isBindingMutatedAfterInit = (declaratorNode, bodyScope, scopes) => {
|
|
24200
|
+
if (!isNodeOfType(declaratorNode.id, "Identifier")) return false;
|
|
24201
|
+
const symbol = scopes.symbolFor(declaratorNode.id);
|
|
24202
|
+
if (!symbol) return false;
|
|
24203
|
+
for (const reference of symbol.references) {
|
|
24204
|
+
if (reference.identifier === declaratorNode.id) continue;
|
|
24205
|
+
if (reference.identifier === declaratorNode.init) continue;
|
|
24206
|
+
if (!isDescendantScope(reference.scope, bodyScope) && reference.scope !== bodyScope) continue;
|
|
24207
|
+
if (isMutationContext(reference.identifier)) return true;
|
|
24208
|
+
}
|
|
24209
|
+
return false;
|
|
24210
|
+
};
|
|
24211
|
+
const hasComponentLocalReferences = (expression, bodyScope, scopes) => {
|
|
24212
|
+
let foundLocal = false;
|
|
24213
|
+
walkAst(expression, (node) => {
|
|
24214
|
+
if (foundLocal) return false;
|
|
24215
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) {
|
|
24216
|
+
foundLocal = true;
|
|
24217
|
+
return false;
|
|
24218
|
+
}
|
|
24219
|
+
const reference = scopes.referenceFor(node);
|
|
24220
|
+
if (reference?.resolvedSymbol && isDescendantScope(reference.resolvedSymbol.scope, bodyScope)) {
|
|
24221
|
+
foundLocal = true;
|
|
24222
|
+
return false;
|
|
24223
|
+
}
|
|
24224
|
+
});
|
|
24225
|
+
return foundLocal;
|
|
24226
|
+
};
|
|
24227
|
+
const isHoistableValueExpression = (expression) => {
|
|
24228
|
+
const stripped = stripParenExpression(expression);
|
|
24229
|
+
return isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ObjectExpression");
|
|
24230
|
+
};
|
|
24231
|
+
const preferModuleScopeStaticValue = defineRule({
|
|
24232
|
+
id: "prefer-module-scope-static-value",
|
|
24233
|
+
tags: ["test-noise"],
|
|
24234
|
+
severity: "warn",
|
|
24235
|
+
category: "Architecture",
|
|
24236
|
+
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.",
|
|
24237
|
+
create: (context) => ({ VariableDeclarator(node) {
|
|
24238
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
24239
|
+
const initializer = node.init;
|
|
24240
|
+
if (!initializer) return;
|
|
24241
|
+
if (!isHoistableValueExpression(initializer)) return;
|
|
24242
|
+
const component = enclosingComponentOrHookScope(node, context.scopes.ownScopeFor);
|
|
24243
|
+
if (!component) return;
|
|
24244
|
+
if (hasComponentLocalReferences(initializer, component.bodyScope, context.scopes)) return;
|
|
24245
|
+
if (isBindingMutatedAfterInit(node, component.bodyScope, context.scopes)) return;
|
|
24246
|
+
const bindingName = node.id.name;
|
|
24247
|
+
context.report({
|
|
24248
|
+
node,
|
|
24249
|
+
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.`
|
|
24250
|
+
});
|
|
24251
|
+
} })
|
|
24252
|
+
});
|
|
24253
|
+
//#endregion
|
|
24254
|
+
//#region src/plugin/rules/performance/prefer-stable-empty-fallback.ts
|
|
24255
|
+
const isEmptyArrayLiteral$1 = (expression) => {
|
|
24256
|
+
const stripped = stripParenExpression(expression);
|
|
24257
|
+
return isNodeOfType(stripped, "ArrayExpression") && (stripped.elements ?? []).length === 0;
|
|
24258
|
+
};
|
|
24259
|
+
const isEmptyObjectLiteral = (expression) => {
|
|
24260
|
+
const stripped = stripParenExpression(expression);
|
|
24261
|
+
return isNodeOfType(stripped, "ObjectExpression") && (stripped.properties ?? []).length === 0;
|
|
24262
|
+
};
|
|
24263
|
+
const isStableNonEmptyExpression = (expression) => {
|
|
24264
|
+
const stripped = stripParenExpression(expression);
|
|
24265
|
+
if (isNodeOfType(stripped, "Identifier")) return true;
|
|
24266
|
+
if (isNodeOfType(stripped, "ThisExpression")) return true;
|
|
24267
|
+
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
24268
|
+
if (stripped.computed) return false;
|
|
24269
|
+
const object = stripped.object;
|
|
24270
|
+
if (!object) return false;
|
|
24271
|
+
return isStableNonEmptyExpression(object);
|
|
24272
|
+
}
|
|
24273
|
+
return false;
|
|
24274
|
+
};
|
|
24275
|
+
const matchEmptyFallbackInLogicalExpression = (expression) => {
|
|
24276
|
+
const stripped = stripParenExpression(expression);
|
|
24277
|
+
if (!isNodeOfType(stripped, "LogicalExpression")) return null;
|
|
24278
|
+
if (stripped.operator !== "||" && stripped.operator !== "??") return null;
|
|
24279
|
+
const left = stripped.left;
|
|
24280
|
+
const right = stripped.right;
|
|
24281
|
+
if (!left || !right) return null;
|
|
24282
|
+
if (isEmptyArrayLiteral$1(right) && isStableNonEmptyExpression(left)) return {
|
|
24283
|
+
emptyKind: "array",
|
|
24284
|
+
emptyNode: right,
|
|
24285
|
+
nonEmptyExpression: left
|
|
24286
|
+
};
|
|
24287
|
+
if (isEmptyObjectLiteral(right) && isStableNonEmptyExpression(left)) return {
|
|
24288
|
+
emptyKind: "object",
|
|
24289
|
+
emptyNode: right,
|
|
24290
|
+
nonEmptyExpression: left
|
|
24291
|
+
};
|
|
24292
|
+
return null;
|
|
24293
|
+
};
|
|
24294
|
+
const buildMessage$3 = (emptyKind) => {
|
|
24295
|
+
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.`;
|
|
24296
|
+
};
|
|
24297
|
+
const preferStableEmptyFallback = defineRule({
|
|
24298
|
+
id: "prefer-stable-empty-fallback",
|
|
24299
|
+
tags: ["react-jsx-only", "test-noise"],
|
|
24300
|
+
severity: "warn",
|
|
24301
|
+
category: "Performance",
|
|
24302
|
+
disabledBy: ["react-compiler"],
|
|
24303
|
+
recommendation: "Hoist a module-level `const EMPTY = []` (or `{}`) and use that as the `||` / `??` fallback so the consumer sees a stable reference.",
|
|
24304
|
+
create: (context) => {
|
|
24305
|
+
let memoRegistry = null;
|
|
24306
|
+
return {
|
|
24307
|
+
Program(node) {
|
|
24308
|
+
memoRegistry = buildSameFileMemoRegistry(node);
|
|
24309
|
+
},
|
|
24310
|
+
JSXAttribute(node) {
|
|
24311
|
+
if (!isInsideFunctionScope(node)) return;
|
|
24312
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
24313
|
+
if (!node.value) return;
|
|
24314
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24315
|
+
const innerExpression = node.value.expression;
|
|
24316
|
+
if (!innerExpression) return;
|
|
24317
|
+
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
24318
|
+
const parentJsxOpening = node.parent;
|
|
24319
|
+
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
24320
|
+
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
24321
|
+
const fallback = matchEmptyFallbackInLogicalExpression(innerExpression);
|
|
24322
|
+
if (!fallback) return;
|
|
24323
|
+
context.report({
|
|
24324
|
+
node: fallback.emptyNode,
|
|
24325
|
+
message: buildMessage$3(fallback.emptyKind)
|
|
24326
|
+
});
|
|
24327
|
+
}
|
|
24328
|
+
};
|
|
24329
|
+
}
|
|
24330
|
+
});
|
|
24331
|
+
//#endregion
|
|
23279
24332
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
23280
24333
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
23281
24334
|
const preferTagOverRole = defineRule({
|
|
@@ -23826,91 +24879,216 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
23826
24879
|
} })
|
|
23827
24880
|
});
|
|
23828
24881
|
//#endregion
|
|
23829
|
-
//#region src/plugin/utils/has-binding-named.ts
|
|
23830
|
-
const hasBindingNamed = (root, bindingName) => {
|
|
23831
|
-
const collected = /* @__PURE__ */ new Set();
|
|
23832
|
-
const visit = (node) => {
|
|
23833
|
-
switch (node.type) {
|
|
23834
|
-
case "VariableDeclarator":
|
|
23835
|
-
if ("id" in node && node.id) collectPatternNames(node.id, collected);
|
|
23836
|
-
break;
|
|
23837
|
-
case "FunctionDeclaration":
|
|
23838
|
-
case "FunctionExpression":
|
|
23839
|
-
case "ClassDeclaration":
|
|
23840
|
-
case "ClassExpression":
|
|
23841
|
-
if ("id" in node && node.id && node.id.type === "Identifier") {
|
|
23842
|
-
const idNode = node.id;
|
|
23843
|
-
if (typeof idNode.name === "string") collected.add(idNode.name);
|
|
23844
|
-
}
|
|
23845
|
-
break;
|
|
23846
|
-
case "ArrowFunctionExpression": break;
|
|
23847
|
-
case "ImportDefaultSpecifier":
|
|
23848
|
-
case "ImportNamespaceSpecifier":
|
|
23849
|
-
case "ImportSpecifier":
|
|
23850
|
-
if ("local" in node && node.local && node.local.type === "Identifier") {
|
|
23851
|
-
const local = node.local;
|
|
23852
|
-
if (typeof local.name === "string") collected.add(local.name);
|
|
23853
|
-
}
|
|
23854
|
-
break;
|
|
23855
|
-
case "TSImportEqualsDeclaration":
|
|
23856
|
-
case "TSEnumDeclaration":
|
|
23857
|
-
case "TSTypeAliasDeclaration":
|
|
23858
|
-
case "TSInterfaceDeclaration":
|
|
23859
|
-
case "TSModuleDeclaration": {
|
|
23860
|
-
const idNode = node.id;
|
|
23861
|
-
if (idNode && idNode.type === "Identifier") {
|
|
23862
|
-
const idObject = idNode;
|
|
23863
|
-
if (typeof idObject.name === "string") collected.add(idObject.name);
|
|
23864
|
-
}
|
|
23865
|
-
break;
|
|
23866
|
-
}
|
|
23867
|
-
default: break;
|
|
23868
|
-
}
|
|
23869
|
-
if ("params" in node && Array.isArray(node.params)) for (const param of node.params) collectPatternNames(param, collected);
|
|
23870
|
-
if (collected.has(bindingName)) return;
|
|
23871
|
-
const nodeRecord = node;
|
|
23872
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
23873
|
-
if (key === "parent") continue;
|
|
23874
|
-
const child = nodeRecord[key];
|
|
23875
|
-
if (Array.isArray(child)) {
|
|
23876
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
23877
|
-
} else if (isAstNode(child)) visit(child);
|
|
23878
|
-
if (collected.has(bindingName)) return;
|
|
23879
|
-
}
|
|
23880
|
-
};
|
|
23881
|
-
visit(root);
|
|
23882
|
-
return collected.has(bindingName);
|
|
23883
|
-
};
|
|
23884
|
-
//#endregion
|
|
23885
24882
|
//#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
|
|
23886
|
-
const MESSAGE$
|
|
24883
|
+
const MESSAGE$6 = "`React` must be in scope when using JSX (the classic JSX transform expands `<a/>` to `React.createElement('a')`).";
|
|
23887
24884
|
const reactInJsxScope = defineRule({
|
|
23888
24885
|
id: "react-in-jsx-scope",
|
|
23889
24886
|
severity: "warn",
|
|
23890
24887
|
defaultEnabled: false,
|
|
23891
24888
|
recommendation: "If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import `React` at the top of the file.",
|
|
23892
|
-
create: (context) => {
|
|
23893
|
-
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
24889
|
+
create: (context) => ({
|
|
24890
|
+
JSXOpeningElement(node) {
|
|
24891
|
+
if (findVariableInitializer(node, "React")) return;
|
|
24892
|
+
context.report({
|
|
24893
|
+
node: node.name,
|
|
24894
|
+
message: MESSAGE$6
|
|
24895
|
+
});
|
|
24896
|
+
},
|
|
24897
|
+
JSXFragment(node) {
|
|
24898
|
+
if (findVariableInitializer(node, "React")) return;
|
|
24899
|
+
context.report({
|
|
24900
|
+
node: node.openingFragment,
|
|
24901
|
+
message: MESSAGE$6
|
|
24902
|
+
});
|
|
24903
|
+
}
|
|
24904
|
+
})
|
|
24905
|
+
});
|
|
24906
|
+
//#endregion
|
|
24907
|
+
//#region src/plugin/utils/collect-react-redux-selector-aliases.ts
|
|
24908
|
+
const REACT_REDUX_MODULE = "react-redux";
|
|
24909
|
+
const collectReactReduxSelectorAliases = (programRoot) => {
|
|
24910
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
24911
|
+
if (!isNodeOfType(programRoot, "Program")) return aliases;
|
|
24912
|
+
for (const topLevel of programRoot.body ?? []) {
|
|
24913
|
+
if (!isNodeOfType(topLevel, "ImportDeclaration")) continue;
|
|
24914
|
+
if (typeof topLevel.source?.value !== "string") continue;
|
|
24915
|
+
if (topLevel.source.value !== REACT_REDUX_MODULE) continue;
|
|
24916
|
+
for (const specifier of topLevel.specifiers ?? []) {
|
|
24917
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
24918
|
+
const imported = specifier.imported;
|
|
24919
|
+
if ((imported && "name" in imported && typeof imported.name === "string" ? imported.name : imported && "value" in imported && typeof imported.value === "string" ? imported.value : null) !== "useSelector") continue;
|
|
24920
|
+
const local = specifier.local;
|
|
24921
|
+
if (isNodeOfType(local, "Identifier")) aliases.add(local.name);
|
|
24922
|
+
}
|
|
24923
|
+
}
|
|
24924
|
+
const collectDeclarations = (node) => {
|
|
24925
|
+
if (!isNodeOfType(node, "VariableDeclaration")) return;
|
|
24926
|
+
for (const declarator of node.declarations ?? []) {
|
|
24927
|
+
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
24928
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
24929
|
+
if (!declarator.init) continue;
|
|
24930
|
+
const initialiser = stripParenExpression(declarator.init);
|
|
24931
|
+
if (!isNodeOfType(initialiser, "Identifier")) continue;
|
|
24932
|
+
if (!aliases.has(initialiser.name)) continue;
|
|
24933
|
+
aliases.add(declarator.id.name);
|
|
24934
|
+
}
|
|
24935
|
+
};
|
|
24936
|
+
for (const topLevel of programRoot.body ?? []) if (isNodeOfType(topLevel, "VariableDeclaration")) collectDeclarations(topLevel);
|
|
24937
|
+
else if (isNodeOfType(topLevel, "ExportNamedDeclaration") && topLevel.declaration) collectDeclarations(topLevel.declaration);
|
|
24938
|
+
return aliases;
|
|
24939
|
+
};
|
|
24940
|
+
const isUseSelectorIdentifier = (calleeNode, aliases) => {
|
|
24941
|
+
if (!isNodeOfType(calleeNode, "Identifier")) return false;
|
|
24942
|
+
if (aliases.has(calleeNode.name)) return true;
|
|
24943
|
+
if (calleeNode.name !== "useSelector") return false;
|
|
24944
|
+
return isImportedFromModule(calleeNode, calleeNode.name, REACT_REDUX_MODULE);
|
|
24945
|
+
};
|
|
24946
|
+
//#endregion
|
|
24947
|
+
//#region src/plugin/rules/state-and-effects/utils/inline-use-selector-function.ts
|
|
24948
|
+
const inlineUseSelectorFunction = (callNode, aliases) => {
|
|
24949
|
+
if (!isUseSelectorIdentifier(callNode.callee, aliases)) return null;
|
|
24950
|
+
const args = callNode.arguments ?? [];
|
|
24951
|
+
if (args.length === 0 || args.length >= 2) return null;
|
|
24952
|
+
const selectorArgument = stripParenExpression(args[0]);
|
|
24953
|
+
if (isNodeOfType(selectorArgument, "ArrowFunctionExpression") || isNodeOfType(selectorArgument, "FunctionExpression")) return selectorArgument;
|
|
24954
|
+
return null;
|
|
24955
|
+
};
|
|
24956
|
+
//#endregion
|
|
24957
|
+
//#region src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts
|
|
24958
|
+
const ALLOCATING_ARRAY_METHODS = new Set([
|
|
24959
|
+
"filter",
|
|
24960
|
+
"map",
|
|
24961
|
+
"flatMap",
|
|
24962
|
+
"slice",
|
|
24963
|
+
"concat",
|
|
24964
|
+
"toSorted",
|
|
24965
|
+
"toReversed",
|
|
24966
|
+
"toSpliced",
|
|
24967
|
+
"with"
|
|
24968
|
+
]);
|
|
24969
|
+
const ALLOCATING_NAMESPACE_CALLS = new Map([["Object", new Set([
|
|
24970
|
+
"keys",
|
|
24971
|
+
"values",
|
|
24972
|
+
"entries",
|
|
24973
|
+
"fromEntries",
|
|
24974
|
+
"assign"
|
|
24975
|
+
])], ["Array", new Set(["from", "of"])]]);
|
|
24976
|
+
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\`.`;
|
|
24977
|
+
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\`.`;
|
|
24978
|
+
const getAllocatingCallSiteDescription = (expression) => {
|
|
24979
|
+
const stripped = stripParenExpression(expression);
|
|
24980
|
+
if (!isNodeOfType(stripped, "CallExpression")) return null;
|
|
24981
|
+
const callee = stripped.callee;
|
|
24982
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
24983
|
+
if (callee.computed) return null;
|
|
24984
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
24985
|
+
const methodName = callee.property.name;
|
|
24986
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
24987
|
+
const namespaceName = callee.object.name;
|
|
24988
|
+
if (ALLOCATING_NAMESPACE_CALLS.get(namespaceName)?.has(methodName)) return {
|
|
24989
|
+
kind: "namespace",
|
|
24990
|
+
namespace: namespaceName,
|
|
24991
|
+
method: methodName
|
|
23901
24992
|
};
|
|
24993
|
+
}
|
|
24994
|
+
if (ALLOCATING_ARRAY_METHODS.has(methodName)) return {
|
|
24995
|
+
kind: "method",
|
|
24996
|
+
method: methodName
|
|
24997
|
+
};
|
|
24998
|
+
return null;
|
|
24999
|
+
};
|
|
25000
|
+
const findReturnedAllocatingCall = (expression) => {
|
|
25001
|
+
const stripped = stripParenExpression(expression);
|
|
25002
|
+
const direct = getAllocatingCallSiteDescription(stripped);
|
|
25003
|
+
if (direct) return {
|
|
25004
|
+
...direct,
|
|
25005
|
+
node: stripped
|
|
25006
|
+
};
|
|
25007
|
+
if (isNodeOfType(stripped, "ConditionalExpression")) return findReturnedAllocatingCall(stripped.consequent) ?? findReturnedAllocatingCall(stripped.alternate);
|
|
25008
|
+
if (isNodeOfType(stripped, "LogicalExpression")) return findReturnedAllocatingCall(stripped.left) ?? findReturnedAllocatingCall(stripped.right);
|
|
25009
|
+
if (isNodeOfType(stripped, "SequenceExpression")) {
|
|
25010
|
+
const lastExpression = stripped.expressions[stripped.expressions.length - 1];
|
|
25011
|
+
return lastExpression ? findReturnedAllocatingCall(lastExpression) : null;
|
|
25012
|
+
}
|
|
25013
|
+
return null;
|
|
25014
|
+
};
|
|
25015
|
+
const reduxUseselectorInlineDerivation = defineRule({
|
|
25016
|
+
id: "redux-useselector-inline-derivation",
|
|
25017
|
+
severity: "warn",
|
|
25018
|
+
category: "Performance",
|
|
25019
|
+
disabledBy: ["react-compiler"],
|
|
25020
|
+
recommendation: "Select the raw slice and derive with `useMemo`, or use `createSelector` from `reselect`.",
|
|
25021
|
+
create: (context) => {
|
|
25022
|
+
let aliases = /* @__PURE__ */ new Set();
|
|
23902
25023
|
return {
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
25024
|
+
Program(node) {
|
|
25025
|
+
aliases = collectReactReduxSelectorAliases(node);
|
|
25026
|
+
},
|
|
25027
|
+
CallExpression(node) {
|
|
25028
|
+
const selectorArgument = inlineUseSelectorFunction(node, aliases);
|
|
25029
|
+
if (!selectorArgument) return;
|
|
25030
|
+
const body = selectorArgument.body;
|
|
25031
|
+
if (!body) return;
|
|
25032
|
+
const returnedExpressions = [];
|
|
25033
|
+
if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
|
|
25034
|
+
if (node !== body && isFunctionLike$2(node)) return false;
|
|
25035
|
+
if (isNodeOfType(node, "ReturnStatement")) {
|
|
25036
|
+
if (node.argument) returnedExpressions.push(node.argument);
|
|
25037
|
+
return false;
|
|
25038
|
+
}
|
|
23908
25039
|
});
|
|
25040
|
+
else returnedExpressions.push(body);
|
|
25041
|
+
for (const returnedExpression of returnedExpressions) {
|
|
25042
|
+
const allocatingCall = findReturnedAllocatingCall(returnedExpression);
|
|
25043
|
+
if (!allocatingCall) continue;
|
|
25044
|
+
const reportMessage = allocatingCall.kind === "method" ? MESSAGE_DERIVATION(allocatingCall.method) : MESSAGE_NAMESPACE(allocatingCall.namespace, allocatingCall.method);
|
|
25045
|
+
context.report({
|
|
25046
|
+
node: allocatingCall.node,
|
|
25047
|
+
message: reportMessage
|
|
25048
|
+
});
|
|
25049
|
+
return;
|
|
25050
|
+
}
|
|
25051
|
+
}
|
|
25052
|
+
};
|
|
25053
|
+
}
|
|
25054
|
+
});
|
|
25055
|
+
//#endregion
|
|
25056
|
+
//#region src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts
|
|
25057
|
+
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.";
|
|
25058
|
+
const isConciseBodyReturningCollection = (functionNode) => {
|
|
25059
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
25060
|
+
const rawBody = functionNode.body;
|
|
25061
|
+
if (!rawBody) return false;
|
|
25062
|
+
if (!isNodeOfType(rawBody, "BlockStatement")) {
|
|
25063
|
+
const conciseExpression = stripParenExpression(rawBody);
|
|
25064
|
+
return isNodeOfType(conciseExpression, "ObjectExpression") || isNodeOfType(conciseExpression, "ArrayExpression");
|
|
25065
|
+
}
|
|
25066
|
+
const statements = rawBody.body ?? [];
|
|
25067
|
+
if (statements.length === 0) return false;
|
|
25068
|
+
const lastStatement = statements[statements.length - 1];
|
|
25069
|
+
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
25070
|
+
if (!lastStatement.argument) return false;
|
|
25071
|
+
const returnedExpression = stripParenExpression(lastStatement.argument);
|
|
25072
|
+
return isNodeOfType(returnedExpression, "ObjectExpression") || isNodeOfType(returnedExpression, "ArrayExpression");
|
|
25073
|
+
};
|
|
25074
|
+
const reduxUseselectorReturnsNewCollection = defineRule({
|
|
25075
|
+
id: "redux-useselector-returns-new-collection",
|
|
25076
|
+
severity: "warn",
|
|
25077
|
+
category: "Performance",
|
|
25078
|
+
disabledBy: ["react-compiler"],
|
|
25079
|
+
recommendation: "Return a primitive, split into multiple useSelector calls, or pass `shallowEqual` from `react-redux` as the second argument.",
|
|
25080
|
+
create: (context) => {
|
|
25081
|
+
let aliases = /* @__PURE__ */ new Set();
|
|
25082
|
+
return {
|
|
25083
|
+
Program(node) {
|
|
25084
|
+
aliases = collectReactReduxSelectorAliases(node);
|
|
23909
25085
|
},
|
|
23910
|
-
|
|
23911
|
-
|
|
25086
|
+
CallExpression(node) {
|
|
25087
|
+
const selectorArgument = inlineUseSelectorFunction(node, aliases);
|
|
25088
|
+
if (!selectorArgument) return;
|
|
25089
|
+
if (!isConciseBodyReturningCollection(selectorArgument)) return;
|
|
23912
25090
|
context.report({
|
|
23913
|
-
node
|
|
25091
|
+
node,
|
|
23914
25092
|
message: MESSAGE$5
|
|
23915
25093
|
});
|
|
23916
25094
|
}
|
|
@@ -24188,7 +25366,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
24188
25366
|
let found = false;
|
|
24189
25367
|
walkAst(functionBody, (child) => {
|
|
24190
25368
|
if (found) return;
|
|
24191
|
-
if (child !== functionBody && isFunctionLike$
|
|
25369
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
24192
25370
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
24193
25371
|
});
|
|
24194
25372
|
return found;
|
|
@@ -24207,7 +25385,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
24207
25385
|
let found = false;
|
|
24208
25386
|
walkAst(componentBody, (child) => {
|
|
24209
25387
|
if (found) return;
|
|
24210
|
-
if (!isFunctionLike$
|
|
25388
|
+
if (!isFunctionLike$2(child)) return;
|
|
24211
25389
|
const functionBody = child.body;
|
|
24212
25390
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
24213
25391
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24661,6 +25839,33 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
24661
25839
|
} })
|
|
24662
25840
|
});
|
|
24663
25841
|
//#endregion
|
|
25842
|
+
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
25843
|
+
const rerenderLazyRefInit = defineRule({
|
|
25844
|
+
id: "rerender-lazy-ref-init",
|
|
25845
|
+
tags: ["test-noise"],
|
|
25846
|
+
severity: "warn",
|
|
25847
|
+
category: "Performance",
|
|
25848
|
+
recommendation: "Initialize lazily: `const ref = useRef<T | null>(null); if (ref.current === null) ref.current = expensiveCall();`",
|
|
25849
|
+
create: (context) => ({ CallExpression(node) {
|
|
25850
|
+
if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
|
|
25851
|
+
const initializer = node.arguments[0];
|
|
25852
|
+
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
25853
|
+
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
25854
|
+
if (!isPlainCall && !isNewCall) return;
|
|
25855
|
+
const callee = initializer.callee;
|
|
25856
|
+
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
25857
|
+
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
25858
|
+
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
25859
|
+
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
25860
|
+
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
25861
|
+
const lazyFix = isNewCall ? `ref.current = new ${calleeName}();` : `ref.current = ${calleeName}();`;
|
|
25862
|
+
context.report({
|
|
25863
|
+
node: initializer,
|
|
25864
|
+
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.`
|
|
25865
|
+
});
|
|
25866
|
+
} })
|
|
25867
|
+
});
|
|
25868
|
+
//#endregion
|
|
24664
25869
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-state-init.ts
|
|
24665
25870
|
const rerenderLazyStateInit = defineRule({
|
|
24666
25871
|
id: "rerender-lazy-state-init",
|
|
@@ -30110,7 +31315,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
30110
31315
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
30111
31316
|
let current = node.parent;
|
|
30112
31317
|
while (current) {
|
|
30113
|
-
if (isFunctionLike$
|
|
31318
|
+
if (isFunctionLike$2(current)) {
|
|
30114
31319
|
const resolvedName = inferFunctionName(current);
|
|
30115
31320
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
30116
31321
|
}
|
|
@@ -30131,7 +31336,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
30131
31336
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
30132
31337
|
let current = node.parent;
|
|
30133
31338
|
while (current) {
|
|
30134
|
-
if (isFunctionLike$
|
|
31339
|
+
if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30135
31340
|
current = current.parent ?? null;
|
|
30136
31341
|
}
|
|
30137
31342
|
return false;
|
|
@@ -30465,7 +31670,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
30465
31670
|
let foundAuthCall = false;
|
|
30466
31671
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
30467
31672
|
if (foundAuthCall) return;
|
|
30468
|
-
if (isFunctionLike$
|
|
31673
|
+
if (isFunctionLike$2(child)) return false;
|
|
30469
31674
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30470
31675
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
30471
31676
|
});
|
|
@@ -33052,6 +34257,28 @@ const reactDoctorRules = [
|
|
|
33052
34257
|
category: "Architecture"
|
|
33053
34258
|
}
|
|
33054
34259
|
},
|
|
34260
|
+
{
|
|
34261
|
+
key: "react-doctor/no-create-context-in-render",
|
|
34262
|
+
id: "no-create-context-in-render",
|
|
34263
|
+
source: "react-doctor",
|
|
34264
|
+
originallyExternal: false,
|
|
34265
|
+
rule: {
|
|
34266
|
+
...noCreateContextInRender,
|
|
34267
|
+
framework: "global",
|
|
34268
|
+
category: "Correctness"
|
|
34269
|
+
}
|
|
34270
|
+
},
|
|
34271
|
+
{
|
|
34272
|
+
key: "react-doctor/no-create-store-in-render",
|
|
34273
|
+
id: "no-create-store-in-render",
|
|
34274
|
+
source: "react-doctor",
|
|
34275
|
+
originallyExternal: false,
|
|
34276
|
+
rule: {
|
|
34277
|
+
...noCreateStoreInRender,
|
|
34278
|
+
framework: "global",
|
|
34279
|
+
category: "Correctness"
|
|
34280
|
+
}
|
|
34281
|
+
},
|
|
33055
34282
|
{
|
|
33056
34283
|
key: "react-doctor/no-danger",
|
|
33057
34284
|
id: "no-danger",
|
|
@@ -33250,6 +34477,17 @@ const reactDoctorRules = [
|
|
|
33250
34477
|
category: "State & Effects"
|
|
33251
34478
|
}
|
|
33252
34479
|
},
|
|
34480
|
+
{
|
|
34481
|
+
key: "react-doctor/no-effect-with-fresh-deps",
|
|
34482
|
+
id: "no-effect-with-fresh-deps",
|
|
34483
|
+
source: "react-doctor",
|
|
34484
|
+
originallyExternal: false,
|
|
34485
|
+
rule: {
|
|
34486
|
+
...noEffectWithFreshDeps,
|
|
34487
|
+
framework: "global",
|
|
34488
|
+
category: "State & Effects"
|
|
34489
|
+
}
|
|
34490
|
+
},
|
|
33253
34491
|
{
|
|
33254
34492
|
key: "react-doctor/no-eval",
|
|
33255
34493
|
id: "no-eval",
|
|
@@ -33745,6 +34983,17 @@ const reactDoctorRules = [
|
|
|
33745
34983
|
category: "Architecture"
|
|
33746
34984
|
}
|
|
33747
34985
|
},
|
|
34986
|
+
{
|
|
34987
|
+
key: "react-doctor/no-random-key",
|
|
34988
|
+
id: "no-random-key",
|
|
34989
|
+
source: "react-doctor",
|
|
34990
|
+
originallyExternal: false,
|
|
34991
|
+
rule: {
|
|
34992
|
+
...noRandomKey,
|
|
34993
|
+
framework: "global",
|
|
34994
|
+
category: "Correctness"
|
|
34995
|
+
}
|
|
34996
|
+
},
|
|
33748
34997
|
{
|
|
33749
34998
|
key: "react-doctor/no-react-children",
|
|
33750
34999
|
id: "no-react-children",
|
|
@@ -34174,6 +35423,39 @@ const reactDoctorRules = [
|
|
|
34174
35423
|
category: "Accessibility"
|
|
34175
35424
|
}
|
|
34176
35425
|
},
|
|
35426
|
+
{
|
|
35427
|
+
key: "react-doctor/prefer-module-scope-pure-function",
|
|
35428
|
+
id: "prefer-module-scope-pure-function",
|
|
35429
|
+
source: "react-doctor",
|
|
35430
|
+
originallyExternal: false,
|
|
35431
|
+
rule: {
|
|
35432
|
+
...preferModuleScopePureFunction,
|
|
35433
|
+
framework: "global",
|
|
35434
|
+
category: "Architecture"
|
|
35435
|
+
}
|
|
35436
|
+
},
|
|
35437
|
+
{
|
|
35438
|
+
key: "react-doctor/prefer-module-scope-static-value",
|
|
35439
|
+
id: "prefer-module-scope-static-value",
|
|
35440
|
+
source: "react-doctor",
|
|
35441
|
+
originallyExternal: false,
|
|
35442
|
+
rule: {
|
|
35443
|
+
...preferModuleScopeStaticValue,
|
|
35444
|
+
framework: "global",
|
|
35445
|
+
category: "Architecture"
|
|
35446
|
+
}
|
|
35447
|
+
},
|
|
35448
|
+
{
|
|
35449
|
+
key: "react-doctor/prefer-stable-empty-fallback",
|
|
35450
|
+
id: "prefer-stable-empty-fallback",
|
|
35451
|
+
source: "react-doctor",
|
|
35452
|
+
originallyExternal: false,
|
|
35453
|
+
rule: {
|
|
35454
|
+
...preferStableEmptyFallback,
|
|
35455
|
+
framework: "global",
|
|
35456
|
+
category: "Performance"
|
|
35457
|
+
}
|
|
35458
|
+
},
|
|
34177
35459
|
{
|
|
34178
35460
|
key: "react-doctor/prefer-tag-over-role",
|
|
34179
35461
|
id: "prefer-tag-over-role",
|
|
@@ -34306,6 +35588,28 @@ const reactDoctorRules = [
|
|
|
34306
35588
|
category: "Correctness"
|
|
34307
35589
|
}
|
|
34308
35590
|
},
|
|
35591
|
+
{
|
|
35592
|
+
key: "react-doctor/redux-useselector-inline-derivation",
|
|
35593
|
+
id: "redux-useselector-inline-derivation",
|
|
35594
|
+
source: "react-doctor",
|
|
35595
|
+
originallyExternal: false,
|
|
35596
|
+
rule: {
|
|
35597
|
+
...reduxUseselectorInlineDerivation,
|
|
35598
|
+
framework: "global",
|
|
35599
|
+
category: "Performance"
|
|
35600
|
+
}
|
|
35601
|
+
},
|
|
35602
|
+
{
|
|
35603
|
+
key: "react-doctor/redux-useselector-returns-new-collection",
|
|
35604
|
+
id: "redux-useselector-returns-new-collection",
|
|
35605
|
+
source: "react-doctor",
|
|
35606
|
+
originallyExternal: false,
|
|
35607
|
+
rule: {
|
|
35608
|
+
...reduxUseselectorReturnsNewCollection,
|
|
35609
|
+
framework: "global",
|
|
35610
|
+
category: "Performance"
|
|
35611
|
+
}
|
|
35612
|
+
},
|
|
34309
35613
|
{
|
|
34310
35614
|
key: "react-doctor/rendering-animate-svg-wrapper",
|
|
34311
35615
|
id: "rendering-animate-svg-wrapper",
|
|
@@ -34449,6 +35753,17 @@ const reactDoctorRules = [
|
|
|
34449
35753
|
category: "Performance"
|
|
34450
35754
|
}
|
|
34451
35755
|
},
|
|
35756
|
+
{
|
|
35757
|
+
key: "react-doctor/rerender-lazy-ref-init",
|
|
35758
|
+
id: "rerender-lazy-ref-init",
|
|
35759
|
+
source: "react-doctor",
|
|
35760
|
+
originallyExternal: false,
|
|
35761
|
+
rule: {
|
|
35762
|
+
...rerenderLazyRefInit,
|
|
35763
|
+
framework: "global",
|
|
35764
|
+
category: "Performance"
|
|
35765
|
+
}
|
|
35766
|
+
},
|
|
34452
35767
|
{
|
|
34453
35768
|
key: "react-doctor/rerender-lazy-state-init",
|
|
34454
35769
|
id: "rerender-lazy-state-init",
|
|
@@ -35273,7 +36588,7 @@ const appendNode = (builder, block, node) => {
|
|
|
35273
36588
|
};
|
|
35274
36589
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
35275
36590
|
builder.nodeBlock.set(node, block);
|
|
35276
|
-
if (isFunctionLike$
|
|
36591
|
+
if (isFunctionLike$2(node)) return;
|
|
35277
36592
|
const record = node;
|
|
35278
36593
|
for (const key of Object.keys(record)) {
|
|
35279
36594
|
if (key === "parent") continue;
|
|
@@ -35611,7 +36926,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35611
36926
|
body: program.body
|
|
35612
36927
|
});
|
|
35613
36928
|
const visit = (node) => {
|
|
35614
|
-
if (isFunctionLike$
|
|
36929
|
+
if (isFunctionLike$2(node)) {
|
|
35615
36930
|
const body = node.body;
|
|
35616
36931
|
if (body) buildFor(node, body);
|
|
35617
36932
|
}
|
|
@@ -35628,7 +36943,7 @@ const analyzeControlFlow = (program) => {
|
|
|
35628
36943
|
const enclosingFunction = (node) => {
|
|
35629
36944
|
let current = node;
|
|
35630
36945
|
while (current) {
|
|
35631
|
-
if (isFunctionLike$
|
|
36946
|
+
if (isFunctionLike$2(current)) return current;
|
|
35632
36947
|
if (isNodeOfType(current, "Program")) return current;
|
|
35633
36948
|
current = current.parent ?? null;
|
|
35634
36949
|
}
|