oxlint-plugin-react-doctor 0.7.2-dev.e872767 → 0.7.2-dev.fb8ffb0
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 +0 -46
- package/dist/index.js +447 -1303
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,12 +532,6 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
-
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
-
"WebSocket",
|
|
537
|
-
"EventSource",
|
|
538
|
-
"BroadcastChannel",
|
|
539
|
-
"RTCPeerConnection"
|
|
540
|
-
]);
|
|
541
535
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
542
536
|
"location",
|
|
543
537
|
"window",
|
|
@@ -693,10 +687,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
693
687
|
"unwatch",
|
|
694
688
|
"unlisten",
|
|
695
689
|
"unsub",
|
|
696
|
-
"abort"
|
|
697
|
-
"disconnect",
|
|
698
|
-
"unobserve",
|
|
699
|
-
"close"
|
|
690
|
+
"abort"
|
|
700
691
|
]);
|
|
701
692
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
702
693
|
"remove",
|
|
@@ -2257,39 +2248,6 @@ const anchorHasContent = defineRule({
|
|
|
2257
2248
|
}
|
|
2258
2249
|
});
|
|
2259
2250
|
//#endregion
|
|
2260
|
-
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
-
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
-
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
-
const expression = stripParenExpression(rawExpression);
|
|
2264
|
-
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
-
return staticValue === null ? null : [staticValue];
|
|
2268
|
-
}
|
|
2269
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
-
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
-
if (consequentValues === null) return null;
|
|
2272
|
-
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
-
if (alternateValues === null) return null;
|
|
2274
|
-
return [...consequentValues, ...alternateValues];
|
|
2275
|
-
}
|
|
2276
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
-
if (remainingHops === 0) return null;
|
|
2278
|
-
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2281
|
-
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2282
|
-
}
|
|
2283
|
-
return null;
|
|
2284
|
-
};
|
|
2285
|
-
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2286
|
-
const value = attribute.value;
|
|
2287
|
-
if (!value) return null;
|
|
2288
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2289
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2290
|
-
return null;
|
|
2291
|
-
};
|
|
2292
|
-
//#endregion
|
|
2293
2251
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2294
2252
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2295
2253
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2317,14 +2275,30 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2317
2275
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2318
2276
|
const isInvalidHref = (value, validHrefs) => {
|
|
2319
2277
|
if (validHrefs.has(value)) return false;
|
|
2320
|
-
|
|
2321
|
-
|
|
2278
|
+
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
2279
|
+
};
|
|
2280
|
+
const getStaticHrefValue = (value) => {
|
|
2281
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2282
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2283
|
+
const expression = value.expression;
|
|
2284
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2285
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2286
|
+
}
|
|
2287
|
+
return null;
|
|
2322
2288
|
};
|
|
2323
|
-
const
|
|
2289
|
+
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2290
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2324
2291
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2325
2292
|
const expression = value.expression;
|
|
2326
2293
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2327
|
-
if (isNodeOfType(expression, "Literal")
|
|
2294
|
+
if (isNodeOfType(expression, "Literal")) {
|
|
2295
|
+
if (expression.value === null) return true;
|
|
2296
|
+
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2297
|
+
}
|
|
2298
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2299
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2300
|
+
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2301
|
+
}
|
|
2328
2302
|
}
|
|
2329
2303
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2330
2304
|
return false;
|
|
@@ -2357,10 +2331,9 @@ const anchorIsValid = defineRule({
|
|
|
2357
2331
|
});
|
|
2358
2332
|
return;
|
|
2359
2333
|
}
|
|
2360
|
-
|
|
2361
|
-
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2334
|
+
if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
|
|
2362
2335
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2363
|
-
if (!hasOnClick &&
|
|
2336
|
+
if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
|
|
2364
2337
|
context.report({
|
|
2365
2338
|
node: node.name,
|
|
2366
2339
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3416,25 +3389,6 @@ const ariaRole = defineRule({
|
|
|
3416
3389
|
if (!roleAttribute) return;
|
|
3417
3390
|
const elementType = getElementType(node, context.settings);
|
|
3418
3391
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3419
|
-
const reportFirstInvalidCandidate = (candidates) => {
|
|
3420
|
-
for (const candidate of candidates) {
|
|
3421
|
-
if (candidate.trim().length === 0) {
|
|
3422
|
-
context.report({
|
|
3423
|
-
node: roleAttribute,
|
|
3424
|
-
message: buildBaseMessage("")
|
|
3425
|
-
});
|
|
3426
|
-
return;
|
|
3427
|
-
}
|
|
3428
|
-
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3429
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3430
|
-
context.report({
|
|
3431
|
-
node: roleAttribute,
|
|
3432
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3433
|
-
});
|
|
3434
|
-
return;
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
};
|
|
3438
3392
|
const value = roleAttribute.value;
|
|
3439
3393
|
if (!value) {
|
|
3440
3394
|
context.report({
|
|
@@ -3451,7 +3405,22 @@ const ariaRole = defineRule({
|
|
|
3451
3405
|
});
|
|
3452
3406
|
return;
|
|
3453
3407
|
}
|
|
3454
|
-
|
|
3408
|
+
const stringValue = value.value;
|
|
3409
|
+
if (stringValue.trim().length === 0) {
|
|
3410
|
+
context.report({
|
|
3411
|
+
node: roleAttribute,
|
|
3412
|
+
message: buildBaseMessage("")
|
|
3413
|
+
});
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3417
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3418
|
+
context.report({
|
|
3419
|
+
node: roleAttribute,
|
|
3420
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3421
|
+
});
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3455
3424
|
return;
|
|
3456
3425
|
}
|
|
3457
3426
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3472,11 +3441,6 @@ const ariaRole = defineRule({
|
|
|
3472
3441
|
});
|
|
3473
3442
|
return;
|
|
3474
3443
|
}
|
|
3475
|
-
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3476
|
-
if (resolvedCandidates !== null) {
|
|
3477
|
-
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3478
|
-
return;
|
|
3479
|
-
}
|
|
3480
3444
|
return;
|
|
3481
3445
|
}
|
|
3482
3446
|
context.report({
|
|
@@ -5193,7 +5157,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5193
5157
|
const callee = node.callee;
|
|
5194
5158
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5195
5159
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5196
|
-
if (!isWebStorageObject(
|
|
5160
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
5197
5161
|
const keyArgument = node.arguments?.[0];
|
|
5198
5162
|
if (!keyArgument) return;
|
|
5199
5163
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6133,21 +6097,19 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6133
6097
|
})
|
|
6134
6098
|
});
|
|
6135
6099
|
//#endregion
|
|
6136
|
-
//#region src/plugin/utils/is-global-method-call.ts
|
|
6137
|
-
const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
6138
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6139
|
-
const callee = stripParenExpression(node.callee);
|
|
6140
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
6141
|
-
const receiver = stripParenExpression(callee.object);
|
|
6142
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6143
|
-
};
|
|
6144
|
-
//#endregion
|
|
6145
6100
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6146
6101
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6147
6102
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6148
6103
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6149
6104
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6150
|
-
const isJsonStringifyCall = (node) =>
|
|
6105
|
+
const isJsonStringifyCall = (node) => {
|
|
6106
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6107
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6108
|
+
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6109
|
+
if (node.callee.object.name !== "JSON") return false;
|
|
6110
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6111
|
+
return node.callee.property.name === "stringify";
|
|
6112
|
+
};
|
|
6151
6113
|
const resolveStringKey = (keyArg, context) => {
|
|
6152
6114
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6153
6115
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6166,9 +6128,8 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6166
6128
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6167
6129
|
create: (context) => ({ CallExpression(node) {
|
|
6168
6130
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6169
|
-
|
|
6170
|
-
if (!
|
|
6171
|
-
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6131
|
+
if (!isNodeOfType(node.callee.object, "Identifier")) return;
|
|
6132
|
+
if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
|
|
6172
6133
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6173
6134
|
if (node.callee.property.name !== "setItem") return;
|
|
6174
6135
|
const keyArg = node.arguments?.[0];
|
|
@@ -6181,7 +6142,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6181
6142
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6182
6143
|
context.report({
|
|
6183
6144
|
node: keyArg,
|
|
6184
|
-
message: `${
|
|
6145
|
+
message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6185
6146
|
});
|
|
6186
6147
|
} })
|
|
6187
6148
|
});
|
|
@@ -7662,96 +7623,6 @@ const displayName = defineRule({
|
|
|
7662
7623
|
}
|
|
7663
7624
|
});
|
|
7664
7625
|
//#endregion
|
|
7665
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7666
|
-
const isReactHookName = (name) => {
|
|
7667
|
-
if (!name.startsWith("use")) return false;
|
|
7668
|
-
if (name.length === 3) return true;
|
|
7669
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
7670
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7671
|
-
};
|
|
7672
|
-
//#endregion
|
|
7673
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7674
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7675
|
-
//#endregion
|
|
7676
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7677
|
-
const hocWrapperCalleeName = (callee) => {
|
|
7678
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7679
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7680
|
-
return null;
|
|
7681
|
-
};
|
|
7682
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7683
|
-
let current = functionNode;
|
|
7684
|
-
for (;;) {
|
|
7685
|
-
const parent = current.parent;
|
|
7686
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7687
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7688
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7689
|
-
current = parent;
|
|
7690
|
-
continue;
|
|
7691
|
-
}
|
|
7692
|
-
}
|
|
7693
|
-
break;
|
|
7694
|
-
}
|
|
7695
|
-
const binding = current.parent;
|
|
7696
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7697
|
-
return null;
|
|
7698
|
-
};
|
|
7699
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7700
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7701
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
7702
|
-
};
|
|
7703
|
-
//#endregion
|
|
7704
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7705
|
-
const findEnclosingFunction = (node) => {
|
|
7706
|
-
let cursor = node.parent;
|
|
7707
|
-
while (cursor) {
|
|
7708
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
7709
|
-
cursor = cursor.parent ?? null;
|
|
7710
|
-
}
|
|
7711
|
-
return null;
|
|
7712
|
-
};
|
|
7713
|
-
//#endregion
|
|
7714
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7715
|
-
const enclosingComponentOrHookName = (node) => {
|
|
7716
|
-
const functionNode = findEnclosingFunction(node);
|
|
7717
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7718
|
-
};
|
|
7719
|
-
//#endregion
|
|
7720
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7721
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
7722
|
-
let node = callExpression;
|
|
7723
|
-
let parent = node.parent;
|
|
7724
|
-
while (parent) {
|
|
7725
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7726
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7727
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7728
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7729
|
-
node = parent;
|
|
7730
|
-
parent = node.parent;
|
|
7731
|
-
continue;
|
|
7732
|
-
}
|
|
7733
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7734
|
-
node = parent;
|
|
7735
|
-
parent = node.parent;
|
|
7736
|
-
continue;
|
|
7737
|
-
}
|
|
7738
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7739
|
-
node = parent;
|
|
7740
|
-
parent = node.parent;
|
|
7741
|
-
continue;
|
|
7742
|
-
}
|
|
7743
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7744
|
-
const expressions = parent.expressions ?? [];
|
|
7745
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
7746
|
-
node = parent;
|
|
7747
|
-
parent = node.parent;
|
|
7748
|
-
continue;
|
|
7749
|
-
}
|
|
7750
|
-
return false;
|
|
7751
|
-
}
|
|
7752
|
-
return false;
|
|
7753
|
-
};
|
|
7754
|
-
//#endregion
|
|
7755
7626
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7756
7627
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7757
7628
|
if (!node || typeof node !== "object") return;
|
|
@@ -7903,17 +7774,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7903
7774
|
};
|
|
7904
7775
|
//#endregion
|
|
7905
7776
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7906
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7907
|
-
const RESOURCE_NOUN_BY_KIND = {
|
|
7908
|
-
subscribe: "subscription",
|
|
7909
|
-
timer: "timer",
|
|
7910
|
-
socket: "connection"
|
|
7911
|
-
};
|
|
7912
|
-
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7913
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
7914
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7915
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7916
|
-
};
|
|
7917
7777
|
const findSubscribeLikeUsages = (callback) => {
|
|
7918
7778
|
const usages = [];
|
|
7919
7779
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7925,14 +7785,6 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7925
7785
|
}
|
|
7926
7786
|
walkAst(callback, (child) => {
|
|
7927
7787
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7928
|
-
if (isSocketConstruction(child)) {
|
|
7929
|
-
usages.push({
|
|
7930
|
-
kind: "socket",
|
|
7931
|
-
node: child,
|
|
7932
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7933
|
-
});
|
|
7934
|
-
return;
|
|
7935
|
-
}
|
|
7936
7788
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7937
7789
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7938
7790
|
usages.push({
|
|
@@ -7942,7 +7794,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7942
7794
|
});
|
|
7943
7795
|
return;
|
|
7944
7796
|
}
|
|
7945
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") &&
|
|
7797
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7946
7798
|
kind: "subscribe",
|
|
7947
7799
|
node: child,
|
|
7948
7800
|
resourceName: child.callee.property.name
|
|
@@ -7958,13 +7810,6 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7958
7810
|
};
|
|
7959
7811
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7960
7812
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7961
|
-
walkAst(effectCallback.body, (child) => {
|
|
7962
|
-
if (!isSubscribeOrObserveCall(child)) return;
|
|
7963
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7964
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7965
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7966
|
-
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7967
|
-
});
|
|
7968
7813
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7969
7814
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7970
7815
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7972,12 +7817,7 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7972
7817
|
const bindingName = declarator.id.name;
|
|
7973
7818
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7974
7819
|
const init = declarator.init;
|
|
7975
|
-
if (!init) continue;
|
|
7976
|
-
if (isSocketConstruction(init)) {
|
|
7977
|
-
bindings.subscriptionNames.add(bindingName);
|
|
7978
|
-
continue;
|
|
7979
|
-
}
|
|
7980
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7820
|
+
if (!init || !isNodeOfType(init, "CallExpression")) continue;
|
|
7981
7821
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7982
7822
|
bindings.subscriptionNames.add(bindingName);
|
|
7983
7823
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -8007,22 +7847,6 @@ const getRangeStart = (node) => {
|
|
|
8007
7847
|
const rangeStart = node.range?.[0];
|
|
8008
7848
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8009
7849
|
};
|
|
8010
|
-
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8011
|
-
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8012
|
-
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8013
|
-
const releaseStarts = [];
|
|
8014
|
-
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8015
|
-
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8016
|
-
const releaseStart = getRangeStart(child);
|
|
8017
|
-
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8018
|
-
});
|
|
8019
|
-
if (releaseStarts.length === 0) return usages;
|
|
8020
|
-
return usages.filter((usage) => {
|
|
8021
|
-
const usageStart = getRangeStart(usage.node);
|
|
8022
|
-
if (usageStart === null) return true;
|
|
8023
|
-
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8024
|
-
});
|
|
8025
|
-
};
|
|
8026
7850
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
8027
7851
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
8028
7852
|
const returnStart = getRangeStart(returnStatement);
|
|
@@ -8045,177 +7869,26 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
8045
7869
|
});
|
|
8046
7870
|
return didFindCleanupReturn;
|
|
8047
7871
|
};
|
|
8048
|
-
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8049
|
-
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8050
|
-
if (!isNodeOfType(property, "Property")) return false;
|
|
8051
|
-
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8052
|
-
if (keyName === "signal") return true;
|
|
8053
|
-
if (keyName !== "once") return false;
|
|
8054
|
-
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8055
|
-
};
|
|
8056
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8057
|
-
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8058
|
-
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8059
|
-
["addListener", new Set([
|
|
8060
|
-
"removeListener",
|
|
8061
|
-
"off",
|
|
8062
|
-
"abort"
|
|
8063
|
-
])],
|
|
8064
|
-
["on", new Set([
|
|
8065
|
-
"off",
|
|
8066
|
-
"removeListener",
|
|
8067
|
-
"on"
|
|
8068
|
-
])],
|
|
8069
|
-
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8070
|
-
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8071
|
-
["watch", new Set(["unwatch", "close"])],
|
|
8072
|
-
["listen", new Set(["unlisten", "close"])],
|
|
8073
|
-
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8074
|
-
]);
|
|
8075
|
-
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8076
|
-
"cleanup",
|
|
8077
|
-
"dispose",
|
|
8078
|
-
"destroy",
|
|
8079
|
-
"teardown"
|
|
8080
|
-
]);
|
|
8081
|
-
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8082
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8083
|
-
const getReleaseVerbName = (node) => {
|
|
8084
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8085
|
-
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8086
|
-
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8087
|
-
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8088
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8089
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8090
|
-
return null;
|
|
8091
|
-
};
|
|
8092
|
-
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8093
|
-
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8094
|
-
let didFindPairedRelease = false;
|
|
8095
|
-
walkAst(body, (child) => {
|
|
8096
|
-
if (didFindPairedRelease) return false;
|
|
8097
|
-
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8098
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8099
|
-
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8100
|
-
didFindPairedRelease = true;
|
|
8101
|
-
return false;
|
|
8102
|
-
}
|
|
8103
|
-
});
|
|
8104
|
-
return didFindPairedRelease;
|
|
8105
|
-
};
|
|
8106
|
-
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8107
|
-
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8108
|
-
let programNode = anyNode;
|
|
8109
|
-
while (programNode.parent) programNode = programNode.parent;
|
|
8110
|
-
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8111
|
-
if (cached) return cached;
|
|
8112
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8113
|
-
walkAst(programNode, (child) => {
|
|
8114
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8115
|
-
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8116
|
-
});
|
|
8117
|
-
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8118
|
-
return releaseVerbNames;
|
|
8119
|
-
};
|
|
8120
|
-
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8121
|
-
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8122
|
-
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8123
|
-
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8124
|
-
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8125
|
-
return false;
|
|
8126
|
-
};
|
|
8127
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8128
|
-
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8129
|
-
const body = retainedFunction.body;
|
|
8130
|
-
if (!body) return null;
|
|
8131
|
-
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8132
|
-
let leak = null;
|
|
8133
|
-
walkAst(body, (child) => {
|
|
8134
|
-
if (leak !== null) return false;
|
|
8135
|
-
if (isFunctionLike$1(child)) return false;
|
|
8136
|
-
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8137
|
-
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8138
|
-
leak = {
|
|
8139
|
-
kind: "socket",
|
|
8140
|
-
node: child,
|
|
8141
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8142
|
-
};
|
|
8143
|
-
return false;
|
|
8144
|
-
}
|
|
8145
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8146
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8147
|
-
leak = {
|
|
8148
|
-
kind: "timer",
|
|
8149
|
-
node: child,
|
|
8150
|
-
resourceName: "setInterval"
|
|
8151
|
-
};
|
|
8152
|
-
return false;
|
|
8153
|
-
}
|
|
8154
|
-
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8155
|
-
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8156
|
-
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8157
|
-
kind: "subscribe",
|
|
8158
|
-
node: child,
|
|
8159
|
-
resourceName: registrationVerbName
|
|
8160
|
-
};
|
|
8161
|
-
return false;
|
|
8162
|
-
}
|
|
8163
|
-
});
|
|
8164
|
-
return leak;
|
|
8165
|
-
};
|
|
8166
|
-
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8167
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8168
|
-
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8169
|
-
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8170
|
-
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8171
|
-
};
|
|
8172
7872
|
const effectNeedsCleanup = defineRule({
|
|
8173
7873
|
id: "effect-needs-cleanup",
|
|
8174
7874
|
title: "Effect subscription or timer never cleaned up",
|
|
8175
7875
|
severity: "error",
|
|
8176
7876
|
tags: ["test-noise"],
|
|
8177
|
-
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers,
|
|
8178
|
-
create: (context) => {
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8193
|
-
return;
|
|
8194
|
-
}
|
|
8195
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8196
|
-
const callback = getEffectCallback(node);
|
|
8197
|
-
if (!callback) return;
|
|
8198
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8199
|
-
if (usages.length === 0) return;
|
|
8200
|
-
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8201
|
-
const firstUsage = usages[0];
|
|
8202
|
-
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8203
|
-
context.report({
|
|
8204
|
-
node,
|
|
8205
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8206
|
-
});
|
|
8207
|
-
},
|
|
8208
|
-
FunctionDeclaration(node) {
|
|
8209
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8210
|
-
},
|
|
8211
|
-
ArrowFunctionExpression(node) {
|
|
8212
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8213
|
-
},
|
|
8214
|
-
FunctionExpression(node) {
|
|
8215
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8216
|
-
}
|
|
8217
|
-
};
|
|
8218
|
-
}
|
|
7877
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
7878
|
+
create: (context) => ({ CallExpression(node) {
|
|
7879
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
7880
|
+
const callback = getEffectCallback(node);
|
|
7881
|
+
if (!callback) return;
|
|
7882
|
+
const usages = findSubscribeLikeUsages(callback);
|
|
7883
|
+
if (usages.length === 0) return;
|
|
7884
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
7885
|
+
const firstUsage = usages[0];
|
|
7886
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
7887
|
+
context.report({
|
|
7888
|
+
node,
|
|
7889
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
7890
|
+
});
|
|
7891
|
+
} })
|
|
8219
7892
|
});
|
|
8220
7893
|
//#endregion
|
|
8221
7894
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8774,6 +8447,17 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8774
8447
|
return computedCaptures;
|
|
8775
8448
|
};
|
|
8776
8449
|
//#endregion
|
|
8450
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8451
|
+
const isReactHookName = (name) => {
|
|
8452
|
+
if (!name.startsWith("use")) return false;
|
|
8453
|
+
if (name.length === 3) return true;
|
|
8454
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
8455
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8456
|
+
};
|
|
8457
|
+
//#endregion
|
|
8458
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8459
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8460
|
+
//#endregion
|
|
8777
8461
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8778
8462
|
const reactHocCalleeName = (callee) => {
|
|
8779
8463
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -10210,10 +9894,7 @@ const PRAGMA = "React";
|
|
|
10210
9894
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
10211
9895
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
10212
9896
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
10213
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10214
|
-
const receiver = stripParenExpression(node.callee.object);
|
|
10215
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10216
|
-
}
|
|
9897
|
+
if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
|
|
10217
9898
|
return true;
|
|
10218
9899
|
};
|
|
10219
9900
|
//#endregion
|
|
@@ -11378,8 +11059,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11378
11059
|
if (node.attributes.length === 0) return;
|
|
11379
11060
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11380
11061
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11381
|
-
const
|
|
11382
|
-
if (
|
|
11062
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
11063
|
+
if (!role) return;
|
|
11383
11064
|
let hasInteractiveHandler = false;
|
|
11384
11065
|
for (const attribute of node.attributes) {
|
|
11385
11066
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11393,16 +11074,11 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11393
11074
|
const elementType = getElementType(node, context.settings);
|
|
11394
11075
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11395
11076
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11077
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11078
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11396
11079
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11400
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11401
|
-
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11402
|
-
}
|
|
11403
|
-
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11404
|
-
const roleDisplay = roleCandidates.join("' / '");
|
|
11405
|
-
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11080
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11081
|
+
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
11406
11082
|
context.report({
|
|
11407
11083
|
node,
|
|
11408
11084
|
message
|
|
@@ -11591,6 +11267,16 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11591
11267
|
return names;
|
|
11592
11268
|
};
|
|
11593
11269
|
//#endregion
|
|
11270
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11271
|
+
const findEnclosingFunction = (node) => {
|
|
11272
|
+
let cursor = node.parent;
|
|
11273
|
+
while (cursor) {
|
|
11274
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
11275
|
+
cursor = cursor.parent ?? null;
|
|
11276
|
+
}
|
|
11277
|
+
return null;
|
|
11278
|
+
};
|
|
11279
|
+
//#endregion
|
|
11594
11280
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11595
11281
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11596
11282
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12290,15 +11976,14 @@ const jsCacheStorage = defineRule({
|
|
|
12290
11976
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12291
11977
|
CallExpression(node) {
|
|
12292
11978
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12293
|
-
|
|
12294
|
-
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11979
|
+
if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
|
|
12295
11980
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12296
11981
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12297
11982
|
const storageKey = String(node.arguments[0].value);
|
|
12298
11983
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12299
11984
|
storageReadCounts.set(storageKey, readCount);
|
|
12300
11985
|
if (readCount === 2) {
|
|
12301
|
-
const storageName =
|
|
11986
|
+
const storageName = node.callee.object.name;
|
|
12302
11987
|
context.report({
|
|
12303
11988
|
node,
|
|
12304
11989
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -12312,16 +11997,13 @@ const jsCacheStorage = defineRule({
|
|
|
12312
11997
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12313
11998
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12314
11999
|
const callee = callExpression.callee;
|
|
12315
|
-
if (isNodeOfType(callee, "MemberExpression"))
|
|
12316
|
-
const receiver = stripParenExpression(callee.object);
|
|
12317
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12318
|
-
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12319
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12320
|
-
return true;
|
|
12321
|
-
}
|
|
12322
|
-
return false;
|
|
12323
|
-
}
|
|
12000
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12324
12001
|
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12002
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12003
|
+
const receiver = callee.object;
|
|
12004
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12005
|
+
return true;
|
|
12006
|
+
}
|
|
12325
12007
|
return false;
|
|
12326
12008
|
};
|
|
12327
12009
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12333,7 +12015,10 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12333
12015
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12334
12016
|
let cursor = receiverNode;
|
|
12335
12017
|
while (cursor) {
|
|
12336
|
-
|
|
12018
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12019
|
+
cursor = cursor.expression;
|
|
12020
|
+
continue;
|
|
12021
|
+
}
|
|
12337
12022
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12338
12023
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12339
12024
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12409,7 +12094,10 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12409
12094
|
let hops = 0;
|
|
12410
12095
|
while (cursor && hops < 12) {
|
|
12411
12096
|
hops += 1;
|
|
12412
|
-
|
|
12097
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12098
|
+
cursor = cursor.expression;
|
|
12099
|
+
continue;
|
|
12100
|
+
}
|
|
12413
12101
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12414
12102
|
const callee = cursor.callee;
|
|
12415
12103
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12433,7 +12121,10 @@ const isSmallLiteralArray = (node) => {
|
|
|
12433
12121
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12434
12122
|
let cursor = receiverNode;
|
|
12435
12123
|
while (cursor) {
|
|
12436
|
-
|
|
12124
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12125
|
+
cursor = cursor.expression;
|
|
12126
|
+
continue;
|
|
12127
|
+
}
|
|
12437
12128
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12438
12129
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12439
12130
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12498,7 +12189,7 @@ const jsCombineIterations = defineRule({
|
|
|
12498
12189
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12499
12190
|
const outerMethod = node.callee.property.name;
|
|
12500
12191
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12501
|
-
const innerCall =
|
|
12192
|
+
const innerCall = node.callee.object;
|
|
12502
12193
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12503
12194
|
const innerMethod = innerCall.callee.property.name;
|
|
12504
12195
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12571,10 +12262,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12571
12262
|
if (!filterArgument) return;
|
|
12572
12263
|
const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
|
|
12573
12264
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12574
|
-
const innerCall =
|
|
12265
|
+
const innerCall = node.callee.object;
|
|
12575
12266
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12576
12267
|
if (innerCall.callee.property.name !== "map") return;
|
|
12577
|
-
const receiver =
|
|
12268
|
+
const receiver = innerCall.callee.object;
|
|
12578
12269
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12579
12270
|
const elements = receiver.elements ?? [];
|
|
12580
12271
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13834,7 +13525,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13834
13525
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13835
13526
|
create: (context) => ({ CallExpression(node) {
|
|
13836
13527
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13837
|
-
const receiver =
|
|
13528
|
+
const receiver = node.callee.object;
|
|
13838
13529
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13839
13530
|
const spreadArgument = receiver.elements[0].argument;
|
|
13840
13531
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18871,8 +18562,7 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18871
18562
|
};
|
|
18872
18563
|
const describeClientSideNavigation = (node) => {
|
|
18873
18564
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18874
|
-
const
|
|
18875
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18565
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
18876
18566
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18877
18567
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18878
18568
|
}
|
|
@@ -19492,7 +19182,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19492
19182
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19493
19183
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19494
19184
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19495
|
-
if (!isCookieReceiver(
|
|
19185
|
+
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19496
19186
|
return node.callee.property.name;
|
|
19497
19187
|
};
|
|
19498
19188
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19506,7 +19196,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19506
19196
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19507
19197
|
const { property, object } = node.callee;
|
|
19508
19198
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19509
|
-
if (isSafeReceiverChain(
|
|
19199
|
+
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19510
19200
|
return true;
|
|
19511
19201
|
};
|
|
19512
19202
|
const getDbCallDescription = (node) => {
|
|
@@ -19514,8 +19204,7 @@ const getDbCallDescription = (node) => {
|
|
|
19514
19204
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19515
19205
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19516
19206
|
const methodName = node.callee.property.name;
|
|
19517
|
-
const
|
|
19518
|
-
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19207
|
+
const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
19519
19208
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19520
19209
|
};
|
|
19521
19210
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20915,13 +20604,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20915
20604
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20916
20605
|
let node = ref.identifier;
|
|
20917
20606
|
let parent = node.parent;
|
|
20918
|
-
while (parent &&
|
|
20607
|
+
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20919
20608
|
node = parent;
|
|
20920
20609
|
parent = node.parent;
|
|
20921
20610
|
}
|
|
20922
20611
|
if (current.callee === node) return current;
|
|
20923
20612
|
}
|
|
20924
|
-
if (isNodeOfType(current, "MemberExpression")
|
|
20613
|
+
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20925
20614
|
return null;
|
|
20926
20615
|
};
|
|
20927
20616
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -21056,7 +20745,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
21056
20745
|
const importDeclaration = declarationNode.parent;
|
|
21057
20746
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
21058
20747
|
}));
|
|
21059
|
-
const isHookCallee
|
|
20748
|
+
const isHookCallee = (analysis, node, hookName) => {
|
|
21060
20749
|
if (!node) return false;
|
|
21061
20750
|
if (isNodeOfType(node, "Identifier")) {
|
|
21062
20751
|
if (node.name === hookName) return true;
|
|
@@ -21065,19 +20754,15 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
21065
20754
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
21066
20755
|
return false;
|
|
21067
20756
|
}
|
|
21068
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21069
|
-
const receiver = stripParenExpression(node.object);
|
|
21070
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21071
|
-
}
|
|
20757
|
+
if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21072
20758
|
return false;
|
|
21073
20759
|
};
|
|
21074
20760
|
const isUseEffect = (node) => {
|
|
21075
20761
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
21076
20762
|
const callee = node.callee;
|
|
21077
20763
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
21078
|
-
if (
|
|
21079
|
-
|
|
21080
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20764
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
|
|
20765
|
+
return false;
|
|
21081
20766
|
};
|
|
21082
20767
|
const getEffectFn = (analysis, node) => {
|
|
21083
20768
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -21105,7 +20790,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21105
20790
|
const node = def.node;
|
|
21106
20791
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21107
20792
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21108
|
-
if (!isHookCallee
|
|
20793
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21109
20794
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21110
20795
|
const elements = node.id.elements ?? [];
|
|
21111
20796
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -21116,7 +20801,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
21116
20801
|
const node = def.node;
|
|
21117
20802
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21118
20803
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21119
|
-
if (!isHookCallee
|
|
20804
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21120
20805
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21121
20806
|
const elements = node.id.elements ?? [];
|
|
21122
20807
|
if (elements.length !== 2) return false;
|
|
@@ -21163,7 +20848,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21163
20848
|
const node = def.node;
|
|
21164
20849
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21165
20850
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21166
|
-
return isHookCallee
|
|
20851
|
+
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21167
20852
|
}));
|
|
21168
20853
|
const isRefCurrent = (ref) => {
|
|
21169
20854
|
const parent = ref.identifier.parent;
|
|
@@ -21176,15 +20861,11 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
21176
20861
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
21177
20862
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
21178
20863
|
if (!isPropAlias(analysis, ref)) return false;
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21182
|
-
effectiveNode = parent;
|
|
21183
|
-
parent = effectiveNode.parent;
|
|
21184
|
-
}
|
|
20864
|
+
const identifier = ref.identifier;
|
|
20865
|
+
const parent = identifier.parent;
|
|
21185
20866
|
if (!parent) return false;
|
|
21186
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
21187
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
20867
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
|
|
20868
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
|
|
21188
20869
|
const memberParent = parent.parent;
|
|
21189
20870
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
21190
20871
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -21195,7 +20876,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
21195
20876
|
};
|
|
21196
20877
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
21197
20878
|
const getUseStateDecl = (analysis, ref) => {
|
|
21198
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee
|
|
20879
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21199
20880
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
21200
20881
|
return node ?? null;
|
|
21201
20882
|
};
|
|
@@ -21400,7 +21081,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21400
21081
|
const noAdjustStateOnPropChange = defineRule({
|
|
21401
21082
|
id: "no-adjust-state-on-prop-change",
|
|
21402
21083
|
title: "State synced to a prop inside an effect",
|
|
21403
|
-
severity: "
|
|
21084
|
+
severity: "error",
|
|
21404
21085
|
tags: ["test-noise"],
|
|
21405
21086
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
21406
21087
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21441,23 +21122,7 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21441
21122
|
"summary",
|
|
21442
21123
|
"textarea"
|
|
21443
21124
|
]);
|
|
21444
|
-
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21445
|
-
const value = attribute.value;
|
|
21446
|
-
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21447
|
-
const expression = value.expression;
|
|
21448
|
-
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21449
|
-
};
|
|
21450
|
-
const DISABLEABLE_TAGS = new Set([
|
|
21451
|
-
"button",
|
|
21452
|
-
"input",
|
|
21453
|
-
"select",
|
|
21454
|
-
"textarea"
|
|
21455
|
-
]);
|
|
21456
21125
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21457
|
-
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21458
|
-
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21459
|
-
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21460
|
-
}
|
|
21461
21126
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21462
21127
|
switch (tagName) {
|
|
21463
21128
|
case "input": {
|
|
@@ -21471,10 +21136,7 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21471
21136
|
case "a":
|
|
21472
21137
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21473
21138
|
case "audio":
|
|
21474
|
-
case "video":
|
|
21475
|
-
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21476
|
-
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21477
|
-
}
|
|
21139
|
+
case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
|
|
21478
21140
|
default: return false;
|
|
21479
21141
|
}
|
|
21480
21142
|
};
|
|
@@ -22438,8 +22100,7 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22438
22100
|
"some"
|
|
22439
22101
|
]);
|
|
22440
22102
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22441
|
-
const isPositionallyStableIterationReceiver = (
|
|
22442
|
-
const receiver = stripParenExpression(receiverNode);
|
|
22103
|
+
const isPositionallyStableIterationReceiver = (receiver) => {
|
|
22443
22104
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22444
22105
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22445
22106
|
const only = receiver.elements[0];
|
|
@@ -22450,13 +22111,17 @@ const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
|
22450
22111
|
}
|
|
22451
22112
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22452
22113
|
const callee = receiver.callee;
|
|
22453
|
-
if (
|
|
22114
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22454
22115
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22455
22116
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22456
22117
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22457
22118
|
return false;
|
|
22458
22119
|
};
|
|
22459
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
22120
|
+
const isArrayFromMapperCallback = (parentCall, callback) => {
|
|
22121
|
+
if (parentCall.arguments[1] !== callback) return false;
|
|
22122
|
+
const callee = parentCall.callee;
|
|
22123
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
|
|
22124
|
+
};
|
|
22460
22125
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22461
22126
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22462
22127
|
for (const property of source.properties ?? []) {
|
|
@@ -22515,12 +22180,18 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22515
22180
|
return false;
|
|
22516
22181
|
}
|
|
22517
22182
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22518
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(
|
|
22183
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22519
22184
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22520
22185
|
}
|
|
22521
22186
|
return false;
|
|
22522
22187
|
};
|
|
22523
|
-
const isReactCloneElement = (callExpression) =>
|
|
22188
|
+
const isReactCloneElement = (callExpression) => {
|
|
22189
|
+
const callee = callExpression.callee;
|
|
22190
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22191
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22192
|
+
if (callee.property.name !== "cloneElement") return false;
|
|
22193
|
+
return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
|
|
22194
|
+
};
|
|
22524
22195
|
const noArrayIndexKey = defineRule({
|
|
22525
22196
|
id: "no-array-index-key",
|
|
22526
22197
|
title: "Array index used as a key",
|
|
@@ -23244,7 +22915,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
23244
22915
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
23245
22916
|
return false;
|
|
23246
22917
|
};
|
|
23247
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
22918
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23248
22919
|
"forEach",
|
|
23249
22920
|
"map",
|
|
23250
22921
|
"filter",
|
|
@@ -23265,51 +22936,30 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
23265
22936
|
if (parent.callee === functionNode) return true;
|
|
23266
22937
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
23267
22938
|
const callee = parent.callee;
|
|
23268
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
22939
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23269
22940
|
};
|
|
23270
22941
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
23271
|
-
const
|
|
23272
|
-
|
|
22942
|
+
const isGuardWithTerminatingBranch = (statement) => {
|
|
22943
|
+
if (!isNodeOfType(statement, "IfStatement")) return null;
|
|
22944
|
+
if (statement.alternate) return null;
|
|
22945
|
+
const consequent = statement.consequent;
|
|
22946
|
+
if (isTerminatingStatement(consequent)) return consequent;
|
|
22947
|
+
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
22948
|
+
return null;
|
|
22949
|
+
};
|
|
22950
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23273
22951
|
let fallThroughCount = 0;
|
|
23274
|
-
let
|
|
22952
|
+
let maxTerminatingPathCount = 0;
|
|
23275
22953
|
for (const statement of statements) {
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23280
|
-
fallThroughCount: 0,
|
|
23281
|
-
maxTerminatedCount: 0,
|
|
23282
|
-
doAllPathsTerminate: false
|
|
23283
|
-
};
|
|
23284
|
-
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23285
|
-
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23286
|
-
fallThroughCount: 0,
|
|
23287
|
-
maxTerminatedCount,
|
|
23288
|
-
doAllPathsTerminate: true
|
|
23289
|
-
};
|
|
23290
|
-
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23291
|
-
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22954
|
+
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22955
|
+
if (guardBranch) {
|
|
22956
|
+
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
23292
22957
|
continue;
|
|
23293
22958
|
}
|
|
23294
|
-
if (isTerminatingStatement(statement))
|
|
23295
|
-
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23296
|
-
return {
|
|
23297
|
-
fallThroughCount: 0,
|
|
23298
|
-
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23299
|
-
doAllPathsTerminate: true
|
|
23300
|
-
};
|
|
23301
|
-
}
|
|
22959
|
+
if (isTerminatingStatement(statement)) break;
|
|
23302
22960
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
23303
22961
|
}
|
|
23304
|
-
return
|
|
23305
|
-
fallThroughCount,
|
|
23306
|
-
maxTerminatedCount,
|
|
23307
|
-
doAllPathsTerminate: false
|
|
23308
|
-
};
|
|
23309
|
-
};
|
|
23310
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23311
|
-
const summary = analyzeStatementSequence(statements, context);
|
|
23312
|
-
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22962
|
+
return Math.max(maxTerminatingPathCount, fallThroughCount);
|
|
23313
22963
|
};
|
|
23314
22964
|
const collectLocalHelperFunctions = (root) => {
|
|
23315
22965
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23440,9 +23090,7 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23440
23090
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23441
23091
|
const firstStatement = (body.body ?? [])[0];
|
|
23442
23092
|
if (!firstStatement) return false;
|
|
23443
|
-
if (!
|
|
23444
|
-
const consequent = firstStatement.consequent;
|
|
23445
|
-
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23093
|
+
if (!isGuardWithTerminatingBranch(firstStatement)) return false;
|
|
23446
23094
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23447
23095
|
};
|
|
23448
23096
|
const noCascadingSetState = defineRule({
|
|
@@ -23801,6 +23449,40 @@ const noCloneElement = defineRule({
|
|
|
23801
23449
|
} })
|
|
23802
23450
|
});
|
|
23803
23451
|
//#endregion
|
|
23452
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23453
|
+
const hocWrapperCalleeName = (callee) => {
|
|
23454
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23455
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23456
|
+
return null;
|
|
23457
|
+
};
|
|
23458
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23459
|
+
let current = functionNode;
|
|
23460
|
+
for (;;) {
|
|
23461
|
+
const parent = current.parent;
|
|
23462
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23463
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23464
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23465
|
+
current = parent;
|
|
23466
|
+
continue;
|
|
23467
|
+
}
|
|
23468
|
+
}
|
|
23469
|
+
break;
|
|
23470
|
+
}
|
|
23471
|
+
const binding = current.parent;
|
|
23472
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23473
|
+
return null;
|
|
23474
|
+
};
|
|
23475
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23476
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23477
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
23478
|
+
};
|
|
23479
|
+
//#endregion
|
|
23480
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23481
|
+
const enclosingComponentOrHookName = (node) => {
|
|
23482
|
+
const functionNode = findEnclosingFunction(node);
|
|
23483
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23484
|
+
};
|
|
23485
|
+
//#endregion
|
|
23804
23486
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23805
23487
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23806
23488
|
const CONTEXT_MODULES = [
|
|
@@ -24771,21 +24453,11 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24771
24453
|
return isInitialOnlyPropName(arg.name);
|
|
24772
24454
|
};
|
|
24773
24455
|
//#endregion
|
|
24774
|
-
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24775
|
-
const isNoOpStatement = (statement) => {
|
|
24776
|
-
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24777
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24778
|
-
const expression = stripParenExpression(statement.expression);
|
|
24779
|
-
if (isNodeOfType(expression, "Literal")) return true;
|
|
24780
|
-
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24781
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24782
|
-
return false;
|
|
24783
|
-
};
|
|
24784
|
-
//#endregion
|
|
24785
24456
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24786
24457
|
const getCallbackStatements = (callback) => {
|
|
24787
24458
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24788
|
-
|
|
24459
|
+
if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
|
|
24460
|
+
return callback.body ? [callback.body] : [];
|
|
24789
24461
|
};
|
|
24790
24462
|
//#endregion
|
|
24791
24463
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24824,27 +24496,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24824
24496
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24825
24497
|
}
|
|
24826
24498
|
};
|
|
24827
|
-
const flattenGuardedStatements = (statements) => {
|
|
24828
|
-
const flattened = [];
|
|
24829
|
-
for (const statement of statements) {
|
|
24830
|
-
if (isNoOpStatement(statement)) continue;
|
|
24831
|
-
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24832
|
-
flattened.push(statement);
|
|
24833
|
-
continue;
|
|
24834
|
-
}
|
|
24835
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
24836
|
-
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24837
|
-
if (!branch) continue;
|
|
24838
|
-
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24839
|
-
if (flattenedBranch === null) return null;
|
|
24840
|
-
flattened.push(...flattenedBranch);
|
|
24841
|
-
}
|
|
24842
|
-
continue;
|
|
24843
|
-
}
|
|
24844
|
-
return null;
|
|
24845
|
-
}
|
|
24846
|
-
return flattened;
|
|
24847
|
-
};
|
|
24848
24499
|
const noDerivedStateEffect = defineRule({
|
|
24849
24500
|
id: "no-derived-state-effect",
|
|
24850
24501
|
title: "Derived state stored in an effect",
|
|
@@ -24876,8 +24527,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24876
24527
|
}
|
|
24877
24528
|
}
|
|
24878
24529
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24879
|
-
const statements =
|
|
24880
|
-
if (statements
|
|
24530
|
+
const statements = getCallbackStatements(callback);
|
|
24531
|
+
if (statements.length === 0) return;
|
|
24881
24532
|
if (!statements.every((statement) => {
|
|
24882
24533
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24883
24534
|
const expression = statement.expression;
|
|
@@ -25512,7 +25163,7 @@ const noDidMountSetState = defineRule({
|
|
|
25512
25163
|
const { mode } = resolveSettings$20(context.settings);
|
|
25513
25164
|
return { CallExpression(node) {
|
|
25514
25165
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25515
|
-
if (!isNodeOfType(
|
|
25166
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25516
25167
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25517
25168
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25518
25169
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25637,7 +25288,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25637
25288
|
const { mode } = resolveSettings$19(context.settings);
|
|
25638
25289
|
return { CallExpression(node) {
|
|
25639
25290
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25640
|
-
if (!isNodeOfType(
|
|
25291
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25641
25292
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25642
25293
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25643
25294
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25958,10 +25609,9 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25958
25609
|
create: (context) => ({ CallExpression(node) {
|
|
25959
25610
|
const callee = node.callee;
|
|
25960
25611
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25961
|
-
|
|
25962
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25612
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
25963
25613
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25964
|
-
if (context.scopes.symbolFor(
|
|
25614
|
+
if (context.scopes.symbolFor(callee.object) !== null) return;
|
|
25965
25615
|
if (!importsReactViewTransition(node)) return;
|
|
25966
25616
|
context.report({
|
|
25967
25617
|
node,
|
|
@@ -25981,8 +25631,7 @@ const noDocumentWrite = defineRule({
|
|
|
25981
25631
|
create: (context) => ({ CallExpression(node) {
|
|
25982
25632
|
const callee = node.callee;
|
|
25983
25633
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25984
|
-
|
|
25985
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25634
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
25986
25635
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25987
25636
|
context.report({
|
|
25988
25637
|
node,
|
|
@@ -26679,7 +26328,6 @@ const noEffectEventInDeps = defineRule({
|
|
|
26679
26328
|
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26680
26329
|
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26681
26330
|
}
|
|
26682
|
-
if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
|
|
26683
26331
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26684
26332
|
} });
|
|
26685
26333
|
return {
|
|
@@ -29259,7 +28907,7 @@ const noIsMounted = defineRule({
|
|
|
29259
28907
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
29260
28908
|
create: (context) => ({ CallExpression(node) {
|
|
29261
28909
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
29262
|
-
if (!isNodeOfType(
|
|
28910
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29263
28911
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
29264
28912
|
if (!getParentComponent(node)) return;
|
|
29265
28913
|
context.report({
|
|
@@ -29274,9 +28922,7 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
29274
28922
|
const isJsonMethodCall = (node, method) => {
|
|
29275
28923
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
29276
28924
|
const callee = node.callee;
|
|
29277
|
-
|
|
29278
|
-
const receiver = stripParenExpression(callee.object);
|
|
29279
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28925
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
29280
28926
|
};
|
|
29281
28927
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
29282
28928
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29361,7 +29007,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29361
29007
|
const noJsxElementType = defineRule({
|
|
29362
29008
|
id: "no-jsx-element-type",
|
|
29363
29009
|
title: "No JSX.Element",
|
|
29364
|
-
severity: "
|
|
29010
|
+
severity: "error",
|
|
29365
29011
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29366
29012
|
create: (context) => {
|
|
29367
29013
|
let isJsxImported = false;
|
|
@@ -29687,378 +29333,6 @@ const noLegacyContextApi = defineRule({
|
|
|
29687
29333
|
}
|
|
29688
29334
|
});
|
|
29689
29335
|
//#endregion
|
|
29690
|
-
//#region src/plugin/utils/executes-during-render.ts
|
|
29691
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29692
|
-
"map",
|
|
29693
|
-
"filter",
|
|
29694
|
-
"forEach",
|
|
29695
|
-
"flatMap",
|
|
29696
|
-
"reduce",
|
|
29697
|
-
"reduceRight",
|
|
29698
|
-
"some",
|
|
29699
|
-
"every",
|
|
29700
|
-
"find",
|
|
29701
|
-
"findIndex",
|
|
29702
|
-
"findLast",
|
|
29703
|
-
"findLastIndex",
|
|
29704
|
-
"sort",
|
|
29705
|
-
"toSorted"
|
|
29706
|
-
]);
|
|
29707
|
-
const executesDuringRender = (functionNode) => {
|
|
29708
|
-
const parent = functionNode.parent;
|
|
29709
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29710
|
-
if (parent.callee === functionNode) return true;
|
|
29711
|
-
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29712
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
29713
|
-
};
|
|
29714
|
-
//#endregion
|
|
29715
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29716
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29717
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29718
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29719
|
-
return false;
|
|
29720
|
-
};
|
|
29721
|
-
//#endregion
|
|
29722
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29723
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29724
|
-
let ancestor = bindingIdentifier.parent;
|
|
29725
|
-
while (ancestor) {
|
|
29726
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29727
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29728
|
-
ancestor = ancestor.parent ?? null;
|
|
29729
|
-
}
|
|
29730
|
-
return null;
|
|
29731
|
-
};
|
|
29732
|
-
//#endregion
|
|
29733
|
-
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29734
|
-
const isFalsyLiteral = (node) => {
|
|
29735
|
-
if (!node) return true;
|
|
29736
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29737
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29738
|
-
};
|
|
29739
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
29740
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29741
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29742
|
-
if (!binding) return false;
|
|
29743
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29744
|
-
if (!declarator?.init) return false;
|
|
29745
|
-
const init = stripParenExpression(declarator.init);
|
|
29746
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29747
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29748
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29749
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
29750
|
-
};
|
|
29751
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29752
|
-
//#endregion
|
|
29753
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29754
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
29755
|
-
let cursor = node;
|
|
29756
|
-
let parent = node.parent;
|
|
29757
|
-
while (parent) {
|
|
29758
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29759
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29760
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29761
|
-
cursor = parent;
|
|
29762
|
-
parent = parent.parent ?? null;
|
|
29763
|
-
}
|
|
29764
|
-
return false;
|
|
29765
|
-
};
|
|
29766
|
-
//#endregion
|
|
29767
|
-
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29768
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29769
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
29770
|
-
const unwrapped = stripParenExpression(expression);
|
|
29771
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29772
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29773
|
-
const property = unwrapped.property;
|
|
29774
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29775
|
-
}
|
|
29776
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29777
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29778
|
-
return false;
|
|
29779
|
-
};
|
|
29780
|
-
//#endregion
|
|
29781
|
-
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29782
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
29783
|
-
let cursor = node;
|
|
29784
|
-
let parent = node.parent;
|
|
29785
|
-
while (parent) {
|
|
29786
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29787
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29788
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29789
|
-
cursor = parent;
|
|
29790
|
-
parent = parent.parent ?? null;
|
|
29791
|
-
}
|
|
29792
|
-
return false;
|
|
29793
|
-
};
|
|
29794
|
-
//#endregion
|
|
29795
|
-
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29796
|
-
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29797
|
-
"toLocaleString",
|
|
29798
|
-
"toLocaleDateString",
|
|
29799
|
-
"toLocaleTimeString"
|
|
29800
|
-
]);
|
|
29801
|
-
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29802
|
-
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29803
|
-
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29804
|
-
"format",
|
|
29805
|
-
"formatToParts",
|
|
29806
|
-
"formatRange"
|
|
29807
|
-
]);
|
|
29808
|
-
const isProvableDateExpression = (expression) => {
|
|
29809
|
-
if (!expression) return false;
|
|
29810
|
-
const unwrapped = stripParenExpression(expression);
|
|
29811
|
-
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29812
|
-
};
|
|
29813
|
-
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29814
|
-
const receiverNameLooksDateFlavored = (expression) => {
|
|
29815
|
-
if (!expression) return false;
|
|
29816
|
-
const unwrapped = stripParenExpression(expression);
|
|
29817
|
-
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29818
|
-
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29819
|
-
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29820
|
-
return false;
|
|
29821
|
-
};
|
|
29822
|
-
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29823
|
-
if (!objectExpression) return false;
|
|
29824
|
-
const unwrapped = stripParenExpression(objectExpression);
|
|
29825
|
-
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29826
|
-
for (const property of unwrapped.properties ?? []) {
|
|
29827
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
29828
|
-
if (property.computed) continue;
|
|
29829
|
-
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29830
|
-
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29831
|
-
}
|
|
29832
|
-
return false;
|
|
29833
|
-
};
|
|
29834
|
-
const hasExplicitLocaleArgument = (argument) => {
|
|
29835
|
-
if (!argument) return false;
|
|
29836
|
-
const unwrapped = stripParenExpression(argument);
|
|
29837
|
-
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29838
|
-
return true;
|
|
29839
|
-
};
|
|
29840
|
-
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29841
|
-
const localeArgument = call.arguments?.[0];
|
|
29842
|
-
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29843
|
-
const optionsArgument = call.arguments?.[1];
|
|
29844
|
-
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29845
|
-
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29846
|
-
};
|
|
29847
|
-
const matchLocaleMethodCall = (call) => {
|
|
29848
|
-
const callee = call.callee;
|
|
29849
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29850
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29851
|
-
const methodName = callee.property.name;
|
|
29852
|
-
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29853
|
-
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29854
|
-
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29855
|
-
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29856
|
-
return {
|
|
29857
|
-
node: call,
|
|
29858
|
-
display: `${methodName}()`
|
|
29859
|
-
};
|
|
29860
|
-
};
|
|
29861
|
-
const getIntlFormatterName = (expression) => {
|
|
29862
|
-
if (!expression) return null;
|
|
29863
|
-
const unwrapped = stripParenExpression(expression);
|
|
29864
|
-
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29865
|
-
const callee = unwrapped.callee;
|
|
29866
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29867
|
-
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29868
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29869
|
-
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29870
|
-
};
|
|
29871
|
-
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29872
|
-
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29873
|
-
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29874
|
-
if (formatterName !== "DateTimeFormat") return true;
|
|
29875
|
-
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29876
|
-
};
|
|
29877
|
-
const matchIntlFormatCall = (call) => {
|
|
29878
|
-
const callee = call.callee;
|
|
29879
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29880
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29881
|
-
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29882
|
-
let construction = stripParenExpression(callee.object);
|
|
29883
|
-
if (isNodeOfType(construction, "Identifier")) {
|
|
29884
|
-
const binding = findVariableInitializer(construction, construction.name);
|
|
29885
|
-
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29886
|
-
}
|
|
29887
|
-
if (!construction) return null;
|
|
29888
|
-
const formatterName = getIntlFormatterName(construction);
|
|
29889
|
-
if (!formatterName) return null;
|
|
29890
|
-
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29891
|
-
return {
|
|
29892
|
-
node: call,
|
|
29893
|
-
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29894
|
-
};
|
|
29895
|
-
};
|
|
29896
|
-
const isDeterministicInputDateConstruction = (expression) => {
|
|
29897
|
-
if (!expression) return false;
|
|
29898
|
-
const unwrapped = stripParenExpression(expression);
|
|
29899
|
-
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29900
|
-
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29901
|
-
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29902
|
-
};
|
|
29903
|
-
const matchDateDefaultStringification = (node) => {
|
|
29904
|
-
if (isNodeOfType(node, "CallExpression")) {
|
|
29905
|
-
const callee = node.callee;
|
|
29906
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29907
|
-
node,
|
|
29908
|
-
display: "Date.prototype.toString()"
|
|
29909
|
-
};
|
|
29910
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29911
|
-
node,
|
|
29912
|
-
display: "String(new Date(…))"
|
|
29913
|
-
};
|
|
29914
|
-
return null;
|
|
29915
|
-
}
|
|
29916
|
-
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29917
|
-
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29918
|
-
node: expression,
|
|
29919
|
-
display: "`${new Date(…)}`"
|
|
29920
|
-
};
|
|
29921
|
-
}
|
|
29922
|
-
return null;
|
|
29923
|
-
};
|
|
29924
|
-
const findRenderPhaseComponentOrHook = (node) => {
|
|
29925
|
-
let functionNode = findEnclosingFunction(node);
|
|
29926
|
-
while (functionNode) {
|
|
29927
|
-
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29928
|
-
if (!executesDuringRender(functionNode)) return null;
|
|
29929
|
-
functionNode = findEnclosingFunction(functionNode);
|
|
29930
|
-
}
|
|
29931
|
-
return null;
|
|
29932
|
-
};
|
|
29933
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29934
|
-
if (fileHasUseClientDirective) return true;
|
|
29935
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29936
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
29937
|
-
let callsHook = false;
|
|
29938
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29939
|
-
if (callsHook) return false;
|
|
29940
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29941
|
-
callsHook = true;
|
|
29942
|
-
return false;
|
|
29943
|
-
}
|
|
29944
|
-
});
|
|
29945
|
-
return callsHook;
|
|
29946
|
-
};
|
|
29947
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
29948
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29949
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29950
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
29951
|
-
let cursor = node;
|
|
29952
|
-
while (cursor) {
|
|
29953
|
-
ancestors.add(cursor);
|
|
29954
|
-
cursor = cursor.parent ?? null;
|
|
29955
|
-
}
|
|
29956
|
-
for (const statement of body.body ?? []) {
|
|
29957
|
-
if (ancestors.has(statement)) return false;
|
|
29958
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29959
|
-
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
29960
|
-
let returnsEarly = false;
|
|
29961
|
-
walkAst(statement.consequent, (child) => {
|
|
29962
|
-
if (isFunctionLike$1(child)) return false;
|
|
29963
|
-
if (isNodeOfType(child, "ReturnStatement")) {
|
|
29964
|
-
returnsEarly = true;
|
|
29965
|
-
return false;
|
|
29966
|
-
}
|
|
29967
|
-
});
|
|
29968
|
-
if (returnsEarly) return true;
|
|
29969
|
-
}
|
|
29970
|
-
return false;
|
|
29971
|
-
};
|
|
29972
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
29973
|
-
let cursor = node.parent;
|
|
29974
|
-
while (cursor) {
|
|
29975
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
29976
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
29977
|
-
cursor = cursor.parent ?? null;
|
|
29978
|
-
}
|
|
29979
|
-
return null;
|
|
29980
|
-
};
|
|
29981
|
-
const noLocaleFormatInRender = defineRule({
|
|
29982
|
-
id: "no-locale-format-in-render",
|
|
29983
|
-
title: "Locale/timezone formatting during render",
|
|
29984
|
-
severity: "warn",
|
|
29985
|
-
category: "Correctness",
|
|
29986
|
-
disabledWhen: [
|
|
29987
|
-
"vite",
|
|
29988
|
-
"cra",
|
|
29989
|
-
"expo",
|
|
29990
|
-
"react-native",
|
|
29991
|
-
"unknown"
|
|
29992
|
-
],
|
|
29993
|
-
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
29994
|
-
create: (context) => {
|
|
29995
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
29996
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
29997
|
-
let fileHasUseClientDirective = false;
|
|
29998
|
-
let fileIsEmailTemplate = false;
|
|
29999
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30000
|
-
const reportIfRenderPhase = (match) => {
|
|
30001
|
-
if (reportedNodes.has(match.node)) return;
|
|
30002
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30003
|
-
if (!componentOrHookNode) return;
|
|
30004
|
-
if (fileIsEmailTemplate) return;
|
|
30005
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30006
|
-
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30007
|
-
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30008
|
-
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30009
|
-
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30010
|
-
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30011
|
-
reportedNodes.add(match.node);
|
|
30012
|
-
context.report({
|
|
30013
|
-
node: match.node,
|
|
30014
|
-
message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30015
|
-
});
|
|
30016
|
-
};
|
|
30017
|
-
return {
|
|
30018
|
-
Program(node) {
|
|
30019
|
-
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30020
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30021
|
-
},
|
|
30022
|
-
CallExpression(node) {
|
|
30023
|
-
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30024
|
-
if (match) reportIfRenderPhase(match);
|
|
30025
|
-
},
|
|
30026
|
-
TemplateLiteral(node) {
|
|
30027
|
-
const match = matchDateDefaultStringification(node);
|
|
30028
|
-
if (match) reportIfRenderPhase(match);
|
|
30029
|
-
},
|
|
30030
|
-
JSXExpressionContainer(node) {
|
|
30031
|
-
const expression = stripParenExpression(node.expression);
|
|
30032
|
-
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30033
|
-
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30034
|
-
const helperName = expression.callee.name;
|
|
30035
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30036
|
-
if (!componentOrHookNode) return;
|
|
30037
|
-
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30038
|
-
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30039
|
-
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30040
|
-
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30041
|
-
if (isFunctionLike$1(child)) return false;
|
|
30042
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30043
|
-
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30044
|
-
if (!match || reportedNodes.has(match.node)) return;
|
|
30045
|
-
if (fileIsEmailTemplate) return;
|
|
30046
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30047
|
-
if (isInsideClientOnlyGuard(node)) return;
|
|
30048
|
-
if (isGatedByFalsyInitialState(node)) return;
|
|
30049
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30050
|
-
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30051
|
-
reportedNodes.add(match.node);
|
|
30052
|
-
context.report({
|
|
30053
|
-
node: match.node,
|
|
30054
|
-
message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30055
|
-
});
|
|
30056
|
-
});
|
|
30057
|
-
}
|
|
30058
|
-
};
|
|
30059
|
-
}
|
|
30060
|
-
});
|
|
30061
|
-
//#endregion
|
|
30062
29336
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
30063
29337
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
30064
29338
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30872,6 +30146,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30872
30146
|
"reverse",
|
|
30873
30147
|
"sort"
|
|
30874
30148
|
]);
|
|
30149
|
+
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30875
30150
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30876
30151
|
"assign",
|
|
30877
30152
|
"defineProperties",
|
|
@@ -30945,6 +30220,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30945
30220
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30946
30221
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30947
30222
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30223
|
+
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30948
30224
|
}
|
|
30949
30225
|
}
|
|
30950
30226
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30983,7 +30259,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30983
30259
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30984
30260
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30985
30261
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30986
|
-
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30987
30262
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30988
30263
|
});
|
|
30989
30264
|
return mutations;
|
|
@@ -31216,7 +30491,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
31216
30491
|
id: "no-nested-component-definition",
|
|
31217
30492
|
title: "Component defined inside another component",
|
|
31218
30493
|
tags: ["test-noise", "react-jsx-only"],
|
|
31219
|
-
severity: "
|
|
30494
|
+
severity: "error",
|
|
31220
30495
|
category: "Correctness",
|
|
31221
30496
|
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
31222
30497
|
create: (context) => {
|
|
@@ -32171,12 +31446,12 @@ const noPassDataToParent = defineRule({
|
|
|
32171
31446
|
if (calleeNode === identifier) {
|
|
32172
31447
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
32173
31448
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
32174
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31449
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
|
|
32175
31450
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
32176
31451
|
if (isCustomHookParameter(ref)) continue;
|
|
32177
31452
|
} else continue;
|
|
32178
31453
|
const methodName = getCallMethodName(calleeNode);
|
|
32179
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31454
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32180
31455
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32181
31456
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
32182
31457
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -32367,7 +31642,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
32367
31642
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
32368
31643
|
const calleeNode = callExpr.callee;
|
|
32369
31644
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
32370
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31645
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32371
31646
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32372
31647
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
32373
31648
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32695,6 +31970,40 @@ const noPreventDefault = defineRule({
|
|
|
32695
31970
|
}
|
|
32696
31971
|
});
|
|
32697
31972
|
//#endregion
|
|
31973
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31974
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
31975
|
+
let node = callExpression;
|
|
31976
|
+
let parent = node.parent;
|
|
31977
|
+
while (parent) {
|
|
31978
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31979
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31980
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31981
|
+
node = parent;
|
|
31982
|
+
parent = node.parent;
|
|
31983
|
+
continue;
|
|
31984
|
+
}
|
|
31985
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31986
|
+
node = parent;
|
|
31987
|
+
parent = node.parent;
|
|
31988
|
+
continue;
|
|
31989
|
+
}
|
|
31990
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31991
|
+
node = parent;
|
|
31992
|
+
parent = node.parent;
|
|
31993
|
+
continue;
|
|
31994
|
+
}
|
|
31995
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31996
|
+
const expressions = parent.expressions ?? [];
|
|
31997
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
31998
|
+
node = parent;
|
|
31999
|
+
parent = node.parent;
|
|
32000
|
+
continue;
|
|
32001
|
+
}
|
|
32002
|
+
return false;
|
|
32003
|
+
}
|
|
32004
|
+
return false;
|
|
32005
|
+
};
|
|
32006
|
+
//#endregion
|
|
32698
32007
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32699
32008
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32700
32009
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32901,7 +32210,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32901
32210
|
return `${callee.name}()`;
|
|
32902
32211
|
}
|
|
32903
32212
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32904
|
-
const receiver =
|
|
32213
|
+
const receiver = callee.object;
|
|
32905
32214
|
const property = callee.property;
|
|
32906
32215
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32907
32216
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -33022,10 +32331,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33022
32331
|
if (typeof sourceValue !== "string") return;
|
|
33023
32332
|
if (handleExtraSource?.(node, context)) return;
|
|
33024
32333
|
if (sourceValue !== source) return;
|
|
33025
|
-
if (isTypeOnlyImport(node)) return;
|
|
33026
32334
|
for (const specifier of node.specifiers ?? []) {
|
|
33027
32335
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33028
|
-
if (specifier.importKind === "type") continue;
|
|
33029
32336
|
const importedName = getImportedName$1(specifier);
|
|
33030
32337
|
if (!importedName) continue;
|
|
33031
32338
|
const message = messages.get(importedName);
|
|
@@ -33044,9 +32351,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33044
32351
|
MemberExpression(node) {
|
|
33045
32352
|
if (namespaceBindings.size === 0) return;
|
|
33046
32353
|
if (node.computed) return;
|
|
33047
|
-
|
|
33048
|
-
if (!
|
|
33049
|
-
if (!namespaceBindings.has(receiver.name)) return;
|
|
32354
|
+
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
32355
|
+
if (!namespaceBindings.has(node.object.name)) return;
|
|
33050
32356
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
33051
32357
|
const message = messages.get(node.property.name);
|
|
33052
32358
|
if (message) context.report({
|
|
@@ -33111,7 +32417,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
33111
32417
|
});
|
|
33112
32418
|
//#endregion
|
|
33113
32419
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
33114
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
32420
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
|
|
33115
32421
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
33116
32422
|
if (!rawFilename) return false;
|
|
33117
32423
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -33149,7 +32455,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
33149
32455
|
requires: ["react:19"],
|
|
33150
32456
|
tags: ["test-noise", "migration-hint"],
|
|
33151
32457
|
severity: "warn",
|
|
33152
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32458
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
|
|
33153
32459
|
create: (context) => {
|
|
33154
32460
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
33155
32461
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33473,11 +32779,34 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33473
32779
|
});
|
|
33474
32780
|
//#endregion
|
|
33475
32781
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32782
|
+
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32783
|
+
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32784
|
+
visitedSymbols.add(symbol);
|
|
32785
|
+
if (isComponentParameterSymbol(symbol)) return true;
|
|
32786
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32787
|
+
const source = symbol.initializer;
|
|
32788
|
+
if (!source) return false;
|
|
32789
|
+
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32790
|
+
};
|
|
32791
|
+
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32792
|
+
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32793
|
+
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32794
|
+
return rootsInProps(node, scopes, visitedSymbols);
|
|
32795
|
+
};
|
|
32796
|
+
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32797
|
+
let current = node;
|
|
32798
|
+
while (isNodeOfType(current, "MemberExpression")) {
|
|
32799
|
+
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32800
|
+
current = current.object;
|
|
32801
|
+
}
|
|
32802
|
+
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32803
|
+
return false;
|
|
32804
|
+
};
|
|
33476
32805
|
const isInsideComponentContext = (node) => {
|
|
33477
32806
|
let cursor = node.parent;
|
|
33478
32807
|
while (cursor) {
|
|
32808
|
+
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33479
32809
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33480
|
-
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33481
32810
|
cursor = cursor.parent ?? null;
|
|
33482
32811
|
}
|
|
33483
32812
|
return false;
|
|
@@ -33487,28 +32816,24 @@ const functionBodyOf = (node) => {
|
|
|
33487
32816
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
33488
32817
|
return null;
|
|
33489
32818
|
};
|
|
33490
|
-
const isHookCallee = (callee) => {
|
|
33491
|
-
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33492
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33493
|
-
return false;
|
|
33494
|
-
};
|
|
33495
32819
|
const containsHookCall = (body) => {
|
|
33496
32820
|
let found = false;
|
|
33497
32821
|
walkAst(body, (child) => {
|
|
33498
|
-
if (found) return
|
|
33499
|
-
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32822
|
+
if (found) return;
|
|
33500
32823
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33501
|
-
|
|
32824
|
+
const name = getCalleeName$2(child);
|
|
32825
|
+
if (name && isReactHookName(name)) found = true;
|
|
33502
32826
|
});
|
|
33503
32827
|
return found;
|
|
33504
32828
|
};
|
|
33505
|
-
const
|
|
32829
|
+
const isModuleScopeHookFreeHelper = (symbol) => {
|
|
33506
32830
|
if (!symbol) return false;
|
|
33507
32831
|
const declaration = symbol.declarationNode;
|
|
33508
32832
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33509
32833
|
const body = functionBodyOf(declaration);
|
|
33510
32834
|
if (!body) return false;
|
|
33511
|
-
|
|
32835
|
+
if (findEnclosingFunction(declaration) !== null) return false;
|
|
32836
|
+
return !containsHookCall(body);
|
|
33512
32837
|
};
|
|
33513
32838
|
const noRenderInRender = defineRule({
|
|
33514
32839
|
id: "no-render-in-render",
|
|
@@ -33517,13 +32842,20 @@ const noRenderInRender = defineRule({
|
|
|
33517
32842
|
tags: ["test-noise"],
|
|
33518
32843
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
33519
32844
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33520
|
-
const expression =
|
|
32845
|
+
const expression = node.expression;
|
|
33521
32846
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33522
|
-
|
|
33523
|
-
|
|
33524
|
-
if (
|
|
32847
|
+
let calleeName = null;
|
|
32848
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
32849
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
32850
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33525
32851
|
if (!isInsideComponentContext(node)) return;
|
|
33526
|
-
if (
|
|
32852
|
+
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
32853
|
+
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32854
|
+
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32855
|
+
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32856
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32857
|
+
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32858
|
+
}
|
|
33527
32859
|
context.report({
|
|
33528
32860
|
node: expression,
|
|
33529
32861
|
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
@@ -33584,7 +32916,13 @@ const noRenderPropChildren = defineRule({
|
|
|
33584
32916
|
//#endregion
|
|
33585
32917
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
33586
32918
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
33587
|
-
const isReactDomRenderCall = (node) =>
|
|
32919
|
+
const isReactDomRenderCall = (node) => {
|
|
32920
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32921
|
+
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32922
|
+
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32923
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32924
|
+
return node.callee.property.name === "render";
|
|
32925
|
+
};
|
|
33588
32926
|
const isUsedAsReturnValue = (parent) => {
|
|
33589
32927
|
if (!parent) return false;
|
|
33590
32928
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -34420,7 +33758,7 @@ const noSetState = defineRule({
|
|
|
34420
33758
|
category: "Architecture",
|
|
34421
33759
|
create: (context) => ({ CallExpression(node) {
|
|
34422
33760
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
34423
|
-
if (!isNodeOfType(
|
|
33761
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34424
33762
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
34425
33763
|
if (!getParentComponent(node)) return;
|
|
34426
33764
|
context.report({
|
|
@@ -36866,36 +36204,6 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36866
36204
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36867
36205
|
return true;
|
|
36868
36206
|
};
|
|
36869
|
-
const isTrivialContainerLiteral = (node) => {
|
|
36870
|
-
if (!node) return false;
|
|
36871
|
-
const innerExpression = stripParenExpression(node);
|
|
36872
|
-
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36873
|
-
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36874
|
-
return false;
|
|
36875
|
-
};
|
|
36876
|
-
const isNonEscapingRead = (identifier) => {
|
|
36877
|
-
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36878
|
-
const memberNode = readRoot.parent;
|
|
36879
|
-
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36880
|
-
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36881
|
-
const memberUseParent = memberUse.parent;
|
|
36882
|
-
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36883
|
-
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36884
|
-
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36885
|
-
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36886
|
-
};
|
|
36887
|
-
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36888
|
-
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36889
|
-
const parentNode = memoUsageRoot.parent;
|
|
36890
|
-
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36891
|
-
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36892
|
-
const bindingTarget = parentNode.id;
|
|
36893
|
-
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36894
|
-
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36895
|
-
const symbol = scopes.symbolFor(bindingTarget);
|
|
36896
|
-
if (!symbol) return false;
|
|
36897
|
-
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36898
|
-
};
|
|
36899
36207
|
const noUsememoSimpleExpression = defineRule({
|
|
36900
36208
|
id: "no-usememo-simple-expression",
|
|
36901
36209
|
title: "useMemo on a cheap value",
|
|
@@ -36917,17 +36225,9 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36917
36225
|
let returnExpression = null;
|
|
36918
36226
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36919
36227
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36920
|
-
if (
|
|
36921
|
-
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36922
|
-
context.report({
|
|
36923
|
-
node,
|
|
36924
|
-
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36925
|
-
});
|
|
36926
|
-
return;
|
|
36927
|
-
}
|
|
36928
|
-
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36228
|
+
if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
|
|
36929
36229
|
node,
|
|
36930
|
-
message: "This
|
|
36230
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36931
36231
|
});
|
|
36932
36232
|
} })
|
|
36933
36233
|
});
|
|
@@ -37033,7 +36333,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
37033
36333
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
37034
36334
|
return { CallExpression(node) {
|
|
37035
36335
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
37036
|
-
if (!isNodeOfType(
|
|
36336
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37037
36337
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
37038
36338
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
37039
36339
|
context.report({
|
|
@@ -37389,7 +36689,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
37389
36689
|
continue;
|
|
37390
36690
|
}
|
|
37391
36691
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
37392
|
-
if (
|
|
36692
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37393
36693
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
37394
36694
|
}
|
|
37395
36695
|
return false;
|
|
@@ -37497,22 +36797,18 @@ const onlyExportComponents = defineRule({
|
|
|
37497
36797
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37498
36798
|
allowExportNames: new Set(settings.allowExportNames),
|
|
37499
36799
|
allowConstantExport: settings.allowConstantExport,
|
|
37500
|
-
localComponentNames
|
|
37501
|
-
scopes: context.scopes
|
|
36800
|
+
localComponentNames
|
|
37502
36801
|
};
|
|
37503
36802
|
for (const child of componentCandidates) {
|
|
37504
36803
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37505
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)
|
|
36804
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37506
36805
|
}
|
|
37507
36806
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37508
36807
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37509
36808
|
}
|
|
37510
36809
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37511
36810
|
const initializer = child.init;
|
|
37512
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37513
|
-
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37514
|
-
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37515
|
-
}
|
|
36811
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37516
36812
|
}
|
|
37517
36813
|
}
|
|
37518
36814
|
const exports = [];
|
|
@@ -38472,8 +37768,8 @@ const preferHtmlDialog = defineRule({
|
|
|
38472
37768
|
if (!HTML_TAGS.has(tagName)) return;
|
|
38473
37769
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
38474
37770
|
if (roleAttribute) {
|
|
38475
|
-
const
|
|
38476
|
-
if (
|
|
37771
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
37772
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
38477
37773
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
38478
37774
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
38479
37775
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -39177,22 +38473,11 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
39177
38473
|
}
|
|
39178
38474
|
return null;
|
|
39179
38475
|
};
|
|
39180
|
-
const isTrivialLiteralExpression = (expression) => {
|
|
39181
|
-
if (isNodeOfType(expression, "Literal")) return true;
|
|
39182
|
-
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39183
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39184
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39185
|
-
return false;
|
|
39186
|
-
};
|
|
39187
38476
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
39188
38477
|
const handlerStatements = getCallbackStatements(handler);
|
|
39189
38478
|
if (handlerStatements.length !== 1) return null;
|
|
39190
38479
|
const onlyStatement = handlerStatements[0];
|
|
39191
|
-
|
|
39192
|
-
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39193
|
-
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39194
|
-
if (!expression) return null;
|
|
39195
|
-
expression = stripParenExpression(expression);
|
|
38480
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
39196
38481
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
39197
38482
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
39198
38483
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -39202,106 +38487,6 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
39202
38487
|
setterArgument: expression.arguments[0]
|
|
39203
38488
|
};
|
|
39204
38489
|
};
|
|
39205
|
-
const isListenerCollectionInitializer = (init) => {
|
|
39206
|
-
if (!init) return false;
|
|
39207
|
-
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39208
|
-
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39209
|
-
};
|
|
39210
|
-
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39211
|
-
if (!isFunctionLike$1(functionNode)) return false;
|
|
39212
|
-
const firstParam = functionNode.params?.[0];
|
|
39213
|
-
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39214
|
-
const listenerParamName = firstParam.name;
|
|
39215
|
-
let registersListener = false;
|
|
39216
|
-
walkAst(functionNode.body, (child) => {
|
|
39217
|
-
if (registersListener) return false;
|
|
39218
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39219
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39220
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39221
|
-
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39222
|
-
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39223
|
-
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39224
|
-
const registeredArgument = child.arguments?.[0];
|
|
39225
|
-
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39226
|
-
});
|
|
39227
|
-
return registersListener;
|
|
39228
|
-
};
|
|
39229
|
-
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39230
|
-
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39231
|
-
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39232
|
-
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39233
|
-
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39234
|
-
mutableBindingNames,
|
|
39235
|
-
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39236
|
-
};
|
|
39237
|
-
for (const statement of programRoot.body ?? []) {
|
|
39238
|
-
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39239
|
-
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39240
|
-
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39241
|
-
continue;
|
|
39242
|
-
}
|
|
39243
|
-
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39244
|
-
for (const declarator of unwrapped.declarations ?? []) {
|
|
39245
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39246
|
-
const init = declarator.init ?? null;
|
|
39247
|
-
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39248
|
-
mutableBindingNames.add(declarator.id.name);
|
|
39249
|
-
continue;
|
|
39250
|
-
}
|
|
39251
|
-
if (isListenerCollectionInitializer(init)) {
|
|
39252
|
-
listenerCollectionNames.add(declarator.id.name);
|
|
39253
|
-
continue;
|
|
39254
|
-
}
|
|
39255
|
-
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39256
|
-
}
|
|
39257
|
-
}
|
|
39258
|
-
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39259
|
-
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39260
|
-
return {
|
|
39261
|
-
mutableBindingNames,
|
|
39262
|
-
subscribeFunctionNames
|
|
39263
|
-
};
|
|
39264
|
-
};
|
|
39265
|
-
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39266
|
-
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39267
|
-
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39268
|
-
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39269
|
-
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39270
|
-
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39271
|
-
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39272
|
-
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39273
|
-
return initialArgument.name;
|
|
39274
|
-
};
|
|
39275
|
-
const argumentForwardsSetter = (argument, setterName) => {
|
|
39276
|
-
if (!argument) return false;
|
|
39277
|
-
const unwrapped = stripParenExpression(argument);
|
|
39278
|
-
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39279
|
-
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39280
|
-
let callsSetter = false;
|
|
39281
|
-
walkAst(unwrapped.body, (child) => {
|
|
39282
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39283
|
-
callsSetter = true;
|
|
39284
|
-
return false;
|
|
39285
|
-
}
|
|
39286
|
-
});
|
|
39287
|
-
return callsSetter;
|
|
39288
|
-
};
|
|
39289
|
-
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39290
|
-
let matchedCall = null;
|
|
39291
|
-
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39292
|
-
if (matchedCall) return false;
|
|
39293
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39294
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39295
|
-
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39296
|
-
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39297
|
-
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39298
|
-
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39299
|
-
matchedCall = child;
|
|
39300
|
-
return false;
|
|
39301
|
-
}
|
|
39302
|
-
});
|
|
39303
|
-
return matchedCall;
|
|
39304
|
-
};
|
|
39305
38490
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
39306
38491
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
39307
38492
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -39318,16 +38503,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39318
38503
|
severity: "warn",
|
|
39319
38504
|
recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
|
|
39320
38505
|
create: (context) => {
|
|
39321
|
-
let cachedStoreIndex = null;
|
|
39322
|
-
const storeIndexFor = (node) => {
|
|
39323
|
-
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39324
|
-
const programRoot = findProgramRoot(node);
|
|
39325
|
-
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39326
|
-
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39327
|
-
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39328
|
-
};
|
|
39329
|
-
return cachedStoreIndex;
|
|
39330
|
-
};
|
|
39331
38506
|
const checkComponent = (componentBody) => {
|
|
39332
38507
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
39333
38508
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -39365,7 +38540,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39365
38540
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
39366
38541
|
if (!useStateInitializer) continue;
|
|
39367
38542
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39368
|
-
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
39369
38543
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
39370
38544
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
39371
38545
|
context.report({
|
|
@@ -39373,47 +38547,14 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39373
38547
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
39374
38548
|
});
|
|
39375
38549
|
}
|
|
39376
|
-
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39377
|
-
};
|
|
39378
|
-
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39379
|
-
const storeIndex = storeIndexFor(componentBody);
|
|
39380
|
-
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39381
|
-
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39382
|
-
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39383
|
-
binding,
|
|
39384
|
-
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39385
|
-
})).filter((candidate) => candidate.storeName !== null);
|
|
39386
|
-
if (snapshotBindings.length === 0) return;
|
|
39387
|
-
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39388
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39389
|
-
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39390
|
-
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39391
|
-
const depsNode = effectCall.arguments[1];
|
|
39392
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39393
|
-
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39394
|
-
const callback = getEffectCallback(effectCall);
|
|
39395
|
-
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39396
|
-
for (const { binding, storeName } of snapshotBindings) {
|
|
39397
|
-
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39398
|
-
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39399
|
-
reportedDeclarators.add(binding.declarator);
|
|
39400
|
-
context.report({
|
|
39401
|
-
node: binding.declarator,
|
|
39402
|
-
message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
|
|
39403
|
-
});
|
|
39404
|
-
}
|
|
39405
|
-
}
|
|
39406
38550
|
};
|
|
39407
38551
|
return {
|
|
39408
38552
|
FunctionDeclaration(node) {
|
|
39409
|
-
|
|
39410
|
-
if (!functionName) return;
|
|
39411
|
-
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38553
|
+
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
39412
38554
|
checkComponent(node.body);
|
|
39413
38555
|
},
|
|
39414
38556
|
VariableDeclarator(node) {
|
|
39415
|
-
|
|
39416
|
-
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38557
|
+
if (!isComponentAssignment(node)) return;
|
|
39417
38558
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
39418
38559
|
checkComponent(node.init.body);
|
|
39419
38560
|
}
|
|
@@ -40023,7 +39164,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
40023
39164
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
40024
39165
|
const body = queryFnValue.body;
|
|
40025
39166
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
40026
|
-
if ((body.body ?? []).
|
|
39167
|
+
if ((body.body ?? []).length === 0) context.report({
|
|
40027
39168
|
node: queryFnProperty,
|
|
40028
39169
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
40029
39170
|
});
|
|
@@ -40530,6 +39671,17 @@ const renderingHoistJsx = defineRule({
|
|
|
40530
39671
|
}
|
|
40531
39672
|
});
|
|
40532
39673
|
//#endregion
|
|
39674
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39675
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39676
|
+
let ancestor = bindingIdentifier.parent;
|
|
39677
|
+
while (ancestor) {
|
|
39678
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39679
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39680
|
+
ancestor = ancestor.parent ?? null;
|
|
39681
|
+
}
|
|
39682
|
+
return null;
|
|
39683
|
+
};
|
|
39684
|
+
//#endregion
|
|
40533
39685
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
40534
39686
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
40535
39687
|
{
|
|
@@ -40538,19 +39690,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
40538
39690
|
},
|
|
40539
39691
|
{
|
|
40540
39692
|
display: "Date.now()",
|
|
40541
|
-
matches: (node) =>
|
|
39693
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
|
|
40542
39694
|
},
|
|
40543
39695
|
{
|
|
40544
39696
|
display: "Math.random()",
|
|
40545
|
-
matches: (node) =>
|
|
39697
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
|
|
40546
39698
|
},
|
|
40547
39699
|
{
|
|
40548
39700
|
display: "performance.now()",
|
|
40549
|
-
matches: (node) =>
|
|
39701
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
|
|
40550
39702
|
},
|
|
40551
39703
|
{
|
|
40552
39704
|
display: "crypto.randomUUID()",
|
|
40553
|
-
matches: (node) =>
|
|
39705
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
|
|
40554
39706
|
}
|
|
40555
39707
|
];
|
|
40556
39708
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -40562,6 +39714,54 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
40562
39714
|
}
|
|
40563
39715
|
return null;
|
|
40564
39716
|
};
|
|
39717
|
+
const executesDuringRender = (functionNode) => {
|
|
39718
|
+
const parent = functionNode.parent;
|
|
39719
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39720
|
+
if (parent.callee === functionNode) return true;
|
|
39721
|
+
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39722
|
+
};
|
|
39723
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39724
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
39725
|
+
const unwrapped = stripParenExpression(expression);
|
|
39726
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39727
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39728
|
+
const property = unwrapped.property;
|
|
39729
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39730
|
+
}
|
|
39731
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39732
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39733
|
+
return false;
|
|
39734
|
+
};
|
|
39735
|
+
const isFalsyLiteral = (node) => {
|
|
39736
|
+
if (!node) return true;
|
|
39737
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39738
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39739
|
+
};
|
|
39740
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
39741
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39742
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39743
|
+
if (!binding) return false;
|
|
39744
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39745
|
+
if (!declarator?.init) return false;
|
|
39746
|
+
const init = stripParenExpression(declarator.init);
|
|
39747
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39748
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39749
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39750
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
39751
|
+
};
|
|
39752
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39753
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
39754
|
+
let cursor = node;
|
|
39755
|
+
let parent = node.parent;
|
|
39756
|
+
while (parent) {
|
|
39757
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39758
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39759
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39760
|
+
cursor = parent;
|
|
39761
|
+
parent = parent.parent ?? null;
|
|
39762
|
+
}
|
|
39763
|
+
return false;
|
|
39764
|
+
};
|
|
40565
39765
|
const isYearOnlyDateRead = (dateNode) => {
|
|
40566
39766
|
const member = dateNode.parent;
|
|
40567
39767
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -40585,6 +39785,23 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
40585
39785
|
}
|
|
40586
39786
|
return false;
|
|
40587
39787
|
};
|
|
39788
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
39789
|
+
let cursor = node;
|
|
39790
|
+
let parent = node.parent;
|
|
39791
|
+
while (parent) {
|
|
39792
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39793
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39794
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39795
|
+
cursor = parent;
|
|
39796
|
+
parent = parent.parent ?? null;
|
|
39797
|
+
}
|
|
39798
|
+
return false;
|
|
39799
|
+
};
|
|
39800
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39801
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39802
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39803
|
+
return false;
|
|
39804
|
+
};
|
|
40588
39805
|
const renderingHydrationMismatchTime = defineRule({
|
|
40589
39806
|
id: "rendering-hydration-mismatch-time",
|
|
40590
39807
|
title: "Time or random value in JSX",
|
|
@@ -40632,35 +39849,6 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
40632
39849
|
}
|
|
40633
39850
|
});
|
|
40634
39851
|
//#endregion
|
|
40635
|
-
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40636
|
-
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40637
|
-
"toLocaleString",
|
|
40638
|
-
"toLocaleDateString",
|
|
40639
|
-
"toLocaleTimeString",
|
|
40640
|
-
"getTimezoneOffset"
|
|
40641
|
-
]);
|
|
40642
|
-
const containsLocaleEnvironmentRead = (expression) => {
|
|
40643
|
-
let readsLocaleEnvironment = false;
|
|
40644
|
-
walkAst(expression, (child) => {
|
|
40645
|
-
if (readsLocaleEnvironment) return false;
|
|
40646
|
-
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40647
|
-
if (child.object.name === "Intl") {
|
|
40648
|
-
readsLocaleEnvironment = true;
|
|
40649
|
-
return false;
|
|
40650
|
-
}
|
|
40651
|
-
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40652
|
-
readsLocaleEnvironment = true;
|
|
40653
|
-
return false;
|
|
40654
|
-
}
|
|
40655
|
-
}
|
|
40656
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
|
|
40657
|
-
readsLocaleEnvironment = true;
|
|
40658
|
-
return false;
|
|
40659
|
-
}
|
|
40660
|
-
});
|
|
40661
|
-
return readsLocaleEnvironment;
|
|
40662
|
-
};
|
|
40663
|
-
//#endregion
|
|
40664
39852
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
40665
39853
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
40666
39854
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -40726,15 +39914,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
40726
39914
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
40727
39915
|
const callback = getEffectCallback(node);
|
|
40728
39916
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
40729
|
-
const bodyStatements =
|
|
40730
|
-
if (bodyStatements.length !== 1) return;
|
|
39917
|
+
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39918
|
+
if (!bodyStatements || bodyStatements.length !== 1) return;
|
|
40731
39919
|
const soleStatement = bodyStatements[0];
|
|
40732
39920
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
40733
39921
|
const expression = soleStatement.expression;
|
|
40734
39922
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
40735
39923
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
40736
39924
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40737
|
-
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
40738
39925
|
context.report({
|
|
40739
39926
|
node,
|
|
40740
39927
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -41553,9 +40740,6 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
41553
40740
|
} })
|
|
41554
40741
|
});
|
|
41555
40742
|
//#endregion
|
|
41556
|
-
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41557
|
-
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41558
|
-
//#endregion
|
|
41559
40743
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
41560
40744
|
const rerenderLazyRefInit = defineRule({
|
|
41561
40745
|
id: "rerender-lazy-ref-init",
|
|
@@ -41566,7 +40750,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41566
40750
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
41567
40751
|
create: (context) => ({ CallExpression(node) {
|
|
41568
40752
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
41569
|
-
const initializer =
|
|
40753
|
+
const initializer = node.arguments[0];
|
|
41570
40754
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
41571
40755
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
41572
40756
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -41574,7 +40758,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41574
40758
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41575
40759
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41576
40760
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41577
|
-
if (
|
|
40761
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
41578
40762
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
41579
40763
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
41580
40764
|
context.report({
|
|
@@ -41610,11 +40794,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
41610
40794
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
41611
40795
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
41612
40796
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
41613
|
-
|
|
41614
|
-
if (isNodeOfType(
|
|
41615
|
-
if (isNodeOfType(
|
|
41616
|
-
if (isNodeOfType(
|
|
41617
|
-
if (isNodeOfType(
|
|
40797
|
+
if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
|
|
40798
|
+
if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
|
|
40799
|
+
if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
|
|
40800
|
+
if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
|
|
40801
|
+
if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
|
|
41618
40802
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
41619
40803
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
41620
40804
|
if (spreadCall) return spreadCall;
|
|
@@ -41637,7 +40821,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
41637
40821
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41638
40822
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41639
40823
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41640
|
-
if (
|
|
40824
|
+
if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
41641
40825
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
41642
40826
|
if (isReactHookName(calleeName)) return;
|
|
41643
40827
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -42304,7 +41488,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
42304
41488
|
const body = reactionFn.body;
|
|
42305
41489
|
let singleAssignment = null;
|
|
42306
41490
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
42307
|
-
const statements =
|
|
41491
|
+
const statements = body.body ?? [];
|
|
42308
41492
|
if (statements.length !== 1) return;
|
|
42309
41493
|
const onlyStatement = statements[0];
|
|
42310
41494
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -42378,8 +41562,7 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
42378
41562
|
"catch",
|
|
42379
41563
|
"finally"
|
|
42380
41564
|
]);
|
|
42381
|
-
const findChainRoot = (
|
|
42382
|
-
const node = stripParenExpression(wrappedNode);
|
|
41565
|
+
const findChainRoot = (node) => {
|
|
42383
41566
|
if (isNodeOfType(node, "CallExpression")) {
|
|
42384
41567
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
42385
41568
|
calleeName: node.callee.name,
|
|
@@ -42967,21 +42150,20 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42967
42150
|
};
|
|
42968
42151
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42969
42152
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
const localName = receiver.name;
|
|
42153
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
42154
|
+
const localName = callee.object.name;
|
|
42973
42155
|
const binding = findVariableInitializer(node, localName);
|
|
42974
42156
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42975
42157
|
return localName === "Dimensions";
|
|
42976
42158
|
}
|
|
42977
|
-
if (isNodeOfType(
|
|
42978
|
-
if (getInitializerModuleSource(node,
|
|
42979
|
-
const rootName = getRootIdentifierName(
|
|
42159
|
+
if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
|
|
42160
|
+
if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
|
|
42161
|
+
const rootName = getRootIdentifierName(callee.object.object);
|
|
42980
42162
|
if (rootName === null) return false;
|
|
42981
42163
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42982
42164
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42983
42165
|
}
|
|
42984
|
-
if (getRequireCallSource(
|
|
42166
|
+
if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
|
|
42985
42167
|
return false;
|
|
42986
42168
|
};
|
|
42987
42169
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -43425,8 +42607,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
43425
42607
|
},
|
|
43426
42608
|
CallExpression(node) {
|
|
43427
42609
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
43428
|
-
|
|
43429
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42610
|
+
if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
|
|
43430
42611
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
43431
42612
|
const stylesArgument = node.arguments?.[0];
|
|
43432
42613
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -44276,7 +43457,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
44276
43457
|
const callee = node.callee;
|
|
44277
43458
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
44278
43459
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
44279
|
-
if (!isStaticMemberNamed(
|
|
43460
|
+
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44280
43461
|
context.report({
|
|
44281
43462
|
node,
|
|
44282
43463
|
message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
|
|
@@ -44504,15 +43685,14 @@ const analyzeGestureChain = (expression) => {
|
|
|
44504
43685
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
44505
43686
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
44506
43687
|
const methodName = callee.property.name;
|
|
44507
|
-
|
|
44508
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43688
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
44509
43689
|
factoryName: methodName,
|
|
44510
43690
|
chainMethodNames,
|
|
44511
43691
|
numberOfTapsArgument
|
|
44512
43692
|
};
|
|
44513
43693
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
44514
43694
|
chainMethodNames.push(methodName);
|
|
44515
|
-
cursor =
|
|
43695
|
+
cursor = callee.object;
|
|
44516
43696
|
}
|
|
44517
43697
|
return null;
|
|
44518
43698
|
};
|
|
@@ -45037,9 +44217,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
45037
44217
|
if (!HTML_TAGS.has(elementType)) return;
|
|
45038
44218
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
45039
44219
|
if (!roleAttribute) return;
|
|
45040
|
-
const
|
|
45041
|
-
if (
|
|
45042
|
-
const roles =
|
|
44220
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
44221
|
+
if (roleValue === null) return;
|
|
44222
|
+
const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
|
|
45043
44223
|
for (const role of roles) {
|
|
45044
44224
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
45045
44225
|
if (!required) continue;
|
|
@@ -48156,9 +47336,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
48156
47336
|
};
|
|
48157
47337
|
//#endregion
|
|
48158
47338
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
48159
|
-
const buildMessageDefault = (
|
|
48160
|
-
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
48161
|
-
};
|
|
47339
|
+
const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
|
|
48162
47340
|
const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
|
|
48163
47341
|
const roleSupportsAriaProps = defineRule({
|
|
48164
47342
|
id: "role-supports-aria-props",
|
|
@@ -48178,8 +47356,6 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48178
47356
|
const propName = propRawName.toLowerCase();
|
|
48179
47357
|
if (!propName.startsWith("aria-")) continue;
|
|
48180
47358
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48181
|
-
const attributeValue = attributeNode.value;
|
|
48182
|
-
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
48183
47359
|
(ariaAttributes ??= []).push({
|
|
48184
47360
|
attribute,
|
|
48185
47361
|
propName
|
|
@@ -48188,21 +47364,17 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48188
47364
|
if (!ariaAttributes) return;
|
|
48189
47365
|
const elementType = getElementType(node, context.settings);
|
|
48190
47366
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
48191
|
-
const
|
|
48192
|
-
if (
|
|
48193
|
-
|
|
48194
|
-
for (const role of roleCandidates) {
|
|
48195
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48196
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
48197
|
-
if (!supported) return;
|
|
48198
|
-
supportedSets.push(supported);
|
|
48199
|
-
}
|
|
47367
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
47368
|
+
if (!role) return;
|
|
47369
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48200
47370
|
const isImplicit = !roleAttribute;
|
|
47371
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47372
|
+
if (!supported) return;
|
|
48201
47373
|
for (const { attribute, propName } of ariaAttributes) {
|
|
48202
|
-
if (
|
|
47374
|
+
if (supported.has(propName)) continue;
|
|
48203
47375
|
context.report({
|
|
48204
47376
|
node: attribute,
|
|
48205
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47377
|
+
message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
|
|
48206
47378
|
});
|
|
48207
47379
|
}
|
|
48208
47380
|
} })
|
|
@@ -48558,11 +47730,7 @@ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
|
48558
47730
|
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
48559
47731
|
return Boolean(symbol && symbol.kind !== "import");
|
|
48560
47732
|
};
|
|
48561
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) =>
|
|
48562
|
-
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
48563
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
48564
|
-
return false;
|
|
48565
|
-
};
|
|
47733
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
48566
47734
|
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
48567
47735
|
const initializer = symbol.initializer;
|
|
48568
47736
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
@@ -48906,8 +48074,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
48906
48074
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48907
48075
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48908
48076
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48909
|
-
const
|
|
48910
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48077
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
48911
48078
|
if (!objectName) return;
|
|
48912
48079
|
const methodName = node.callee.property.name;
|
|
48913
48080
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -49583,17 +48750,7 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
49583
48750
|
};
|
|
49584
48751
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
49585
48752
|
let chainTip = referenceIdentifier;
|
|
49586
|
-
while (chainTip.parent)
|
|
49587
|
-
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49588
|
-
chainTip = chainTip.parent;
|
|
49589
|
-
continue;
|
|
49590
|
-
}
|
|
49591
|
-
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49592
|
-
chainTip = chainTip.parent;
|
|
49593
|
-
continue;
|
|
49594
|
-
}
|
|
49595
|
-
break;
|
|
49596
|
-
}
|
|
48753
|
+
while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
|
|
49597
48754
|
return chainTip;
|
|
49598
48755
|
};
|
|
49599
48756
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -49618,8 +48775,7 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
49618
48775
|
const callee = callExpression.callee;
|
|
49619
48776
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
49620
48777
|
const methodName = getMemberPropertyName$1(callee);
|
|
49621
|
-
|
|
49622
|
-
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48778
|
+
return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
49623
48779
|
}
|
|
49624
48780
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
49625
48781
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -50349,7 +49505,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50349
49505
|
};
|
|
50350
49506
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
50351
49507
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
50352
|
-
let currentNode =
|
|
49508
|
+
let currentNode = outerNode.callee.object;
|
|
50353
49509
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
50354
49510
|
const calleeName = getCalleeName$2(currentNode);
|
|
50355
49511
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -50360,7 +49516,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50360
49516
|
}
|
|
50361
49517
|
}
|
|
50362
49518
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
50363
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode =
|
|
49519
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50364
49520
|
else break;
|
|
50365
49521
|
}
|
|
50366
49522
|
return result;
|
|
@@ -51013,7 +50169,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
51013
50169
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
51014
50170
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
51015
50171
|
if (methodName) methodNames.unshift(methodName);
|
|
51016
|
-
currentNode =
|
|
50172
|
+
currentNode = currentNode.callee.object;
|
|
51017
50173
|
}
|
|
51018
50174
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
51019
50175
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -54095,18 +53251,6 @@ const reactDoctorRules = [
|
|
|
54095
53251
|
category: "Bugs"
|
|
54096
53252
|
}
|
|
54097
53253
|
},
|
|
54098
|
-
{
|
|
54099
|
-
key: "react-doctor/no-locale-format-in-render",
|
|
54100
|
-
id: "no-locale-format-in-render",
|
|
54101
|
-
source: "react-doctor",
|
|
54102
|
-
originallyExternal: false,
|
|
54103
|
-
rule: {
|
|
54104
|
-
...noLocaleFormatInRender,
|
|
54105
|
-
framework: "global",
|
|
54106
|
-
category: "Bugs",
|
|
54107
|
-
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54108
|
-
}
|
|
54109
|
-
},
|
|
54110
53254
|
{
|
|
54111
53255
|
key: "react-doctor/no-long-transition-duration",
|
|
54112
53256
|
id: "no-long-transition-duration",
|