oxlint-plugin-react-doctor 0.7.2-dev.f10f9ca → 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 +485 -1408
- 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;
|
|
@@ -8987,8 +8671,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8987
8671
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8988
8672
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8989
8673
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8990
|
-
* `symbolHasStableHookOrigin` / `
|
|
8991
|
-
* multiple sites — extracting
|
|
8674
|
+
* `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
|
|
8675
|
+
* `isRecursiveInitializerCapture` at multiple sites — extracting
|
|
8992
8676
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8993
8677
|
* declared logic.
|
|
8994
8678
|
*
|
|
@@ -9044,6 +8728,11 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
9044
8728
|
}
|
|
9045
8729
|
return false;
|
|
9046
8730
|
};
|
|
8731
|
+
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
8732
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8733
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8734
|
+
return getHookName(initializer.callee) === "useEffectEvent";
|
|
8735
|
+
};
|
|
9047
8736
|
const getFunctionValueNode = (symbol) => {
|
|
9048
8737
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
9049
8738
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -9129,37 +8818,6 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9129
8818
|
};
|
|
9130
8819
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9131
8820
|
//#endregion
|
|
9132
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9133
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9134
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9135
|
-
if (importSource === null) return false;
|
|
9136
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9137
|
-
};
|
|
9138
|
-
//#endregion
|
|
9139
|
-
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9140
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9141
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9142
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
9143
|
-
};
|
|
9144
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9145
|
-
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9146
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9147
|
-
return false;
|
|
9148
|
-
};
|
|
9149
|
-
//#endregion
|
|
9150
|
-
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9151
|
-
const getUseEffectEventCalleeName = (callee) => {
|
|
9152
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9153
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9154
|
-
return null;
|
|
9155
|
-
};
|
|
9156
|
-
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9157
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9158
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9159
|
-
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9160
|
-
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9161
|
-
};
|
|
9162
|
-
//#endregion
|
|
9163
8821
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
9164
8822
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
9165
8823
|
"useEffect",
|
|
@@ -9810,7 +9468,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9810
9468
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9811
9469
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9812
9470
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9813
|
-
if (depSymbol &&
|
|
9471
|
+
if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
|
|
9814
9472
|
context.report({
|
|
9815
9473
|
node: elementNode,
|
|
9816
9474
|
message: buildEffectEventDepMessage()
|
|
@@ -10236,10 +9894,7 @@ const PRAGMA = "React";
|
|
|
10236
9894
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
10237
9895
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
10238
9896
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
10239
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10240
|
-
const receiver = stripParenExpression(node.callee.object);
|
|
10241
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10242
|
-
}
|
|
9897
|
+
if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
|
|
10243
9898
|
return true;
|
|
10244
9899
|
};
|
|
10245
9900
|
//#endregion
|
|
@@ -11404,8 +11059,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11404
11059
|
if (node.attributes.length === 0) return;
|
|
11405
11060
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11406
11061
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11407
|
-
const
|
|
11408
|
-
if (
|
|
11062
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
11063
|
+
if (!role) return;
|
|
11409
11064
|
let hasInteractiveHandler = false;
|
|
11410
11065
|
for (const attribute of node.attributes) {
|
|
11411
11066
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11419,16 +11074,11 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11419
11074
|
const elementType = getElementType(node, context.settings);
|
|
11420
11075
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11421
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;
|
|
11422
11079
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11426
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11427
|
-
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11428
|
-
}
|
|
11429
|
-
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11430
|
-
const roleDisplay = roleCandidates.join("' / '");
|
|
11431
|
-
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);
|
|
11432
11082
|
context.report({
|
|
11433
11083
|
node,
|
|
11434
11084
|
message
|
|
@@ -11617,6 +11267,16 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11617
11267
|
return names;
|
|
11618
11268
|
};
|
|
11619
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
|
|
11620
11280
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11621
11281
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11622
11282
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12316,15 +11976,14 @@ const jsCacheStorage = defineRule({
|
|
|
12316
11976
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12317
11977
|
CallExpression(node) {
|
|
12318
11978
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12319
|
-
|
|
12320
|
-
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;
|
|
12321
11980
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12322
11981
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12323
11982
|
const storageKey = String(node.arguments[0].value);
|
|
12324
11983
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12325
11984
|
storageReadCounts.set(storageKey, readCount);
|
|
12326
11985
|
if (readCount === 2) {
|
|
12327
|
-
const storageName =
|
|
11986
|
+
const storageName = node.callee.object.name;
|
|
12328
11987
|
context.report({
|
|
12329
11988
|
node,
|
|
12330
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`
|
|
@@ -12338,16 +11997,13 @@ const jsCacheStorage = defineRule({
|
|
|
12338
11997
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12339
11998
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12340
11999
|
const callee = callExpression.callee;
|
|
12341
|
-
if (isNodeOfType(callee, "MemberExpression"))
|
|
12342
|
-
const receiver = stripParenExpression(callee.object);
|
|
12343
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12344
|
-
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12345
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12346
|
-
return true;
|
|
12347
|
-
}
|
|
12348
|
-
return false;
|
|
12349
|
-
}
|
|
12000
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12350
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
|
+
}
|
|
12351
12007
|
return false;
|
|
12352
12008
|
};
|
|
12353
12009
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12359,7 +12015,10 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12359
12015
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12360
12016
|
let cursor = receiverNode;
|
|
12361
12017
|
while (cursor) {
|
|
12362
|
-
|
|
12018
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12019
|
+
cursor = cursor.expression;
|
|
12020
|
+
continue;
|
|
12021
|
+
}
|
|
12363
12022
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12364
12023
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12365
12024
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12435,7 +12094,10 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12435
12094
|
let hops = 0;
|
|
12436
12095
|
while (cursor && hops < 12) {
|
|
12437
12096
|
hops += 1;
|
|
12438
|
-
|
|
12097
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12098
|
+
cursor = cursor.expression;
|
|
12099
|
+
continue;
|
|
12100
|
+
}
|
|
12439
12101
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12440
12102
|
const callee = cursor.callee;
|
|
12441
12103
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12459,7 +12121,10 @@ const isSmallLiteralArray = (node) => {
|
|
|
12459
12121
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12460
12122
|
let cursor = receiverNode;
|
|
12461
12123
|
while (cursor) {
|
|
12462
|
-
|
|
12124
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12125
|
+
cursor = cursor.expression;
|
|
12126
|
+
continue;
|
|
12127
|
+
}
|
|
12463
12128
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12464
12129
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12465
12130
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12524,7 +12189,7 @@ const jsCombineIterations = defineRule({
|
|
|
12524
12189
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12525
12190
|
const outerMethod = node.callee.property.name;
|
|
12526
12191
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12527
|
-
const innerCall =
|
|
12192
|
+
const innerCall = node.callee.object;
|
|
12528
12193
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12529
12194
|
const innerMethod = innerCall.callee.property.name;
|
|
12530
12195
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12597,10 +12262,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12597
12262
|
if (!filterArgument) return;
|
|
12598
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;
|
|
12599
12264
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12600
|
-
const innerCall =
|
|
12265
|
+
const innerCall = node.callee.object;
|
|
12601
12266
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12602
12267
|
if (innerCall.callee.property.name !== "map") return;
|
|
12603
|
-
const receiver =
|
|
12268
|
+
const receiver = innerCall.callee.object;
|
|
12604
12269
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12605
12270
|
const elements = receiver.elements ?? [];
|
|
12606
12271
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13860,7 +13525,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13860
13525
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13861
13526
|
create: (context) => ({ CallExpression(node) {
|
|
13862
13527
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13863
|
-
const receiver =
|
|
13528
|
+
const receiver = node.callee.object;
|
|
13864
13529
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13865
13530
|
const spreadArgument = receiver.elements[0].argument;
|
|
13866
13531
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18897,8 +18562,7 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18897
18562
|
};
|
|
18898
18563
|
const describeClientSideNavigation = (node) => {
|
|
18899
18564
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18900
|
-
const
|
|
18901
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18565
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
18902
18566
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18903
18567
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18904
18568
|
}
|
|
@@ -19518,7 +19182,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19518
19182
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19519
19183
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19520
19184
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19521
|
-
if (!isCookieReceiver(
|
|
19185
|
+
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19522
19186
|
return node.callee.property.name;
|
|
19523
19187
|
};
|
|
19524
19188
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19532,7 +19196,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19532
19196
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19533
19197
|
const { property, object } = node.callee;
|
|
19534
19198
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19535
|
-
if (isSafeReceiverChain(
|
|
19199
|
+
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19536
19200
|
return true;
|
|
19537
19201
|
};
|
|
19538
19202
|
const getDbCallDescription = (node) => {
|
|
@@ -19540,8 +19204,7 @@ const getDbCallDescription = (node) => {
|
|
|
19540
19204
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19541
19205
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19542
19206
|
const methodName = node.callee.property.name;
|
|
19543
|
-
const
|
|
19544
|
-
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19207
|
+
const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
19545
19208
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19546
19209
|
};
|
|
19547
19210
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20941,13 +20604,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20941
20604
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20942
20605
|
let node = ref.identifier;
|
|
20943
20606
|
let parent = node.parent;
|
|
20944
|
-
while (parent &&
|
|
20607
|
+
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20945
20608
|
node = parent;
|
|
20946
20609
|
parent = node.parent;
|
|
20947
20610
|
}
|
|
20948
20611
|
if (current.callee === node) return current;
|
|
20949
20612
|
}
|
|
20950
|
-
if (isNodeOfType(current, "MemberExpression")
|
|
20613
|
+
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20951
20614
|
return null;
|
|
20952
20615
|
};
|
|
20953
20616
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -21082,7 +20745,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
21082
20745
|
const importDeclaration = declarationNode.parent;
|
|
21083
20746
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
21084
20747
|
}));
|
|
21085
|
-
const isHookCallee
|
|
20748
|
+
const isHookCallee = (analysis, node, hookName) => {
|
|
21086
20749
|
if (!node) return false;
|
|
21087
20750
|
if (isNodeOfType(node, "Identifier")) {
|
|
21088
20751
|
if (node.name === hookName) return true;
|
|
@@ -21091,19 +20754,15 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
21091
20754
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
21092
20755
|
return false;
|
|
21093
20756
|
}
|
|
21094
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21095
|
-
const receiver = stripParenExpression(node.object);
|
|
21096
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21097
|
-
}
|
|
20757
|
+
if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21098
20758
|
return false;
|
|
21099
20759
|
};
|
|
21100
20760
|
const isUseEffect = (node) => {
|
|
21101
20761
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
21102
20762
|
const callee = node.callee;
|
|
21103
20763
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
21104
|
-
if (
|
|
21105
|
-
|
|
21106
|
-
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;
|
|
21107
20766
|
};
|
|
21108
20767
|
const getEffectFn = (analysis, node) => {
|
|
21109
20768
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -21131,7 +20790,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21131
20790
|
const node = def.node;
|
|
21132
20791
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21133
20792
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21134
|
-
if (!isHookCallee
|
|
20793
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21135
20794
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21136
20795
|
const elements = node.id.elements ?? [];
|
|
21137
20796
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -21142,7 +20801,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
21142
20801
|
const node = def.node;
|
|
21143
20802
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21144
20803
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21145
|
-
if (!isHookCallee
|
|
20804
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21146
20805
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21147
20806
|
const elements = node.id.elements ?? [];
|
|
21148
20807
|
if (elements.length !== 2) return false;
|
|
@@ -21189,7 +20848,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21189
20848
|
const node = def.node;
|
|
21190
20849
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21191
20850
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21192
|
-
return isHookCallee
|
|
20851
|
+
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21193
20852
|
}));
|
|
21194
20853
|
const isRefCurrent = (ref) => {
|
|
21195
20854
|
const parent = ref.identifier.parent;
|
|
@@ -21202,15 +20861,11 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
21202
20861
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
21203
20862
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
21204
20863
|
if (!isPropAlias(analysis, ref)) return false;
|
|
21205
|
-
|
|
21206
|
-
|
|
21207
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21208
|
-
effectiveNode = parent;
|
|
21209
|
-
parent = effectiveNode.parent;
|
|
21210
|
-
}
|
|
20864
|
+
const identifier = ref.identifier;
|
|
20865
|
+
const parent = identifier.parent;
|
|
21211
20866
|
if (!parent) return false;
|
|
21212
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
21213
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
20867
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
|
|
20868
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
|
|
21214
20869
|
const memberParent = parent.parent;
|
|
21215
20870
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
21216
20871
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -21221,7 +20876,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
21221
20876
|
};
|
|
21222
20877
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
21223
20878
|
const getUseStateDecl = (analysis, ref) => {
|
|
21224
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee
|
|
20879
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21225
20880
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
21226
20881
|
return node ?? null;
|
|
21227
20882
|
};
|
|
@@ -21426,7 +21081,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21426
21081
|
const noAdjustStateOnPropChange = defineRule({
|
|
21427
21082
|
id: "no-adjust-state-on-prop-change",
|
|
21428
21083
|
title: "State synced to a prop inside an effect",
|
|
21429
|
-
severity: "
|
|
21084
|
+
severity: "error",
|
|
21430
21085
|
tags: ["test-noise"],
|
|
21431
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",
|
|
21432
21087
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21467,23 +21122,7 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21467
21122
|
"summary",
|
|
21468
21123
|
"textarea"
|
|
21469
21124
|
]);
|
|
21470
|
-
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21471
|
-
const value = attribute.value;
|
|
21472
|
-
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21473
|
-
const expression = value.expression;
|
|
21474
|
-
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21475
|
-
};
|
|
21476
|
-
const DISABLEABLE_TAGS = new Set([
|
|
21477
|
-
"button",
|
|
21478
|
-
"input",
|
|
21479
|
-
"select",
|
|
21480
|
-
"textarea"
|
|
21481
|
-
]);
|
|
21482
21125
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21483
|
-
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21484
|
-
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21485
|
-
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21486
|
-
}
|
|
21487
21126
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21488
21127
|
switch (tagName) {
|
|
21489
21128
|
case "input": {
|
|
@@ -21497,10 +21136,7 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21497
21136
|
case "a":
|
|
21498
21137
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21499
21138
|
case "audio":
|
|
21500
|
-
case "video":
|
|
21501
|
-
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21502
|
-
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21503
|
-
}
|
|
21139
|
+
case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
|
|
21504
21140
|
default: return false;
|
|
21505
21141
|
}
|
|
21506
21142
|
};
|
|
@@ -22464,8 +22100,7 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22464
22100
|
"some"
|
|
22465
22101
|
]);
|
|
22466
22102
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22467
|
-
const isPositionallyStableIterationReceiver = (
|
|
22468
|
-
const receiver = stripParenExpression(receiverNode);
|
|
22103
|
+
const isPositionallyStableIterationReceiver = (receiver) => {
|
|
22469
22104
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22470
22105
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22471
22106
|
const only = receiver.elements[0];
|
|
@@ -22476,13 +22111,17 @@ const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
|
22476
22111
|
}
|
|
22477
22112
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22478
22113
|
const callee = receiver.callee;
|
|
22479
|
-
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;
|
|
22480
22115
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22481
22116
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22482
22117
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22483
22118
|
return false;
|
|
22484
22119
|
};
|
|
22485
|
-
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
|
+
};
|
|
22486
22125
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22487
22126
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22488
22127
|
for (const property of source.properties ?? []) {
|
|
@@ -22541,12 +22180,18 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22541
22180
|
return false;
|
|
22542
22181
|
}
|
|
22543
22182
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22544
|
-
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;
|
|
22545
22184
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22546
22185
|
}
|
|
22547
22186
|
return false;
|
|
22548
22187
|
};
|
|
22549
|
-
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
|
+
};
|
|
22550
22195
|
const noArrayIndexKey = defineRule({
|
|
22551
22196
|
id: "no-array-index-key",
|
|
22552
22197
|
title: "Array index used as a key",
|
|
@@ -23270,7 +22915,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
23270
22915
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
23271
22916
|
return false;
|
|
23272
22917
|
};
|
|
23273
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
22918
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23274
22919
|
"forEach",
|
|
23275
22920
|
"map",
|
|
23276
22921
|
"filter",
|
|
@@ -23291,51 +22936,30 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
23291
22936
|
if (parent.callee === functionNode) return true;
|
|
23292
22937
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
23293
22938
|
const callee = parent.callee;
|
|
23294
|
-
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);
|
|
23295
22940
|
};
|
|
23296
22941
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
23297
|
-
const
|
|
23298
|
-
|
|
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) => {
|
|
23299
22951
|
let fallThroughCount = 0;
|
|
23300
|
-
let
|
|
22952
|
+
let maxTerminatingPathCount = 0;
|
|
23301
22953
|
for (const statement of statements) {
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23306
|
-
fallThroughCount: 0,
|
|
23307
|
-
maxTerminatedCount: 0,
|
|
23308
|
-
doAllPathsTerminate: false
|
|
23309
|
-
};
|
|
23310
|
-
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23311
|
-
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23312
|
-
fallThroughCount: 0,
|
|
23313
|
-
maxTerminatedCount,
|
|
23314
|
-
doAllPathsTerminate: true
|
|
23315
|
-
};
|
|
23316
|
-
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23317
|
-
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22954
|
+
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22955
|
+
if (guardBranch) {
|
|
22956
|
+
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
23318
22957
|
continue;
|
|
23319
22958
|
}
|
|
23320
|
-
if (isTerminatingStatement(statement))
|
|
23321
|
-
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23322
|
-
return {
|
|
23323
|
-
fallThroughCount: 0,
|
|
23324
|
-
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23325
|
-
doAllPathsTerminate: true
|
|
23326
|
-
};
|
|
23327
|
-
}
|
|
22959
|
+
if (isTerminatingStatement(statement)) break;
|
|
23328
22960
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
23329
22961
|
}
|
|
23330
|
-
return
|
|
23331
|
-
fallThroughCount,
|
|
23332
|
-
maxTerminatedCount,
|
|
23333
|
-
doAllPathsTerminate: false
|
|
23334
|
-
};
|
|
23335
|
-
};
|
|
23336
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23337
|
-
const summary = analyzeStatementSequence(statements, context);
|
|
23338
|
-
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22962
|
+
return Math.max(maxTerminatingPathCount, fallThroughCount);
|
|
23339
22963
|
};
|
|
23340
22964
|
const collectLocalHelperFunctions = (root) => {
|
|
23341
22965
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23466,9 +23090,7 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23466
23090
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23467
23091
|
const firstStatement = (body.body ?? [])[0];
|
|
23468
23092
|
if (!firstStatement) return false;
|
|
23469
|
-
if (!
|
|
23470
|
-
const consequent = firstStatement.consequent;
|
|
23471
|
-
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23093
|
+
if (!isGuardWithTerminatingBranch(firstStatement)) return false;
|
|
23472
23094
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23473
23095
|
};
|
|
23474
23096
|
const noCascadingSetState = defineRule({
|
|
@@ -23827,6 +23449,40 @@ const noCloneElement = defineRule({
|
|
|
23827
23449
|
} })
|
|
23828
23450
|
});
|
|
23829
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
|
|
23830
23486
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23831
23487
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23832
23488
|
const CONTEXT_MODULES = [
|
|
@@ -24797,21 +24453,11 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24797
24453
|
return isInitialOnlyPropName(arg.name);
|
|
24798
24454
|
};
|
|
24799
24455
|
//#endregion
|
|
24800
|
-
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24801
|
-
const isNoOpStatement = (statement) => {
|
|
24802
|
-
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24803
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24804
|
-
const expression = stripParenExpression(statement.expression);
|
|
24805
|
-
if (isNodeOfType(expression, "Literal")) return true;
|
|
24806
|
-
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24807
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24808
|
-
return false;
|
|
24809
|
-
};
|
|
24810
|
-
//#endregion
|
|
24811
24456
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24812
24457
|
const getCallbackStatements = (callback) => {
|
|
24813
24458
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24814
|
-
|
|
24459
|
+
if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
|
|
24460
|
+
return callback.body ? [callback.body] : [];
|
|
24815
24461
|
};
|
|
24816
24462
|
//#endregion
|
|
24817
24463
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24850,27 +24496,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24850
24496
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24851
24497
|
}
|
|
24852
24498
|
};
|
|
24853
|
-
const flattenGuardedStatements = (statements) => {
|
|
24854
|
-
const flattened = [];
|
|
24855
|
-
for (const statement of statements) {
|
|
24856
|
-
if (isNoOpStatement(statement)) continue;
|
|
24857
|
-
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24858
|
-
flattened.push(statement);
|
|
24859
|
-
continue;
|
|
24860
|
-
}
|
|
24861
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
24862
|
-
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24863
|
-
if (!branch) continue;
|
|
24864
|
-
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24865
|
-
if (flattenedBranch === null) return null;
|
|
24866
|
-
flattened.push(...flattenedBranch);
|
|
24867
|
-
}
|
|
24868
|
-
continue;
|
|
24869
|
-
}
|
|
24870
|
-
return null;
|
|
24871
|
-
}
|
|
24872
|
-
return flattened;
|
|
24873
|
-
};
|
|
24874
24499
|
const noDerivedStateEffect = defineRule({
|
|
24875
24500
|
id: "no-derived-state-effect",
|
|
24876
24501
|
title: "Derived state stored in an effect",
|
|
@@ -24902,8 +24527,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24902
24527
|
}
|
|
24903
24528
|
}
|
|
24904
24529
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24905
|
-
const statements =
|
|
24906
|
-
if (statements
|
|
24530
|
+
const statements = getCallbackStatements(callback);
|
|
24531
|
+
if (statements.length === 0) return;
|
|
24907
24532
|
if (!statements.every((statement) => {
|
|
24908
24533
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24909
24534
|
const expression = statement.expression;
|
|
@@ -25538,7 +25163,7 @@ const noDidMountSetState = defineRule({
|
|
|
25538
25163
|
const { mode } = resolveSettings$20(context.settings);
|
|
25539
25164
|
return { CallExpression(node) {
|
|
25540
25165
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25541
|
-
if (!isNodeOfType(
|
|
25166
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25542
25167
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25543
25168
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25544
25169
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25663,7 +25288,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25663
25288
|
const { mode } = resolveSettings$19(context.settings);
|
|
25664
25289
|
return { CallExpression(node) {
|
|
25665
25290
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25666
|
-
if (!isNodeOfType(
|
|
25291
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25667
25292
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25668
25293
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25669
25294
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25760,45 +25385,18 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25760
25385
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25761
25386
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25762
25387
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
25763
|
-
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
25764
|
-
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
25765
|
-
"assign",
|
|
25766
|
-
"entries",
|
|
25767
|
-
"fromEntries",
|
|
25768
|
-
"keys",
|
|
25769
|
-
"values"
|
|
25770
|
-
]);
|
|
25771
|
-
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
25772
|
-
"map",
|
|
25773
|
-
"filter",
|
|
25774
|
-
"slice",
|
|
25775
|
-
"concat",
|
|
25776
|
-
"flat",
|
|
25777
|
-
"flatMap",
|
|
25778
|
-
"toSorted",
|
|
25779
|
-
"toReversed",
|
|
25780
|
-
"toSpliced",
|
|
25781
|
-
"with"
|
|
25782
|
-
]);
|
|
25783
25388
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25784
25389
|
const isPlainDataProducerCall = (expression) => {
|
|
25785
25390
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25786
25391
|
const callee = expression.callee;
|
|
25787
25392
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25788
25393
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25789
|
-
if (isNodeOfType(callee.object, "Identifier"))
|
|
25790
|
-
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25791
|
-
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
25792
|
-
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
25793
|
-
}
|
|
25394
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25794
25395
|
return producesPlainStateValue(callee.object);
|
|
25795
25396
|
};
|
|
25796
|
-
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
25797
|
-
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25798
25397
|
const producesPlainStateValue = (expression) => {
|
|
25799
25398
|
const unwrapped = stripParenExpression(expression);
|
|
25800
25399
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25801
|
-
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25802
25400
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25803
25401
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25804
25402
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25813,38 +25411,6 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25813
25411
|
}
|
|
25814
25412
|
return producesPlainStateValue(unwrapped);
|
|
25815
25413
|
};
|
|
25816
|
-
const producesOpaqueInstanceValue = (expression) => {
|
|
25817
|
-
if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
|
|
25818
|
-
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25819
|
-
const callee = expression.callee;
|
|
25820
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
25821
|
-
if (isPlainDataProducerCall(expression)) return false;
|
|
25822
|
-
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
25823
|
-
return true;
|
|
25824
|
-
};
|
|
25825
|
-
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25826
|
-
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25827
|
-
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25828
|
-
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
25829
|
-
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25830
|
-
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25831
|
-
const setterName = node.callee.name;
|
|
25832
|
-
if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
|
|
25833
|
-
const argument = node.arguments?.[0];
|
|
25834
|
-
if (!argument) return;
|
|
25835
|
-
const unwrapped = stripParenExpression(argument);
|
|
25836
|
-
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
25837
|
-
if (producesPlainStateValue(unwrapped)) {
|
|
25838
|
-
plainFedSetterNames.add(setterName);
|
|
25839
|
-
return;
|
|
25840
|
-
}
|
|
25841
|
-
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25842
|
-
}, true);
|
|
25843
|
-
return {
|
|
25844
|
-
plainFedSetterNames,
|
|
25845
|
-
opaqueFedSetterNames
|
|
25846
|
-
};
|
|
25847
|
-
};
|
|
25848
25414
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25849
25415
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25850
25416
|
walkAst(componentBody, (node) => {
|
|
@@ -25914,15 +25480,11 @@ const noDirectStateMutation = defineRule({
|
|
|
25914
25480
|
if (bindings.length === 0) return;
|
|
25915
25481
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25916
25482
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25917
|
-
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25918
25483
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25919
25484
|
for (const binding of bindings) {
|
|
25920
25485
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25921
25486
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25922
|
-
|
|
25923
|
-
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
25924
|
-
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
25925
|
-
plainObjectStateValueNames.add(binding.valueName);
|
|
25487
|
+
if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
|
|
25926
25488
|
}
|
|
25927
25489
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25928
25490
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -26047,10 +25609,9 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
26047
25609
|
create: (context) => ({ CallExpression(node) {
|
|
26048
25610
|
const callee = node.callee;
|
|
26049
25611
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
26050
|
-
|
|
26051
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25612
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
26052
25613
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
26053
|
-
if (context.scopes.symbolFor(
|
|
25614
|
+
if (context.scopes.symbolFor(callee.object) !== null) return;
|
|
26054
25615
|
if (!importsReactViewTransition(node)) return;
|
|
26055
25616
|
context.report({
|
|
26056
25617
|
node,
|
|
@@ -26070,8 +25631,7 @@ const noDocumentWrite = defineRule({
|
|
|
26070
25631
|
create: (context) => ({ CallExpression(node) {
|
|
26071
25632
|
const callee = node.callee;
|
|
26072
25633
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
26073
|
-
|
|
26074
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25634
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
26075
25635
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
26076
25636
|
context.report({
|
|
26077
25637
|
node,
|
|
@@ -26703,6 +26263,13 @@ const noEffectEventHandler = defineRule({
|
|
|
26703
26263
|
}
|
|
26704
26264
|
});
|
|
26705
26265
|
//#endregion
|
|
26266
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
26267
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
26268
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
26269
|
+
if (importSource === null) return false;
|
|
26270
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
26271
|
+
};
|
|
26272
|
+
//#endregion
|
|
26706
26273
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26707
26274
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26708
26275
|
const componentBindingStack = [];
|
|
@@ -26756,7 +26323,11 @@ const noEffectEventInDeps = defineRule({
|
|
|
26756
26323
|
const initializer = declaratorNode.init;
|
|
26757
26324
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26758
26325
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26759
|
-
if (
|
|
26326
|
+
if (isNodeOfType(initializer.callee, "Identifier")) {
|
|
26327
|
+
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26328
|
+
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26329
|
+
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26330
|
+
}
|
|
26760
26331
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26761
26332
|
} });
|
|
26762
26333
|
return {
|
|
@@ -29336,7 +28907,7 @@ const noIsMounted = defineRule({
|
|
|
29336
28907
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
29337
28908
|
create: (context) => ({ CallExpression(node) {
|
|
29338
28909
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
29339
|
-
if (!isNodeOfType(
|
|
28910
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29340
28911
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
29341
28912
|
if (!getParentComponent(node)) return;
|
|
29342
28913
|
context.report({
|
|
@@ -29351,9 +28922,7 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
29351
28922
|
const isJsonMethodCall = (node, method) => {
|
|
29352
28923
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
29353
28924
|
const callee = node.callee;
|
|
29354
|
-
|
|
29355
|
-
const receiver = stripParenExpression(callee.object);
|
|
29356
|
-
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;
|
|
29357
28926
|
};
|
|
29358
28927
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
29359
28928
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29438,7 +29007,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29438
29007
|
const noJsxElementType = defineRule({
|
|
29439
29008
|
id: "no-jsx-element-type",
|
|
29440
29009
|
title: "No JSX.Element",
|
|
29441
|
-
severity: "
|
|
29010
|
+
severity: "error",
|
|
29442
29011
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29443
29012
|
create: (context) => {
|
|
29444
29013
|
let isJsxImported = false;
|
|
@@ -29764,378 +29333,6 @@ const noLegacyContextApi = defineRule({
|
|
|
29764
29333
|
}
|
|
29765
29334
|
});
|
|
29766
29335
|
//#endregion
|
|
29767
|
-
//#region src/plugin/utils/executes-during-render.ts
|
|
29768
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29769
|
-
"map",
|
|
29770
|
-
"filter",
|
|
29771
|
-
"forEach",
|
|
29772
|
-
"flatMap",
|
|
29773
|
-
"reduce",
|
|
29774
|
-
"reduceRight",
|
|
29775
|
-
"some",
|
|
29776
|
-
"every",
|
|
29777
|
-
"find",
|
|
29778
|
-
"findIndex",
|
|
29779
|
-
"findLast",
|
|
29780
|
-
"findLastIndex",
|
|
29781
|
-
"sort",
|
|
29782
|
-
"toSorted"
|
|
29783
|
-
]);
|
|
29784
|
-
const executesDuringRender = (functionNode) => {
|
|
29785
|
-
const parent = functionNode.parent;
|
|
29786
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29787
|
-
if (parent.callee === functionNode) return true;
|
|
29788
|
-
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29789
|
-
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;
|
|
29790
|
-
};
|
|
29791
|
-
//#endregion
|
|
29792
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29793
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29794
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29795
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29796
|
-
return false;
|
|
29797
|
-
};
|
|
29798
|
-
//#endregion
|
|
29799
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29800
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29801
|
-
let ancestor = bindingIdentifier.parent;
|
|
29802
|
-
while (ancestor) {
|
|
29803
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29804
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29805
|
-
ancestor = ancestor.parent ?? null;
|
|
29806
|
-
}
|
|
29807
|
-
return null;
|
|
29808
|
-
};
|
|
29809
|
-
//#endregion
|
|
29810
|
-
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29811
|
-
const isFalsyLiteral = (node) => {
|
|
29812
|
-
if (!node) return true;
|
|
29813
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29814
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29815
|
-
};
|
|
29816
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
29817
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29818
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29819
|
-
if (!binding) return false;
|
|
29820
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29821
|
-
if (!declarator?.init) return false;
|
|
29822
|
-
const init = stripParenExpression(declarator.init);
|
|
29823
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29824
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29825
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29826
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
29827
|
-
};
|
|
29828
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29829
|
-
//#endregion
|
|
29830
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29831
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
29832
|
-
let cursor = node;
|
|
29833
|
-
let parent = node.parent;
|
|
29834
|
-
while (parent) {
|
|
29835
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29836
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29837
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29838
|
-
cursor = parent;
|
|
29839
|
-
parent = parent.parent ?? null;
|
|
29840
|
-
}
|
|
29841
|
-
return false;
|
|
29842
|
-
};
|
|
29843
|
-
//#endregion
|
|
29844
|
-
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29845
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29846
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
29847
|
-
const unwrapped = stripParenExpression(expression);
|
|
29848
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29849
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29850
|
-
const property = unwrapped.property;
|
|
29851
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29852
|
-
}
|
|
29853
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29854
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29855
|
-
return false;
|
|
29856
|
-
};
|
|
29857
|
-
//#endregion
|
|
29858
|
-
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29859
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
29860
|
-
let cursor = node;
|
|
29861
|
-
let parent = node.parent;
|
|
29862
|
-
while (parent) {
|
|
29863
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29864
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29865
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29866
|
-
cursor = parent;
|
|
29867
|
-
parent = parent.parent ?? null;
|
|
29868
|
-
}
|
|
29869
|
-
return false;
|
|
29870
|
-
};
|
|
29871
|
-
//#endregion
|
|
29872
|
-
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29873
|
-
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29874
|
-
"toLocaleString",
|
|
29875
|
-
"toLocaleDateString",
|
|
29876
|
-
"toLocaleTimeString"
|
|
29877
|
-
]);
|
|
29878
|
-
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29879
|
-
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29880
|
-
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29881
|
-
"format",
|
|
29882
|
-
"formatToParts",
|
|
29883
|
-
"formatRange"
|
|
29884
|
-
]);
|
|
29885
|
-
const isProvableDateExpression = (expression) => {
|
|
29886
|
-
if (!expression) return false;
|
|
29887
|
-
const unwrapped = stripParenExpression(expression);
|
|
29888
|
-
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29889
|
-
};
|
|
29890
|
-
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29891
|
-
const receiverNameLooksDateFlavored = (expression) => {
|
|
29892
|
-
if (!expression) return false;
|
|
29893
|
-
const unwrapped = stripParenExpression(expression);
|
|
29894
|
-
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29895
|
-
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29896
|
-
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29897
|
-
return false;
|
|
29898
|
-
};
|
|
29899
|
-
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29900
|
-
if (!objectExpression) return false;
|
|
29901
|
-
const unwrapped = stripParenExpression(objectExpression);
|
|
29902
|
-
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29903
|
-
for (const property of unwrapped.properties ?? []) {
|
|
29904
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
29905
|
-
if (property.computed) continue;
|
|
29906
|
-
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29907
|
-
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29908
|
-
}
|
|
29909
|
-
return false;
|
|
29910
|
-
};
|
|
29911
|
-
const hasExplicitLocaleArgument = (argument) => {
|
|
29912
|
-
if (!argument) return false;
|
|
29913
|
-
const unwrapped = stripParenExpression(argument);
|
|
29914
|
-
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29915
|
-
return true;
|
|
29916
|
-
};
|
|
29917
|
-
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29918
|
-
const localeArgument = call.arguments?.[0];
|
|
29919
|
-
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29920
|
-
const optionsArgument = call.arguments?.[1];
|
|
29921
|
-
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29922
|
-
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29923
|
-
};
|
|
29924
|
-
const matchLocaleMethodCall = (call) => {
|
|
29925
|
-
const callee = call.callee;
|
|
29926
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29927
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29928
|
-
const methodName = callee.property.name;
|
|
29929
|
-
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29930
|
-
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29931
|
-
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29932
|
-
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29933
|
-
return {
|
|
29934
|
-
node: call,
|
|
29935
|
-
display: `${methodName}()`
|
|
29936
|
-
};
|
|
29937
|
-
};
|
|
29938
|
-
const getIntlFormatterName = (expression) => {
|
|
29939
|
-
if (!expression) return null;
|
|
29940
|
-
const unwrapped = stripParenExpression(expression);
|
|
29941
|
-
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29942
|
-
const callee = unwrapped.callee;
|
|
29943
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29944
|
-
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29945
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29946
|
-
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29947
|
-
};
|
|
29948
|
-
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29949
|
-
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29950
|
-
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29951
|
-
if (formatterName !== "DateTimeFormat") return true;
|
|
29952
|
-
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29953
|
-
};
|
|
29954
|
-
const matchIntlFormatCall = (call) => {
|
|
29955
|
-
const callee = call.callee;
|
|
29956
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29957
|
-
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29958
|
-
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29959
|
-
let construction = stripParenExpression(callee.object);
|
|
29960
|
-
if (isNodeOfType(construction, "Identifier")) {
|
|
29961
|
-
const binding = findVariableInitializer(construction, construction.name);
|
|
29962
|
-
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29963
|
-
}
|
|
29964
|
-
if (!construction) return null;
|
|
29965
|
-
const formatterName = getIntlFormatterName(construction);
|
|
29966
|
-
if (!formatterName) return null;
|
|
29967
|
-
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29968
|
-
return {
|
|
29969
|
-
node: call,
|
|
29970
|
-
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29971
|
-
};
|
|
29972
|
-
};
|
|
29973
|
-
const isDeterministicInputDateConstruction = (expression) => {
|
|
29974
|
-
if (!expression) return false;
|
|
29975
|
-
const unwrapped = stripParenExpression(expression);
|
|
29976
|
-
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29977
|
-
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29978
|
-
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29979
|
-
};
|
|
29980
|
-
const matchDateDefaultStringification = (node) => {
|
|
29981
|
-
if (isNodeOfType(node, "CallExpression")) {
|
|
29982
|
-
const callee = node.callee;
|
|
29983
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29984
|
-
node,
|
|
29985
|
-
display: "Date.prototype.toString()"
|
|
29986
|
-
};
|
|
29987
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29988
|
-
node,
|
|
29989
|
-
display: "String(new Date(…))"
|
|
29990
|
-
};
|
|
29991
|
-
return null;
|
|
29992
|
-
}
|
|
29993
|
-
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29994
|
-
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29995
|
-
node: expression,
|
|
29996
|
-
display: "`${new Date(…)}`"
|
|
29997
|
-
};
|
|
29998
|
-
}
|
|
29999
|
-
return null;
|
|
30000
|
-
};
|
|
30001
|
-
const findRenderPhaseComponentOrHook = (node) => {
|
|
30002
|
-
let functionNode = findEnclosingFunction(node);
|
|
30003
|
-
while (functionNode) {
|
|
30004
|
-
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30005
|
-
if (!executesDuringRender(functionNode)) return null;
|
|
30006
|
-
functionNode = findEnclosingFunction(functionNode);
|
|
30007
|
-
}
|
|
30008
|
-
return null;
|
|
30009
|
-
};
|
|
30010
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30011
|
-
if (fileHasUseClientDirective) return true;
|
|
30012
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30013
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
30014
|
-
let callsHook = false;
|
|
30015
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30016
|
-
if (callsHook) return false;
|
|
30017
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30018
|
-
callsHook = true;
|
|
30019
|
-
return false;
|
|
30020
|
-
}
|
|
30021
|
-
});
|
|
30022
|
-
return callsHook;
|
|
30023
|
-
};
|
|
30024
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30025
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30026
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30027
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
30028
|
-
let cursor = node;
|
|
30029
|
-
while (cursor) {
|
|
30030
|
-
ancestors.add(cursor);
|
|
30031
|
-
cursor = cursor.parent ?? null;
|
|
30032
|
-
}
|
|
30033
|
-
for (const statement of body.body ?? []) {
|
|
30034
|
-
if (ancestors.has(statement)) return false;
|
|
30035
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30036
|
-
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30037
|
-
let returnsEarly = false;
|
|
30038
|
-
walkAst(statement.consequent, (child) => {
|
|
30039
|
-
if (isFunctionLike$1(child)) return false;
|
|
30040
|
-
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30041
|
-
returnsEarly = true;
|
|
30042
|
-
return false;
|
|
30043
|
-
}
|
|
30044
|
-
});
|
|
30045
|
-
if (returnsEarly) return true;
|
|
30046
|
-
}
|
|
30047
|
-
return false;
|
|
30048
|
-
};
|
|
30049
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
30050
|
-
let cursor = node.parent;
|
|
30051
|
-
while (cursor) {
|
|
30052
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30053
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30054
|
-
cursor = cursor.parent ?? null;
|
|
30055
|
-
}
|
|
30056
|
-
return null;
|
|
30057
|
-
};
|
|
30058
|
-
const noLocaleFormatInRender = defineRule({
|
|
30059
|
-
id: "no-locale-format-in-render",
|
|
30060
|
-
title: "Locale/timezone formatting during render",
|
|
30061
|
-
severity: "warn",
|
|
30062
|
-
category: "Correctness",
|
|
30063
|
-
disabledWhen: [
|
|
30064
|
-
"vite",
|
|
30065
|
-
"cra",
|
|
30066
|
-
"expo",
|
|
30067
|
-
"react-native",
|
|
30068
|
-
"unknown"
|
|
30069
|
-
],
|
|
30070
|
-
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.",
|
|
30071
|
-
create: (context) => {
|
|
30072
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
30073
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30074
|
-
let fileHasUseClientDirective = false;
|
|
30075
|
-
let fileIsEmailTemplate = false;
|
|
30076
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30077
|
-
const reportIfRenderPhase = (match) => {
|
|
30078
|
-
if (reportedNodes.has(match.node)) return;
|
|
30079
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30080
|
-
if (!componentOrHookNode) return;
|
|
30081
|
-
if (fileIsEmailTemplate) return;
|
|
30082
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30083
|
-
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30084
|
-
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30085
|
-
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30086
|
-
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30087
|
-
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30088
|
-
reportedNodes.add(match.node);
|
|
30089
|
-
context.report({
|
|
30090
|
-
node: match.node,
|
|
30091
|
-
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.`
|
|
30092
|
-
});
|
|
30093
|
-
};
|
|
30094
|
-
return {
|
|
30095
|
-
Program(node) {
|
|
30096
|
-
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30097
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30098
|
-
},
|
|
30099
|
-
CallExpression(node) {
|
|
30100
|
-
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30101
|
-
if (match) reportIfRenderPhase(match);
|
|
30102
|
-
},
|
|
30103
|
-
TemplateLiteral(node) {
|
|
30104
|
-
const match = matchDateDefaultStringification(node);
|
|
30105
|
-
if (match) reportIfRenderPhase(match);
|
|
30106
|
-
},
|
|
30107
|
-
JSXExpressionContainer(node) {
|
|
30108
|
-
const expression = stripParenExpression(node.expression);
|
|
30109
|
-
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30110
|
-
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30111
|
-
const helperName = expression.callee.name;
|
|
30112
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30113
|
-
if (!componentOrHookNode) return;
|
|
30114
|
-
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30115
|
-
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30116
|
-
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30117
|
-
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30118
|
-
if (isFunctionLike$1(child)) return false;
|
|
30119
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30120
|
-
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30121
|
-
if (!match || reportedNodes.has(match.node)) return;
|
|
30122
|
-
if (fileIsEmailTemplate) return;
|
|
30123
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30124
|
-
if (isInsideClientOnlyGuard(node)) return;
|
|
30125
|
-
if (isGatedByFalsyInitialState(node)) return;
|
|
30126
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30127
|
-
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30128
|
-
reportedNodes.add(match.node);
|
|
30129
|
-
context.report({
|
|
30130
|
-
node: match.node,
|
|
30131
|
-
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.`
|
|
30132
|
-
});
|
|
30133
|
-
});
|
|
30134
|
-
}
|
|
30135
|
-
};
|
|
30136
|
-
}
|
|
30137
|
-
});
|
|
30138
|
-
//#endregion
|
|
30139
29336
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
30140
29337
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
30141
29338
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30949,6 +30146,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30949
30146
|
"reverse",
|
|
30950
30147
|
"sort"
|
|
30951
30148
|
]);
|
|
30149
|
+
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30952
30150
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30953
30151
|
"assign",
|
|
30954
30152
|
"defineProperties",
|
|
@@ -31022,6 +30220,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
31022
30220
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
31023
30221
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
31024
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;
|
|
31025
30224
|
}
|
|
31026
30225
|
}
|
|
31027
30226
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -31060,7 +30259,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
31060
30259
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
31061
30260
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
31062
30261
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
31063
|
-
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
31064
30262
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
31065
30263
|
});
|
|
31066
30264
|
return mutations;
|
|
@@ -31293,7 +30491,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
31293
30491
|
id: "no-nested-component-definition",
|
|
31294
30492
|
title: "Component defined inside another component",
|
|
31295
30493
|
tags: ["test-noise", "react-jsx-only"],
|
|
31296
|
-
severity: "
|
|
30494
|
+
severity: "error",
|
|
31297
30495
|
category: "Correctness",
|
|
31298
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.",
|
|
31299
30497
|
create: (context) => {
|
|
@@ -32248,12 +31446,12 @@ const noPassDataToParent = defineRule({
|
|
|
32248
31446
|
if (calleeNode === identifier) {
|
|
32249
31447
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
32250
31448
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
32251
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31449
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
|
|
32252
31450
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
32253
31451
|
if (isCustomHookParameter(ref)) continue;
|
|
32254
31452
|
} else continue;
|
|
32255
31453
|
const methodName = getCallMethodName(calleeNode);
|
|
32256
|
-
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));
|
|
32257
31455
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32258
31456
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
32259
31457
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -32444,7 +31642,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
32444
31642
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
32445
31643
|
const calleeNode = callExpr.callee;
|
|
32446
31644
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
32447
|
-
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));
|
|
32448
31646
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32449
31647
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
32450
31648
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32772,6 +31970,40 @@ const noPreventDefault = defineRule({
|
|
|
32772
31970
|
}
|
|
32773
31971
|
});
|
|
32774
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
|
|
32775
32007
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32776
32008
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32777
32009
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32978,7 +32210,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32978
32210
|
return `${callee.name}()`;
|
|
32979
32211
|
}
|
|
32980
32212
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32981
|
-
const receiver =
|
|
32213
|
+
const receiver = callee.object;
|
|
32982
32214
|
const property = callee.property;
|
|
32983
32215
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32984
32216
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -33099,10 +32331,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33099
32331
|
if (typeof sourceValue !== "string") return;
|
|
33100
32332
|
if (handleExtraSource?.(node, context)) return;
|
|
33101
32333
|
if (sourceValue !== source) return;
|
|
33102
|
-
if (isTypeOnlyImport(node)) return;
|
|
33103
32334
|
for (const specifier of node.specifiers ?? []) {
|
|
33104
32335
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33105
|
-
if (specifier.importKind === "type") continue;
|
|
33106
32336
|
const importedName = getImportedName$1(specifier);
|
|
33107
32337
|
if (!importedName) continue;
|
|
33108
32338
|
const message = messages.get(importedName);
|
|
@@ -33121,9 +32351,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33121
32351
|
MemberExpression(node) {
|
|
33122
32352
|
if (namespaceBindings.size === 0) return;
|
|
33123
32353
|
if (node.computed) return;
|
|
33124
|
-
|
|
33125
|
-
if (!
|
|
33126
|
-
if (!namespaceBindings.has(receiver.name)) return;
|
|
32354
|
+
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
32355
|
+
if (!namespaceBindings.has(node.object.name)) return;
|
|
33127
32356
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
33128
32357
|
const message = messages.get(node.property.name);
|
|
33129
32358
|
if (message) context.report({
|
|
@@ -33188,7 +32417,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
33188
32417
|
});
|
|
33189
32418
|
//#endregion
|
|
33190
32419
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
33191
|
-
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'`."]]);
|
|
33192
32421
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
33193
32422
|
if (!rawFilename) return false;
|
|
33194
32423
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -33226,7 +32455,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
33226
32455
|
requires: ["react:19"],
|
|
33227
32456
|
tags: ["test-noise", "migration-hint"],
|
|
33228
32457
|
severity: "warn",
|
|
33229
|
-
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.",
|
|
33230
32459
|
create: (context) => {
|
|
33231
32460
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
33232
32461
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33550,11 +32779,34 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33550
32779
|
});
|
|
33551
32780
|
//#endregion
|
|
33552
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
|
+
};
|
|
33553
32805
|
const isInsideComponentContext = (node) => {
|
|
33554
32806
|
let cursor = node.parent;
|
|
33555
32807
|
while (cursor) {
|
|
32808
|
+
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33556
32809
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33557
|
-
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33558
32810
|
cursor = cursor.parent ?? null;
|
|
33559
32811
|
}
|
|
33560
32812
|
return false;
|
|
@@ -33564,28 +32816,24 @@ const functionBodyOf = (node) => {
|
|
|
33564
32816
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
33565
32817
|
return null;
|
|
33566
32818
|
};
|
|
33567
|
-
const isHookCallee = (callee) => {
|
|
33568
|
-
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33569
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33570
|
-
return false;
|
|
33571
|
-
};
|
|
33572
32819
|
const containsHookCall = (body) => {
|
|
33573
32820
|
let found = false;
|
|
33574
32821
|
walkAst(body, (child) => {
|
|
33575
|
-
if (found) return
|
|
33576
|
-
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32822
|
+
if (found) return;
|
|
33577
32823
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33578
|
-
|
|
32824
|
+
const name = getCalleeName$2(child);
|
|
32825
|
+
if (name && isReactHookName(name)) found = true;
|
|
33579
32826
|
});
|
|
33580
32827
|
return found;
|
|
33581
32828
|
};
|
|
33582
|
-
const
|
|
32829
|
+
const isModuleScopeHookFreeHelper = (symbol) => {
|
|
33583
32830
|
if (!symbol) return false;
|
|
33584
32831
|
const declaration = symbol.declarationNode;
|
|
33585
32832
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33586
32833
|
const body = functionBodyOf(declaration);
|
|
33587
32834
|
if (!body) return false;
|
|
33588
|
-
|
|
32835
|
+
if (findEnclosingFunction(declaration) !== null) return false;
|
|
32836
|
+
return !containsHookCall(body);
|
|
33589
32837
|
};
|
|
33590
32838
|
const noRenderInRender = defineRule({
|
|
33591
32839
|
id: "no-render-in-render",
|
|
@@ -33594,13 +32842,20 @@ const noRenderInRender = defineRule({
|
|
|
33594
32842
|
tags: ["test-noise"],
|
|
33595
32843
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
33596
32844
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33597
|
-
const expression =
|
|
32845
|
+
const expression = node.expression;
|
|
33598
32846
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33599
|
-
|
|
33600
|
-
|
|
33601
|
-
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;
|
|
33602
32851
|
if (!isInsideComponentContext(node)) return;
|
|
33603
|
-
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
|
+
}
|
|
33604
32859
|
context.report({
|
|
33605
32860
|
node: expression,
|
|
33606
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.`
|
|
@@ -33661,7 +32916,13 @@ const noRenderPropChildren = defineRule({
|
|
|
33661
32916
|
//#endregion
|
|
33662
32917
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
33663
32918
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
33664
|
-
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
|
+
};
|
|
33665
32926
|
const isUsedAsReturnValue = (parent) => {
|
|
33666
32927
|
if (!parent) return false;
|
|
33667
32928
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -34497,7 +33758,7 @@ const noSetState = defineRule({
|
|
|
34497
33758
|
category: "Architecture",
|
|
34498
33759
|
create: (context) => ({ CallExpression(node) {
|
|
34499
33760
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
34500
|
-
if (!isNodeOfType(
|
|
33761
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34501
33762
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
34502
33763
|
if (!getParentComponent(node)) return;
|
|
34503
33764
|
context.report({
|
|
@@ -36943,36 +36204,6 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36943
36204
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36944
36205
|
return true;
|
|
36945
36206
|
};
|
|
36946
|
-
const isTrivialContainerLiteral = (node) => {
|
|
36947
|
-
if (!node) return false;
|
|
36948
|
-
const innerExpression = stripParenExpression(node);
|
|
36949
|
-
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36950
|
-
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36951
|
-
return false;
|
|
36952
|
-
};
|
|
36953
|
-
const isNonEscapingRead = (identifier) => {
|
|
36954
|
-
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36955
|
-
const memberNode = readRoot.parent;
|
|
36956
|
-
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36957
|
-
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36958
|
-
const memberUseParent = memberUse.parent;
|
|
36959
|
-
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36960
|
-
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36961
|
-
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36962
|
-
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36963
|
-
};
|
|
36964
|
-
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36965
|
-
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36966
|
-
const parentNode = memoUsageRoot.parent;
|
|
36967
|
-
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36968
|
-
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36969
|
-
const bindingTarget = parentNode.id;
|
|
36970
|
-
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36971
|
-
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36972
|
-
const symbol = scopes.symbolFor(bindingTarget);
|
|
36973
|
-
if (!symbol) return false;
|
|
36974
|
-
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36975
|
-
};
|
|
36976
36207
|
const noUsememoSimpleExpression = defineRule({
|
|
36977
36208
|
id: "no-usememo-simple-expression",
|
|
36978
36209
|
title: "useMemo on a cheap value",
|
|
@@ -36994,17 +36225,9 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36994
36225
|
let returnExpression = null;
|
|
36995
36226
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36996
36227
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36997
|
-
if (
|
|
36998
|
-
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36999
|
-
context.report({
|
|
37000
|
-
node,
|
|
37001
|
-
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37002
|
-
});
|
|
37003
|
-
return;
|
|
37004
|
-
}
|
|
37005
|
-
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36228
|
+
if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
|
|
37006
36229
|
node,
|
|
37007
|
-
message: "This
|
|
36230
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37008
36231
|
});
|
|
37009
36232
|
} })
|
|
37010
36233
|
});
|
|
@@ -37110,7 +36333,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
37110
36333
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
37111
36334
|
return { CallExpression(node) {
|
|
37112
36335
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
37113
|
-
if (!isNodeOfType(
|
|
36336
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37114
36337
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
37115
36338
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
37116
36339
|
context.report({
|
|
@@ -37427,10 +36650,6 @@ const isRouteFactoryCall = (expression) => {
|
|
|
37427
36650
|
}
|
|
37428
36651
|
return false;
|
|
37429
36652
|
};
|
|
37430
|
-
const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
|
|
37431
|
-
const expression = skipTsExpression(argument);
|
|
37432
|
-
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37433
|
-
});
|
|
37434
36653
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
37435
36654
|
const isHocCallee = (callee, state) => {
|
|
37436
36655
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -37470,7 +36689,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
37470
36689
|
continue;
|
|
37471
36690
|
}
|
|
37472
36691
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
37473
|
-
if (
|
|
36692
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37474
36693
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
37475
36694
|
}
|
|
37476
36695
|
return false;
|
|
@@ -37578,22 +36797,18 @@ const onlyExportComponents = defineRule({
|
|
|
37578
36797
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37579
36798
|
allowExportNames: new Set(settings.allowExportNames),
|
|
37580
36799
|
allowConstantExport: settings.allowConstantExport,
|
|
37581
|
-
localComponentNames
|
|
37582
|
-
scopes: context.scopes
|
|
36800
|
+
localComponentNames
|
|
37583
36801
|
};
|
|
37584
36802
|
for (const child of componentCandidates) {
|
|
37585
36803
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37586
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)
|
|
36804
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37587
36805
|
}
|
|
37588
36806
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37589
36807
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37590
36808
|
}
|
|
37591
36809
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37592
36810
|
const initializer = child.init;
|
|
37593
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37594
|
-
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37595
|
-
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37596
|
-
}
|
|
36811
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37597
36812
|
}
|
|
37598
36813
|
}
|
|
37599
36814
|
const exports = [];
|
|
@@ -37663,10 +36878,6 @@ const onlyExportComponents = defineRule({
|
|
|
37663
36878
|
return false;
|
|
37664
36879
|
})();
|
|
37665
36880
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37666
|
-
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37667
|
-
kind: "non-component",
|
|
37668
|
-
reportNode: stripped
|
|
37669
|
-
});
|
|
37670
36881
|
else context.report({
|
|
37671
36882
|
node: stripped,
|
|
37672
36883
|
message: ANONYMOUS_MESSAGE
|
|
@@ -38557,8 +37768,8 @@ const preferHtmlDialog = defineRule({
|
|
|
38557
37768
|
if (!HTML_TAGS.has(tagName)) return;
|
|
38558
37769
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
38559
37770
|
if (roleAttribute) {
|
|
38560
|
-
const
|
|
38561
|
-
if (
|
|
37771
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
37772
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
38562
37773
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
38563
37774
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
38564
37775
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -39262,22 +38473,11 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
39262
38473
|
}
|
|
39263
38474
|
return null;
|
|
39264
38475
|
};
|
|
39265
|
-
const isTrivialLiteralExpression = (expression) => {
|
|
39266
|
-
if (isNodeOfType(expression, "Literal")) return true;
|
|
39267
|
-
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39268
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39269
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39270
|
-
return false;
|
|
39271
|
-
};
|
|
39272
38476
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
39273
38477
|
const handlerStatements = getCallbackStatements(handler);
|
|
39274
38478
|
if (handlerStatements.length !== 1) return null;
|
|
39275
38479
|
const onlyStatement = handlerStatements[0];
|
|
39276
|
-
|
|
39277
|
-
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39278
|
-
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39279
|
-
if (!expression) return null;
|
|
39280
|
-
expression = stripParenExpression(expression);
|
|
38480
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
39281
38481
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
39282
38482
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
39283
38483
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -39287,106 +38487,6 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
39287
38487
|
setterArgument: expression.arguments[0]
|
|
39288
38488
|
};
|
|
39289
38489
|
};
|
|
39290
|
-
const isListenerCollectionInitializer = (init) => {
|
|
39291
|
-
if (!init) return false;
|
|
39292
|
-
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39293
|
-
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39294
|
-
};
|
|
39295
|
-
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39296
|
-
if (!isFunctionLike$1(functionNode)) return false;
|
|
39297
|
-
const firstParam = functionNode.params?.[0];
|
|
39298
|
-
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39299
|
-
const listenerParamName = firstParam.name;
|
|
39300
|
-
let registersListener = false;
|
|
39301
|
-
walkAst(functionNode.body, (child) => {
|
|
39302
|
-
if (registersListener) return false;
|
|
39303
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39304
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39305
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39306
|
-
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39307
|
-
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39308
|
-
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39309
|
-
const registeredArgument = child.arguments?.[0];
|
|
39310
|
-
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39311
|
-
});
|
|
39312
|
-
return registersListener;
|
|
39313
|
-
};
|
|
39314
|
-
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39315
|
-
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39316
|
-
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39317
|
-
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39318
|
-
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39319
|
-
mutableBindingNames,
|
|
39320
|
-
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39321
|
-
};
|
|
39322
|
-
for (const statement of programRoot.body ?? []) {
|
|
39323
|
-
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39324
|
-
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39325
|
-
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39326
|
-
continue;
|
|
39327
|
-
}
|
|
39328
|
-
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39329
|
-
for (const declarator of unwrapped.declarations ?? []) {
|
|
39330
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39331
|
-
const init = declarator.init ?? null;
|
|
39332
|
-
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39333
|
-
mutableBindingNames.add(declarator.id.name);
|
|
39334
|
-
continue;
|
|
39335
|
-
}
|
|
39336
|
-
if (isListenerCollectionInitializer(init)) {
|
|
39337
|
-
listenerCollectionNames.add(declarator.id.name);
|
|
39338
|
-
continue;
|
|
39339
|
-
}
|
|
39340
|
-
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39341
|
-
}
|
|
39342
|
-
}
|
|
39343
|
-
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39344
|
-
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39345
|
-
return {
|
|
39346
|
-
mutableBindingNames,
|
|
39347
|
-
subscribeFunctionNames
|
|
39348
|
-
};
|
|
39349
|
-
};
|
|
39350
|
-
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39351
|
-
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39352
|
-
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39353
|
-
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39354
|
-
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39355
|
-
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39356
|
-
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39357
|
-
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39358
|
-
return initialArgument.name;
|
|
39359
|
-
};
|
|
39360
|
-
const argumentForwardsSetter = (argument, setterName) => {
|
|
39361
|
-
if (!argument) return false;
|
|
39362
|
-
const unwrapped = stripParenExpression(argument);
|
|
39363
|
-
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39364
|
-
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39365
|
-
let callsSetter = false;
|
|
39366
|
-
walkAst(unwrapped.body, (child) => {
|
|
39367
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39368
|
-
callsSetter = true;
|
|
39369
|
-
return false;
|
|
39370
|
-
}
|
|
39371
|
-
});
|
|
39372
|
-
return callsSetter;
|
|
39373
|
-
};
|
|
39374
|
-
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39375
|
-
let matchedCall = null;
|
|
39376
|
-
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39377
|
-
if (matchedCall) return false;
|
|
39378
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39379
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39380
|
-
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39381
|
-
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39382
|
-
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39383
|
-
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39384
|
-
matchedCall = child;
|
|
39385
|
-
return false;
|
|
39386
|
-
}
|
|
39387
|
-
});
|
|
39388
|
-
return matchedCall;
|
|
39389
|
-
};
|
|
39390
38490
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
39391
38491
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
39392
38492
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -39403,16 +38503,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39403
38503
|
severity: "warn",
|
|
39404
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.",
|
|
39405
38505
|
create: (context) => {
|
|
39406
|
-
let cachedStoreIndex = null;
|
|
39407
|
-
const storeIndexFor = (node) => {
|
|
39408
|
-
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39409
|
-
const programRoot = findProgramRoot(node);
|
|
39410
|
-
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39411
|
-
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39412
|
-
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39413
|
-
};
|
|
39414
|
-
return cachedStoreIndex;
|
|
39415
|
-
};
|
|
39416
38506
|
const checkComponent = (componentBody) => {
|
|
39417
38507
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
39418
38508
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -39450,7 +38540,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39450
38540
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
39451
38541
|
if (!useStateInitializer) continue;
|
|
39452
38542
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39453
|
-
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
39454
38543
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
39455
38544
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
39456
38545
|
context.report({
|
|
@@ -39458,47 +38547,14 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39458
38547
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
39459
38548
|
});
|
|
39460
38549
|
}
|
|
39461
|
-
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39462
|
-
};
|
|
39463
|
-
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39464
|
-
const storeIndex = storeIndexFor(componentBody);
|
|
39465
|
-
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39466
|
-
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39467
|
-
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39468
|
-
binding,
|
|
39469
|
-
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39470
|
-
})).filter((candidate) => candidate.storeName !== null);
|
|
39471
|
-
if (snapshotBindings.length === 0) return;
|
|
39472
|
-
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39473
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39474
|
-
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39475
|
-
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39476
|
-
const depsNode = effectCall.arguments[1];
|
|
39477
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39478
|
-
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39479
|
-
const callback = getEffectCallback(effectCall);
|
|
39480
|
-
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39481
|
-
for (const { binding, storeName } of snapshotBindings) {
|
|
39482
|
-
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39483
|
-
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39484
|
-
reportedDeclarators.add(binding.declarator);
|
|
39485
|
-
context.report({
|
|
39486
|
-
node: binding.declarator,
|
|
39487
|
-
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.`
|
|
39488
|
-
});
|
|
39489
|
-
}
|
|
39490
|
-
}
|
|
39491
38550
|
};
|
|
39492
38551
|
return {
|
|
39493
38552
|
FunctionDeclaration(node) {
|
|
39494
|
-
|
|
39495
|
-
if (!functionName) return;
|
|
39496
|
-
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38553
|
+
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
39497
38554
|
checkComponent(node.body);
|
|
39498
38555
|
},
|
|
39499
38556
|
VariableDeclarator(node) {
|
|
39500
|
-
|
|
39501
|
-
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38557
|
+
if (!isComponentAssignment(node)) return;
|
|
39502
38558
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
39503
38559
|
checkComponent(node.init.body);
|
|
39504
38560
|
}
|
|
@@ -40108,7 +39164,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
40108
39164
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
40109
39165
|
const body = queryFnValue.body;
|
|
40110
39166
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
40111
|
-
if ((body.body ?? []).
|
|
39167
|
+
if ((body.body ?? []).length === 0) context.report({
|
|
40112
39168
|
node: queryFnProperty,
|
|
40113
39169
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
40114
39170
|
});
|
|
@@ -40615,6 +39671,17 @@ const renderingHoistJsx = defineRule({
|
|
|
40615
39671
|
}
|
|
40616
39672
|
});
|
|
40617
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
|
|
40618
39685
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
40619
39686
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
40620
39687
|
{
|
|
@@ -40623,19 +39690,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
40623
39690
|
},
|
|
40624
39691
|
{
|
|
40625
39692
|
display: "Date.now()",
|
|
40626
|
-
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"
|
|
40627
39694
|
},
|
|
40628
39695
|
{
|
|
40629
39696
|
display: "Math.random()",
|
|
40630
|
-
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"
|
|
40631
39698
|
},
|
|
40632
39699
|
{
|
|
40633
39700
|
display: "performance.now()",
|
|
40634
|
-
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"
|
|
40635
39702
|
},
|
|
40636
39703
|
{
|
|
40637
39704
|
display: "crypto.randomUUID()",
|
|
40638
|
-
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"
|
|
40639
39706
|
}
|
|
40640
39707
|
];
|
|
40641
39708
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -40647,6 +39714,54 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
40647
39714
|
}
|
|
40648
39715
|
return null;
|
|
40649
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
|
+
};
|
|
40650
39765
|
const isYearOnlyDateRead = (dateNode) => {
|
|
40651
39766
|
const member = dateNode.parent;
|
|
40652
39767
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -40670,6 +39785,23 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
40670
39785
|
}
|
|
40671
39786
|
return false;
|
|
40672
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
|
+
};
|
|
40673
39805
|
const renderingHydrationMismatchTime = defineRule({
|
|
40674
39806
|
id: "rendering-hydration-mismatch-time",
|
|
40675
39807
|
title: "Time or random value in JSX",
|
|
@@ -40717,35 +39849,6 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
40717
39849
|
}
|
|
40718
39850
|
});
|
|
40719
39851
|
//#endregion
|
|
40720
|
-
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40721
|
-
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40722
|
-
"toLocaleString",
|
|
40723
|
-
"toLocaleDateString",
|
|
40724
|
-
"toLocaleTimeString",
|
|
40725
|
-
"getTimezoneOffset"
|
|
40726
|
-
]);
|
|
40727
|
-
const containsLocaleEnvironmentRead = (expression) => {
|
|
40728
|
-
let readsLocaleEnvironment = false;
|
|
40729
|
-
walkAst(expression, (child) => {
|
|
40730
|
-
if (readsLocaleEnvironment) return false;
|
|
40731
|
-
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40732
|
-
if (child.object.name === "Intl") {
|
|
40733
|
-
readsLocaleEnvironment = true;
|
|
40734
|
-
return false;
|
|
40735
|
-
}
|
|
40736
|
-
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40737
|
-
readsLocaleEnvironment = true;
|
|
40738
|
-
return false;
|
|
40739
|
-
}
|
|
40740
|
-
}
|
|
40741
|
-
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)) {
|
|
40742
|
-
readsLocaleEnvironment = true;
|
|
40743
|
-
return false;
|
|
40744
|
-
}
|
|
40745
|
-
});
|
|
40746
|
-
return readsLocaleEnvironment;
|
|
40747
|
-
};
|
|
40748
|
-
//#endregion
|
|
40749
39852
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
40750
39853
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
40751
39854
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -40811,15 +39914,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
40811
39914
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
40812
39915
|
const callback = getEffectCallback(node);
|
|
40813
39916
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
40814
|
-
const bodyStatements =
|
|
40815
|
-
if (bodyStatements.length !== 1) return;
|
|
39917
|
+
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39918
|
+
if (!bodyStatements || bodyStatements.length !== 1) return;
|
|
40816
39919
|
const soleStatement = bodyStatements[0];
|
|
40817
39920
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
40818
39921
|
const expression = soleStatement.expression;
|
|
40819
39922
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
40820
39923
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
40821
39924
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40822
|
-
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
40823
39925
|
context.report({
|
|
40824
39926
|
node,
|
|
40825
39927
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -41638,9 +40740,6 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
41638
40740
|
} })
|
|
41639
40741
|
});
|
|
41640
40742
|
//#endregion
|
|
41641
|
-
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41642
|
-
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41643
|
-
//#endregion
|
|
41644
40743
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
41645
40744
|
const rerenderLazyRefInit = defineRule({
|
|
41646
40745
|
id: "rerender-lazy-ref-init",
|
|
@@ -41651,7 +40750,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41651
40750
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
41652
40751
|
create: (context) => ({ CallExpression(node) {
|
|
41653
40752
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
41654
|
-
const initializer =
|
|
40753
|
+
const initializer = node.arguments[0];
|
|
41655
40754
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
41656
40755
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
41657
40756
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -41659,7 +40758,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41659
40758
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41660
40759
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41661
40760
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41662
|
-
if (
|
|
40761
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
41663
40762
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
41664
40763
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
41665
40764
|
context.report({
|
|
@@ -41695,11 +40794,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
41695
40794
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
41696
40795
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
41697
40796
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
41698
|
-
|
|
41699
|
-
if (isNodeOfType(
|
|
41700
|
-
if (isNodeOfType(
|
|
41701
|
-
if (isNodeOfType(
|
|
41702
|
-
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 ?? []) {
|
|
41703
40802
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
41704
40803
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
41705
40804
|
if (spreadCall) return spreadCall;
|
|
@@ -41722,7 +40821,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
41722
40821
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41723
40822
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41724
40823
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41725
|
-
if (
|
|
40824
|
+
if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
41726
40825
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
41727
40826
|
if (isReactHookName(calleeName)) return;
|
|
41728
40827
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -42389,7 +41488,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
42389
41488
|
const body = reactionFn.body;
|
|
42390
41489
|
let singleAssignment = null;
|
|
42391
41490
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
42392
|
-
const statements =
|
|
41491
|
+
const statements = body.body ?? [];
|
|
42393
41492
|
if (statements.length !== 1) return;
|
|
42394
41493
|
const onlyStatement = statements[0];
|
|
42395
41494
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -42463,8 +41562,7 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
42463
41562
|
"catch",
|
|
42464
41563
|
"finally"
|
|
42465
41564
|
]);
|
|
42466
|
-
const findChainRoot = (
|
|
42467
|
-
const node = stripParenExpression(wrappedNode);
|
|
41565
|
+
const findChainRoot = (node) => {
|
|
42468
41566
|
if (isNodeOfType(node, "CallExpression")) {
|
|
42469
41567
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
42470
41568
|
calleeName: node.callee.name,
|
|
@@ -43052,21 +42150,20 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
43052
42150
|
};
|
|
43053
42151
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
43054
42152
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
43055
|
-
|
|
43056
|
-
|
|
43057
|
-
const localName = receiver.name;
|
|
42153
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
42154
|
+
const localName = callee.object.name;
|
|
43058
42155
|
const binding = findVariableInitializer(node, localName);
|
|
43059
42156
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
43060
42157
|
return localName === "Dimensions";
|
|
43061
42158
|
}
|
|
43062
|
-
if (isNodeOfType(
|
|
43063
|
-
if (getInitializerModuleSource(node,
|
|
43064
|
-
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);
|
|
43065
42162
|
if (rootName === null) return false;
|
|
43066
42163
|
const importBinding = getImportBindingForName(node, rootName);
|
|
43067
42164
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
43068
42165
|
}
|
|
43069
|
-
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";
|
|
43070
42167
|
return false;
|
|
43071
42168
|
};
|
|
43072
42169
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -43510,8 +42607,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
43510
42607
|
},
|
|
43511
42608
|
CallExpression(node) {
|
|
43512
42609
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
43513
|
-
|
|
43514
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42610
|
+
if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
|
|
43515
42611
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
43516
42612
|
const stylesArgument = node.arguments?.[0];
|
|
43517
42613
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -44361,7 +43457,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
44361
43457
|
const callee = node.callee;
|
|
44362
43458
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
44363
43459
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
44364
|
-
if (!isStaticMemberNamed(
|
|
43460
|
+
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44365
43461
|
context.report({
|
|
44366
43462
|
node,
|
|
44367
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."
|
|
@@ -44589,15 +43685,14 @@ const analyzeGestureChain = (expression) => {
|
|
|
44589
43685
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
44590
43686
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
44591
43687
|
const methodName = callee.property.name;
|
|
44592
|
-
|
|
44593
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43688
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
44594
43689
|
factoryName: methodName,
|
|
44595
43690
|
chainMethodNames,
|
|
44596
43691
|
numberOfTapsArgument
|
|
44597
43692
|
};
|
|
44598
43693
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
44599
43694
|
chainMethodNames.push(methodName);
|
|
44600
|
-
cursor =
|
|
43695
|
+
cursor = callee.object;
|
|
44601
43696
|
}
|
|
44602
43697
|
return null;
|
|
44603
43698
|
};
|
|
@@ -45122,9 +44217,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
45122
44217
|
if (!HTML_TAGS.has(elementType)) return;
|
|
45123
44218
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
45124
44219
|
if (!roleAttribute) return;
|
|
45125
|
-
const
|
|
45126
|
-
if (
|
|
45127
|
-
const roles =
|
|
44220
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
44221
|
+
if (roleValue === null) return;
|
|
44222
|
+
const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
|
|
45128
44223
|
for (const role of roles) {
|
|
45129
44224
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
45130
44225
|
if (!required) continue;
|
|
@@ -48241,9 +47336,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
48241
47336
|
};
|
|
48242
47337
|
//#endregion
|
|
48243
47338
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
48244
|
-
const buildMessageDefault = (
|
|
48245
|
-
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.`;
|
|
48246
|
-
};
|
|
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.`;
|
|
48247
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.`;
|
|
48248
47341
|
const roleSupportsAriaProps = defineRule({
|
|
48249
47342
|
id: "role-supports-aria-props",
|
|
@@ -48263,8 +47356,6 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48263
47356
|
const propName = propRawName.toLowerCase();
|
|
48264
47357
|
if (!propName.startsWith("aria-")) continue;
|
|
48265
47358
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48266
|
-
const attributeValue = attributeNode.value;
|
|
48267
|
-
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
48268
47359
|
(ariaAttributes ??= []).push({
|
|
48269
47360
|
attribute,
|
|
48270
47361
|
propName
|
|
@@ -48273,21 +47364,17 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48273
47364
|
if (!ariaAttributes) return;
|
|
48274
47365
|
const elementType = getElementType(node, context.settings);
|
|
48275
47366
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
48276
|
-
const
|
|
48277
|
-
if (
|
|
48278
|
-
|
|
48279
|
-
for (const role of roleCandidates) {
|
|
48280
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48281
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
48282
|
-
if (!supported) return;
|
|
48283
|
-
supportedSets.push(supported);
|
|
48284
|
-
}
|
|
47367
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
47368
|
+
if (!role) return;
|
|
47369
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48285
47370
|
const isImplicit = !roleAttribute;
|
|
47371
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47372
|
+
if (!supported) return;
|
|
48286
47373
|
for (const { attribute, propName } of ariaAttributes) {
|
|
48287
|
-
if (
|
|
47374
|
+
if (supported.has(propName)) continue;
|
|
48288
47375
|
context.report({
|
|
48289
47376
|
node: attribute,
|
|
48290
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47377
|
+
message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
|
|
48291
47378
|
});
|
|
48292
47379
|
}
|
|
48293
47380
|
} })
|
|
@@ -48634,6 +47721,21 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
48634
47721
|
}
|
|
48635
47722
|
return false;
|
|
48636
47723
|
};
|
|
47724
|
+
const isUseEffectEventSymbol = (symbol) => {
|
|
47725
|
+
const initializer = symbol.initializer;
|
|
47726
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47727
|
+
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47728
|
+
};
|
|
47729
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47730
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47731
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
47732
|
+
};
|
|
47733
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47734
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47735
|
+
const initializer = symbol.initializer;
|
|
47736
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47737
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47738
|
+
};
|
|
48637
47739
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
48638
47740
|
let current = node.parent;
|
|
48639
47741
|
while (current) {
|
|
@@ -48823,7 +47925,8 @@ const rulesOfHooks = defineRule({
|
|
|
48823
47925
|
},
|
|
48824
47926
|
Identifier(node) {
|
|
48825
47927
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
48826
|
-
if (!symbol || !
|
|
47928
|
+
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47929
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
48827
47930
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
48828
47931
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
48829
47932
|
context.report({
|
|
@@ -48971,8 +48074,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
48971
48074
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48972
48075
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48973
48076
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48974
|
-
const
|
|
48975
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48077
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
48976
48078
|
if (!objectName) return;
|
|
48977
48079
|
const methodName = node.callee.property.name;
|
|
48978
48080
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -49648,17 +48750,7 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
49648
48750
|
};
|
|
49649
48751
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
49650
48752
|
let chainTip = referenceIdentifier;
|
|
49651
|
-
while (chainTip.parent)
|
|
49652
|
-
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49653
|
-
chainTip = chainTip.parent;
|
|
49654
|
-
continue;
|
|
49655
|
-
}
|
|
49656
|
-
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49657
|
-
chainTip = chainTip.parent;
|
|
49658
|
-
continue;
|
|
49659
|
-
}
|
|
49660
|
-
break;
|
|
49661
|
-
}
|
|
48753
|
+
while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
|
|
49662
48754
|
return chainTip;
|
|
49663
48755
|
};
|
|
49664
48756
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -49683,8 +48775,7 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
49683
48775
|
const callee = callExpression.callee;
|
|
49684
48776
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
49685
48777
|
const methodName = getMemberPropertyName$1(callee);
|
|
49686
|
-
|
|
49687
|
-
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);
|
|
49688
48779
|
}
|
|
49689
48780
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
49690
48781
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -50414,7 +49505,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50414
49505
|
};
|
|
50415
49506
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
50416
49507
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
50417
|
-
let currentNode =
|
|
49508
|
+
let currentNode = outerNode.callee.object;
|
|
50418
49509
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
50419
49510
|
const calleeName = getCalleeName$2(currentNode);
|
|
50420
49511
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -50425,7 +49516,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50425
49516
|
}
|
|
50426
49517
|
}
|
|
50427
49518
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
50428
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode =
|
|
49519
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50429
49520
|
else break;
|
|
50430
49521
|
}
|
|
50431
49522
|
return result;
|
|
@@ -51078,7 +50169,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
51078
50169
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
51079
50170
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
51080
50171
|
if (methodName) methodNames.unshift(methodName);
|
|
51081
|
-
currentNode =
|
|
50172
|
+
currentNode = currentNode.callee.object;
|
|
51082
50173
|
}
|
|
51083
50174
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
51084
50175
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -54160,18 +53251,6 @@ const reactDoctorRules = [
|
|
|
54160
53251
|
category: "Bugs"
|
|
54161
53252
|
}
|
|
54162
53253
|
},
|
|
54163
|
-
{
|
|
54164
|
-
key: "react-doctor/no-locale-format-in-render",
|
|
54165
|
-
id: "no-locale-format-in-render",
|
|
54166
|
-
source: "react-doctor",
|
|
54167
|
-
originallyExternal: false,
|
|
54168
|
-
rule: {
|
|
54169
|
-
...noLocaleFormatInRender,
|
|
54170
|
-
framework: "global",
|
|
54171
|
-
category: "Bugs",
|
|
54172
|
-
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54173
|
-
}
|
|
54174
|
-
},
|
|
54175
53254
|
{
|
|
54176
53255
|
key: "react-doctor/no-long-transition-duration",
|
|
54177
53256
|
id: "no-long-transition-duration",
|
|
@@ -57048,7 +56127,6 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
57048
56127
|
"nextjs-no-use-search-params-without-suspense",
|
|
57049
56128
|
"no-dynamic-import-path",
|
|
57050
56129
|
"no-full-lodash-import",
|
|
57051
|
-
"no-locale-format-in-render",
|
|
57052
56130
|
"no-mutating-reducer-state",
|
|
57053
56131
|
"prefer-dynamic-import",
|
|
57054
56132
|
"rendering-hydration-mismatch-time",
|
|
@@ -57183,7 +56261,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
57183
56261
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
57184
56262
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
57185
56263
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57186
|
-
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
57187
56264
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
57188
56265
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
57189
56266
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|