oxlint-plugin-react-doctor 0.7.2-dev.43d766f → 0.7.2-dev.6ae8e46
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 +46 -0
- package/dist/index.js +1504 -531
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,6 +532,12 @@ 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
|
+
]);
|
|
535
541
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
536
542
|
"location",
|
|
537
543
|
"window",
|
|
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
595
601
|
"parseInt",
|
|
596
602
|
"parseFloat"
|
|
597
603
|
]);
|
|
604
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
605
|
+
"Date",
|
|
606
|
+
"Map",
|
|
607
|
+
"Set",
|
|
608
|
+
"WeakMap",
|
|
609
|
+
"WeakSet",
|
|
610
|
+
"WeakRef",
|
|
611
|
+
"RegExp",
|
|
612
|
+
"Error",
|
|
613
|
+
"URL",
|
|
614
|
+
"URLSearchParams",
|
|
615
|
+
"AbortController"
|
|
616
|
+
]);
|
|
598
617
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
599
618
|
"Boolean",
|
|
600
619
|
"String",
|
|
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
674
693
|
"unwatch",
|
|
675
694
|
"unlisten",
|
|
676
695
|
"unsub",
|
|
677
|
-
"abort"
|
|
696
|
+
"abort",
|
|
697
|
+
"disconnect",
|
|
698
|
+
"unobserve",
|
|
699
|
+
"close"
|
|
678
700
|
]);
|
|
679
701
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
680
702
|
"remove",
|
|
@@ -2235,6 +2257,39 @@ const anchorHasContent = defineRule({
|
|
|
2235
2257
|
}
|
|
2236
2258
|
});
|
|
2237
2259
|
//#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
|
|
2238
2293
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2239
2294
|
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).";
|
|
2240
2295
|
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.";
|
|
@@ -2262,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2262
2317
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2263
2318
|
const isInvalidHref = (value, validHrefs) => {
|
|
2264
2319
|
if (validHrefs.has(value)) return false;
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
const getStaticHrefValue = (value) => {
|
|
2268
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2269
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2270
|
-
const expression = value.expression;
|
|
2271
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2272
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2273
|
-
}
|
|
2274
|
-
return null;
|
|
2320
|
+
const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
|
|
2321
|
+
return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
|
|
2275
2322
|
};
|
|
2276
|
-
const
|
|
2277
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2323
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2278
2324
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2279
2325
|
const expression = value.expression;
|
|
2280
2326
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2281
|
-
if (isNodeOfType(expression, "Literal"))
|
|
2282
|
-
if (expression.value === null) return true;
|
|
2283
|
-
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2284
|
-
}
|
|
2285
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2286
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2287
|
-
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2288
|
-
}
|
|
2327
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2289
2328
|
}
|
|
2290
2329
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2291
2330
|
return false;
|
|
@@ -2318,9 +2357,10 @@ const anchorIsValid = defineRule({
|
|
|
2318
2357
|
});
|
|
2319
2358
|
return;
|
|
2320
2359
|
}
|
|
2321
|
-
|
|
2360
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2361
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2322
2362
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2323
|
-
if (!hasOnClick &&
|
|
2363
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2324
2364
|
context.report({
|
|
2325
2365
|
node: node.name,
|
|
2326
2366
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3376,6 +3416,25 @@ const ariaRole = defineRule({
|
|
|
3376
3416
|
if (!roleAttribute) return;
|
|
3377
3417
|
const elementType = getElementType(node, context.settings);
|
|
3378
3418
|
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
|
+
};
|
|
3379
3438
|
const value = roleAttribute.value;
|
|
3380
3439
|
if (!value) {
|
|
3381
3440
|
context.report({
|
|
@@ -3392,22 +3451,7 @@ const ariaRole = defineRule({
|
|
|
3392
3451
|
});
|
|
3393
3452
|
return;
|
|
3394
3453
|
}
|
|
3395
|
-
|
|
3396
|
-
if (stringValue.trim().length === 0) {
|
|
3397
|
-
context.report({
|
|
3398
|
-
node: roleAttribute,
|
|
3399
|
-
message: buildBaseMessage("")
|
|
3400
|
-
});
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3404
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3405
|
-
context.report({
|
|
3406
|
-
node: roleAttribute,
|
|
3407
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3408
|
-
});
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3454
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3411
3455
|
return;
|
|
3412
3456
|
}
|
|
3413
3457
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3428,6 +3472,11 @@ const ariaRole = defineRule({
|
|
|
3428
3472
|
});
|
|
3429
3473
|
return;
|
|
3430
3474
|
}
|
|
3475
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3476
|
+
if (resolvedCandidates !== null) {
|
|
3477
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3431
3480
|
return;
|
|
3432
3481
|
}
|
|
3433
3482
|
context.report({
|
|
@@ -5144,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5144
5193
|
const callee = node.callee;
|
|
5145
5194
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5146
5195
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5147
|
-
if (!isWebStorageObject(callee.object)) return;
|
|
5196
|
+
if (!isWebStorageObject(stripParenExpression(callee.object))) return;
|
|
5148
5197
|
const keyArgument = node.arguments?.[0];
|
|
5149
5198
|
if (!keyArgument) return;
|
|
5150
5199
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6084,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6084
6133
|
})
|
|
6085
6134
|
});
|
|
6086
6135
|
//#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
|
|
6087
6145
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6088
6146
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6089
6147
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6090
6148
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6091
6149
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6092
|
-
const isJsonStringifyCall = (node) =>
|
|
6093
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6094
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6095
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6096
|
-
if (node.callee.object.name !== "JSON") return false;
|
|
6097
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6098
|
-
return node.callee.property.name === "stringify";
|
|
6099
|
-
};
|
|
6150
|
+
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6100
6151
|
const resolveStringKey = (keyArg, context) => {
|
|
6101
6152
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6102
6153
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6115,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6115
6166
|
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.",
|
|
6116
6167
|
create: (context) => ({ CallExpression(node) {
|
|
6117
6168
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6118
|
-
|
|
6119
|
-
if (!
|
|
6169
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6170
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6171
|
+
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6120
6172
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6121
6173
|
if (node.callee.property.name !== "setItem") return;
|
|
6122
6174
|
const keyArg = node.arguments?.[0];
|
|
@@ -6129,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6129
6181
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6130
6182
|
context.report({
|
|
6131
6183
|
node: keyArg,
|
|
6132
|
-
message: `${
|
|
6184
|
+
message: `${receiver.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").`
|
|
6133
6185
|
});
|
|
6134
6186
|
} })
|
|
6135
6187
|
});
|
|
@@ -7610,6 +7662,96 @@ const displayName = defineRule({
|
|
|
7610
7662
|
}
|
|
7611
7663
|
});
|
|
7612
7664
|
//#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
|
|
7613
7755
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7614
7756
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7615
7757
|
if (!node || typeof node !== "object") return;
|
|
@@ -7761,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7761
7903
|
};
|
|
7762
7904
|
//#endregion
|
|
7763
7905
|
//#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
|
+
};
|
|
7764
7917
|
const findSubscribeLikeUsages = (callback) => {
|
|
7765
7918
|
const usages = [];
|
|
7766
7919
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7772,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7772
7925
|
}
|
|
7773
7926
|
walkAst(callback, (child) => {
|
|
7774
7927
|
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
|
+
}
|
|
7775
7936
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7776
7937
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7777
7938
|
usages.push({
|
|
@@ -7781,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7781
7942
|
});
|
|
7782
7943
|
return;
|
|
7783
7944
|
}
|
|
7784
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7945
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
7785
7946
|
kind: "subscribe",
|
|
7786
7947
|
node: child,
|
|
7787
7948
|
resourceName: child.callee.property.name
|
|
@@ -7797,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7797
7958
|
};
|
|
7798
7959
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7799
7960
|
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
|
+
});
|
|
7800
7968
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7801
7969
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7802
7970
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7804,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7804
7972
|
const bindingName = declarator.id.name;
|
|
7805
7973
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7806
7974
|
const init = declarator.init;
|
|
7807
|
-
if (!init
|
|
7975
|
+
if (!init) continue;
|
|
7976
|
+
if (isSocketConstruction(init)) {
|
|
7977
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7978
|
+
continue;
|
|
7979
|
+
}
|
|
7980
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7808
7981
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7809
7982
|
bindings.subscriptionNames.add(bindingName);
|
|
7810
7983
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7834,6 +8007,22 @@ const getRangeStart = (node) => {
|
|
|
7834
8007
|
const rangeStart = node.range?.[0];
|
|
7835
8008
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7836
8009
|
};
|
|
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
|
+
};
|
|
7837
8026
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7838
8027
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
7839
8028
|
const returnStart = getRangeStart(returnStatement);
|
|
@@ -7856,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7856
8045
|
});
|
|
7857
8046
|
return didFindCleanupReturn;
|
|
7858
8047
|
};
|
|
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
|
+
};
|
|
7859
8172
|
const effectNeedsCleanup = defineRule({
|
|
7860
8173
|
id: "effect-needs-cleanup",
|
|
7861
8174
|
title: "Effect subscription or timer never cleaned up",
|
|
7862
8175
|
severity: "error",
|
|
7863
8176
|
tags: ["test-noise"],
|
|
7864
|
-
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.",
|
|
7865
|
-
create: (context) =>
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
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, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8178
|
+
create: (context) => {
|
|
8179
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8180
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8181
|
+
if (!leak) return;
|
|
8182
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8183
|
+
context.report({
|
|
8184
|
+
node: leak.node,
|
|
8185
|
+
message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
|
|
8186
|
+
});
|
|
8187
|
+
};
|
|
8188
|
+
return {
|
|
8189
|
+
CallExpression(node) {
|
|
8190
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8191
|
+
const retainedCallback = getEffectCallback(node);
|
|
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
|
+
}
|
|
7879
8219
|
});
|
|
7880
8220
|
//#endregion
|
|
7881
8221
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8434,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8434
8774
|
return computedCaptures;
|
|
8435
8775
|
};
|
|
8436
8776
|
//#endregion
|
|
8437
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8438
|
-
const isReactHookName = (name) => {
|
|
8439
|
-
if (!name.startsWith("use")) return false;
|
|
8440
|
-
if (name.length === 3) return true;
|
|
8441
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
8442
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8443
|
-
};
|
|
8444
|
-
//#endregion
|
|
8445
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8446
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8447
|
-
//#endregion
|
|
8448
8777
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8449
8778
|
const reactHocCalleeName = (callee) => {
|
|
8450
8779
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8651,6 +8980,24 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8651
8980
|
return false;
|
|
8652
8981
|
};
|
|
8653
8982
|
//#endregion
|
|
8983
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
8984
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
8985
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
8986
|
+
if (importSource === null) return false;
|
|
8987
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
8988
|
+
};
|
|
8989
|
+
//#endregion
|
|
8990
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
8991
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
8992
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
8993
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
8994
|
+
};
|
|
8995
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
8996
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
8997
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
8998
|
+
return false;
|
|
8999
|
+
};
|
|
9000
|
+
//#endregion
|
|
8654
9001
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
8655
9002
|
/**
|
|
8656
9003
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -8715,10 +9062,11 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
8715
9062
|
}
|
|
8716
9063
|
return false;
|
|
8717
9064
|
};
|
|
8718
|
-
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
9065
|
+
const symbolHasUseEffectEventOrigin = (symbol, scopes) => {
|
|
8719
9066
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8720
9067
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8721
|
-
|
|
9068
|
+
if (getHookName(initializer.callee) !== "useEffectEvent") return false;
|
|
9069
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
8722
9070
|
};
|
|
8723
9071
|
const getFunctionValueNode = (symbol) => {
|
|
8724
9072
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
@@ -9455,7 +9803,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9455
9803
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9456
9804
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9457
9805
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9458
|
-
if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
|
|
9806
|
+
if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9459
9807
|
context.report({
|
|
9460
9808
|
node: elementNode,
|
|
9461
9809
|
message: buildEffectEventDepMessage()
|
|
@@ -9616,7 +9964,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9964
|
});
|
|
9617
9965
|
//#endregion
|
|
9618
9966
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9967
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9968
|
const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
|
|
9621
9969
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9970
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9628,7 +9976,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9976
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
9629
9977
|
create: (context) => {
|
|
9630
9978
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9979
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9980
|
return {
|
|
9633
9981
|
MemberExpression(node) {
|
|
9634
9982
|
if (!node.computed) return;
|
|
@@ -9881,7 +10229,10 @@ const PRAGMA = "React";
|
|
|
9881
10229
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9882
10230
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9883
10231
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9884
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10232
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10233
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10234
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10235
|
+
}
|
|
9885
10236
|
return true;
|
|
9886
10237
|
};
|
|
9887
10238
|
//#endregion
|
|
@@ -11046,8 +11397,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11397
|
if (node.attributes.length === 0) return;
|
|
11047
11398
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11399
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11400
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11401
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11402
|
let hasInteractiveHandler = false;
|
|
11052
11403
|
for (const attribute of node.attributes) {
|
|
11053
11404
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11412,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11412
|
const elementType = getElementType(node, context.settings);
|
|
11062
11413
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11414
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11064
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11065
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11066
11415
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11416
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11417
|
+
for (const role of roleCandidates) {
|
|
11418
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11419
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11420
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11421
|
+
}
|
|
11422
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11423
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11424
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11425
|
context.report({
|
|
11070
11426
|
node,
|
|
11071
11427
|
message
|
|
@@ -11254,16 +11610,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11254
11610
|
return names;
|
|
11255
11611
|
};
|
|
11256
11612
|
//#endregion
|
|
11257
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11258
|
-
const findEnclosingFunction = (node) => {
|
|
11259
|
-
let cursor = node.parent;
|
|
11260
|
-
while (cursor) {
|
|
11261
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
11262
|
-
cursor = cursor.parent ?? null;
|
|
11263
|
-
}
|
|
11264
|
-
return null;
|
|
11265
|
-
};
|
|
11266
|
-
//#endregion
|
|
11267
11613
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11268
11614
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11269
11615
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -11963,14 +12309,15 @@ const jsCacheStorage = defineRule({
|
|
|
11963
12309
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
11964
12310
|
CallExpression(node) {
|
|
11965
12311
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
11966
|
-
|
|
12312
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12313
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11967
12314
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
11968
12315
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
11969
12316
|
const storageKey = String(node.arguments[0].value);
|
|
11970
12317
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
11971
12318
|
storageReadCounts.set(storageKey, readCount);
|
|
11972
12319
|
if (readCount === 2) {
|
|
11973
|
-
const storageName =
|
|
12320
|
+
const storageName = receiver.name;
|
|
11974
12321
|
context.report({
|
|
11975
12322
|
node,
|
|
11976
12323
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -11984,13 +12331,16 @@ const jsCacheStorage = defineRule({
|
|
|
11984
12331
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
11985
12332
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
11986
12333
|
const callee = callExpression.callee;
|
|
11987
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
12334
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12335
|
+
const receiver = stripParenExpression(callee.object);
|
|
12336
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12337
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12338
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12339
|
+
return true;
|
|
12340
|
+
}
|
|
12341
|
+
return false;
|
|
11993
12342
|
}
|
|
12343
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
11994
12344
|
return false;
|
|
11995
12345
|
};
|
|
11996
12346
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12002,10 +12352,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12002
12352
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12003
12353
|
let cursor = receiverNode;
|
|
12004
12354
|
while (cursor) {
|
|
12005
|
-
|
|
12006
|
-
cursor = cursor.expression;
|
|
12007
|
-
continue;
|
|
12008
|
-
}
|
|
12355
|
+
cursor = stripParenExpression(cursor);
|
|
12009
12356
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12010
12357
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12011
12358
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12081,10 +12428,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12081
12428
|
let hops = 0;
|
|
12082
12429
|
while (cursor && hops < 12) {
|
|
12083
12430
|
hops += 1;
|
|
12084
|
-
|
|
12085
|
-
cursor = cursor.expression;
|
|
12086
|
-
continue;
|
|
12087
|
-
}
|
|
12431
|
+
cursor = stripParenExpression(cursor);
|
|
12088
12432
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12089
12433
|
const callee = cursor.callee;
|
|
12090
12434
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12108,10 +12452,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12108
12452
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12109
12453
|
let cursor = receiverNode;
|
|
12110
12454
|
while (cursor) {
|
|
12111
|
-
|
|
12112
|
-
cursor = cursor.expression;
|
|
12113
|
-
continue;
|
|
12114
|
-
}
|
|
12455
|
+
cursor = stripParenExpression(cursor);
|
|
12115
12456
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12116
12457
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12117
12458
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12176,7 +12517,7 @@ const jsCombineIterations = defineRule({
|
|
|
12176
12517
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12177
12518
|
const outerMethod = node.callee.property.name;
|
|
12178
12519
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12179
|
-
const innerCall = node.callee.object;
|
|
12520
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12180
12521
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12181
12522
|
const innerMethod = innerCall.callee.property.name;
|
|
12182
12523
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12249,10 +12590,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12249
12590
|
if (!filterArgument) return;
|
|
12250
12591
|
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;
|
|
12251
12592
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12252
|
-
const innerCall = node.callee.object;
|
|
12593
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12253
12594
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12254
12595
|
if (innerCall.callee.property.name !== "map") return;
|
|
12255
|
-
const receiver = innerCall.callee.object;
|
|
12596
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12256
12597
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12257
12598
|
const elements = receiver.elements ?? [];
|
|
12258
12599
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13512,7 +13853,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13512
13853
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13513
13854
|
create: (context) => ({ CallExpression(node) {
|
|
13514
13855
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13515
|
-
const receiver = node.callee.object;
|
|
13856
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13516
13857
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13517
13858
|
const spreadArgument = receiver.elements[0].argument;
|
|
13518
13859
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18549,7 +18890,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18549
18890
|
};
|
|
18550
18891
|
const describeClientSideNavigation = (node) => {
|
|
18551
18892
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18552
|
-
const
|
|
18893
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18894
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18553
18895
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18554
18896
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18555
18897
|
}
|
|
@@ -19169,7 +19511,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19169
19511
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19170
19512
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19171
19513
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19172
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19514
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19173
19515
|
return node.callee.property.name;
|
|
19174
19516
|
};
|
|
19175
19517
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19183,7 +19525,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19183
19525
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19184
19526
|
const { property, object } = node.callee;
|
|
19185
19527
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19186
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19528
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19187
19529
|
return true;
|
|
19188
19530
|
};
|
|
19189
19531
|
const getDbCallDescription = (node) => {
|
|
@@ -19191,7 +19533,8 @@ const getDbCallDescription = (node) => {
|
|
|
19191
19533
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19192
19534
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19193
19535
|
const methodName = node.callee.property.name;
|
|
19194
|
-
const
|
|
19536
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19537
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19195
19538
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19196
19539
|
};
|
|
19197
19540
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20591,13 +20934,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20591
20934
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20592
20935
|
let node = ref.identifier;
|
|
20593
20936
|
let parent = node.parent;
|
|
20594
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20937
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20595
20938
|
node = parent;
|
|
20596
20939
|
parent = node.parent;
|
|
20597
20940
|
}
|
|
20598
20941
|
if (current.callee === node) return current;
|
|
20599
20942
|
}
|
|
20600
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20943
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20601
20944
|
return null;
|
|
20602
20945
|
};
|
|
20603
20946
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20732,7 +21075,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20732
21075
|
const importDeclaration = declarationNode.parent;
|
|
20733
21076
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20734
21077
|
}));
|
|
20735
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21078
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20736
21079
|
if (!node) return false;
|
|
20737
21080
|
if (isNodeOfType(node, "Identifier")) {
|
|
20738
21081
|
if (node.name === hookName) return true;
|
|
@@ -20741,15 +21084,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20741
21084
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20742
21085
|
return false;
|
|
20743
21086
|
}
|
|
20744
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21087
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21088
|
+
const receiver = stripParenExpression(node.object);
|
|
21089
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21090
|
+
}
|
|
20745
21091
|
return false;
|
|
20746
21092
|
};
|
|
20747
21093
|
const isUseEffect = (node) => {
|
|
20748
21094
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20749
21095
|
const callee = node.callee;
|
|
20750
21096
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20751
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20752
|
-
|
|
21097
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21098
|
+
const receiver = stripParenExpression(callee.object);
|
|
21099
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20753
21100
|
};
|
|
20754
21101
|
const getEffectFn = (analysis, node) => {
|
|
20755
21102
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20777,7 +21124,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20777
21124
|
const node = def.node;
|
|
20778
21125
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20779
21126
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20780
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21127
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20781
21128
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20782
21129
|
const elements = node.id.elements ?? [];
|
|
20783
21130
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20788,7 +21135,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20788
21135
|
const node = def.node;
|
|
20789
21136
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20790
21137
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20791
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21138
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20792
21139
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20793
21140
|
const elements = node.id.elements ?? [];
|
|
20794
21141
|
if (elements.length !== 2) return false;
|
|
@@ -20835,7 +21182,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20835
21182
|
const node = def.node;
|
|
20836
21183
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20837
21184
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20838
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21185
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20839
21186
|
}));
|
|
20840
21187
|
const isRefCurrent = (ref) => {
|
|
20841
21188
|
const parent = ref.identifier.parent;
|
|
@@ -20848,11 +21195,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20848
21195
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20849
21196
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20850
21197
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20851
|
-
|
|
20852
|
-
|
|
21198
|
+
let effectiveNode = ref.identifier;
|
|
21199
|
+
let parent = effectiveNode.parent;
|
|
21200
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21201
|
+
effectiveNode = parent;
|
|
21202
|
+
parent = effectiveNode.parent;
|
|
21203
|
+
}
|
|
20853
21204
|
if (!parent) return false;
|
|
20854
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20855
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21205
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21206
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20856
21207
|
const memberParent = parent.parent;
|
|
20857
21208
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20858
21209
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20863,7 +21214,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20863
21214
|
};
|
|
20864
21215
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20865
21216
|
const getUseStateDecl = (analysis, ref) => {
|
|
20866
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21217
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20867
21218
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20868
21219
|
return node ?? null;
|
|
20869
21220
|
};
|
|
@@ -21068,7 +21419,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21068
21419
|
const noAdjustStateOnPropChange = defineRule({
|
|
21069
21420
|
id: "no-adjust-state-on-prop-change",
|
|
21070
21421
|
title: "State synced to a prop inside an effect",
|
|
21071
|
-
severity: "
|
|
21422
|
+
severity: "warn",
|
|
21072
21423
|
tags: ["test-noise"],
|
|
21073
21424
|
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",
|
|
21074
21425
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21109,7 +21460,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21109
21460
|
"summary",
|
|
21110
21461
|
"textarea"
|
|
21111
21462
|
]);
|
|
21463
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21464
|
+
const value = attribute.value;
|
|
21465
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21466
|
+
const expression = value.expression;
|
|
21467
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21468
|
+
};
|
|
21469
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21470
|
+
"button",
|
|
21471
|
+
"input",
|
|
21472
|
+
"select",
|
|
21473
|
+
"textarea"
|
|
21474
|
+
]);
|
|
21112
21475
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21476
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21477
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21478
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21479
|
+
}
|
|
21113
21480
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21114
21481
|
switch (tagName) {
|
|
21115
21482
|
case "input": {
|
|
@@ -21123,7 +21490,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21123
21490
|
case "a":
|
|
21124
21491
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21125
21492
|
case "audio":
|
|
21126
|
-
case "video":
|
|
21493
|
+
case "video": {
|
|
21494
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21495
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21496
|
+
}
|
|
21127
21497
|
default: return false;
|
|
21128
21498
|
}
|
|
21129
21499
|
};
|
|
@@ -22087,7 +22457,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22087
22457
|
"some"
|
|
22088
22458
|
]);
|
|
22089
22459
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22090
|
-
const isPositionallyStableIterationReceiver = (
|
|
22460
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22461
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22091
22462
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22092
22463
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22093
22464
|
const only = receiver.elements[0];
|
|
@@ -22098,17 +22469,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22098
22469
|
}
|
|
22099
22470
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22100
22471
|
const callee = receiver.callee;
|
|
22101
|
-
if (
|
|
22472
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22102
22473
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22103
22474
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22104
22475
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22105
22476
|
return false;
|
|
22106
22477
|
};
|
|
22107
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
22108
|
-
if (parentCall.arguments[1] !== callback) return false;
|
|
22109
|
-
const callee = parentCall.callee;
|
|
22110
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
|
|
22111
|
-
};
|
|
22478
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22112
22479
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22113
22480
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22114
22481
|
for (const property of source.properties ?? []) {
|
|
@@ -22167,18 +22534,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22167
22534
|
return false;
|
|
22168
22535
|
}
|
|
22169
22536
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22170
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22537
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22171
22538
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22172
22539
|
}
|
|
22173
22540
|
return false;
|
|
22174
22541
|
};
|
|
22175
|
-
const isReactCloneElement = (callExpression) =>
|
|
22176
|
-
const callee = callExpression.callee;
|
|
22177
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22178
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22179
|
-
if (callee.property.name !== "cloneElement") return false;
|
|
22180
|
-
return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
|
|
22181
|
-
};
|
|
22542
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22182
22543
|
const noArrayIndexKey = defineRule({
|
|
22183
22544
|
id: "no-array-index-key",
|
|
22184
22545
|
title: "Array index used as a key",
|
|
@@ -22477,6 +22838,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22477
22838
|
const section = manifest[sectionName];
|
|
22478
22839
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22479
22840
|
});
|
|
22841
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22842
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22843
|
+
return false;
|
|
22844
|
+
};
|
|
22480
22845
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22481
22846
|
const classifyPackagePlatform = (filename) => {
|
|
22482
22847
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22493,9 +22858,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22493
22858
|
return result;
|
|
22494
22859
|
};
|
|
22495
22860
|
//#endregion
|
|
22496
|
-
//#region src/plugin/utils/is-
|
|
22497
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22861
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22499
22862
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22500
22863
|
const resolveRealDirectory = (directory) => {
|
|
22501
22864
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22516,6 +22879,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22516
22879
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22517
22880
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22518
22881
|
};
|
|
22882
|
+
//#endregion
|
|
22883
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22884
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22885
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22519
22886
|
const classifyReactNativeFileTarget = (context) => {
|
|
22520
22887
|
const rawFilename = context.filename;
|
|
22521
22888
|
if (!rawFilename) return "unknown";
|
|
@@ -22896,7 +23263,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22896
23263
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22897
23264
|
return false;
|
|
22898
23265
|
};
|
|
22899
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23266
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22900
23267
|
"forEach",
|
|
22901
23268
|
"map",
|
|
22902
23269
|
"filter",
|
|
@@ -22917,31 +23284,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22917
23284
|
if (parent.callee === functionNode) return true;
|
|
22918
23285
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22919
23286
|
const callee = parent.callee;
|
|
22920
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23287
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22921
23288
|
};
|
|
22922
23289
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
22923
|
-
const
|
|
22924
|
-
|
|
22925
|
-
if (statement.alternate) return null;
|
|
22926
|
-
const consequent = statement.consequent;
|
|
22927
|
-
if (isTerminatingStatement(consequent)) return consequent;
|
|
22928
|
-
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
22929
|
-
return null;
|
|
22930
|
-
};
|
|
22931
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23290
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23291
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22932
23292
|
let fallThroughCount = 0;
|
|
22933
|
-
let
|
|
23293
|
+
let maxTerminatedCount = 0;
|
|
22934
23294
|
for (const statement of statements) {
|
|
22935
|
-
if (
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
23295
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23296
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23297
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23298
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23299
|
+
fallThroughCount: 0,
|
|
23300
|
+
maxTerminatedCount: 0,
|
|
23301
|
+
doAllPathsTerminate: false
|
|
23302
|
+
};
|
|
23303
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23304
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23305
|
+
fallThroughCount: 0,
|
|
23306
|
+
maxTerminatedCount,
|
|
23307
|
+
doAllPathsTerminate: true
|
|
23308
|
+
};
|
|
23309
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23310
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22939
23311
|
continue;
|
|
22940
23312
|
}
|
|
22941
|
-
if (isTerminatingStatement(statement))
|
|
23313
|
+
if (isTerminatingStatement(statement)) {
|
|
23314
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23315
|
+
return {
|
|
23316
|
+
fallThroughCount: 0,
|
|
23317
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23318
|
+
doAllPathsTerminate: true
|
|
23319
|
+
};
|
|
23320
|
+
}
|
|
22942
23321
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22943
23322
|
}
|
|
22944
|
-
return
|
|
23323
|
+
return {
|
|
23324
|
+
fallThroughCount,
|
|
23325
|
+
maxTerminatedCount,
|
|
23326
|
+
doAllPathsTerminate: false
|
|
23327
|
+
};
|
|
23328
|
+
};
|
|
23329
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23330
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23331
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22945
23332
|
};
|
|
22946
23333
|
const collectLocalHelperFunctions = (root) => {
|
|
22947
23334
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -22968,14 +23355,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22968
23355
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22969
23356
|
return parent.parent === effectCallback.body;
|
|
22970
23357
|
};
|
|
23358
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23359
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23360
|
+
const body = functionNode.body;
|
|
23361
|
+
if (!body) return 0;
|
|
23362
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23363
|
+
};
|
|
22971
23364
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22972
23365
|
if (!node || typeof node !== "object") return 0;
|
|
22973
|
-
if (
|
|
23366
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22974
23367
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22975
23368
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22976
23369
|
const consequent = node.consequent;
|
|
22977
23370
|
const alternate = node.alternate;
|
|
22978
|
-
|
|
23371
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23372
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23373
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23374
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22979
23375
|
}
|
|
22980
23376
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22981
23377
|
let maxRunSetters = 0;
|
|
@@ -23005,28 +23401,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23005
23401
|
}
|
|
23006
23402
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23007
23403
|
let nestedSettersInArgs = 0;
|
|
23008
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23404
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23009
23405
|
return 1 + nestedSettersInArgs;
|
|
23010
23406
|
}
|
|
23011
23407
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23012
23408
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23013
23409
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23014
23410
|
context.activeHelpers.add(helperFunction);
|
|
23015
|
-
let helperCount =
|
|
23411
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23016
23412
|
context.activeHelpers.delete(helperFunction);
|
|
23017
23413
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23018
23414
|
return helperCount;
|
|
23019
23415
|
}
|
|
23020
23416
|
}
|
|
23021
|
-
const
|
|
23417
|
+
const countChild = (child) => {
|
|
23418
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23419
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23420
|
+
};
|
|
23022
23421
|
let total = 0;
|
|
23023
23422
|
const nodeRecord = node;
|
|
23024
23423
|
for (const key of Object.keys(nodeRecord)) {
|
|
23025
23424
|
if (key === "parent") continue;
|
|
23026
23425
|
const child = nodeRecord[key];
|
|
23027
23426
|
if (Array.isArray(child)) {
|
|
23028
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item
|
|
23029
|
-
} else if (child && typeof child === "object" && "type" in child
|
|
23427
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23428
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23030
23429
|
}
|
|
23031
23430
|
return total;
|
|
23032
23431
|
};
|
|
@@ -23060,7 +23459,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23060
23459
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23061
23460
|
const firstStatement = (body.body ?? [])[0];
|
|
23062
23461
|
if (!firstStatement) return false;
|
|
23063
|
-
if (!
|
|
23462
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23463
|
+
const consequent = firstStatement.consequent;
|
|
23464
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23064
23465
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23065
23466
|
};
|
|
23066
23467
|
const noCascadingSetState = defineRule({
|
|
@@ -23075,7 +23476,7 @@ const noCascadingSetState = defineRule({
|
|
|
23075
23476
|
const callback = getEffectCallback(node);
|
|
23076
23477
|
if (!callback) return;
|
|
23077
23478
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23078
|
-
const setStateCallCount =
|
|
23479
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23079
23480
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23080
23481
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23081
23482
|
effectCallback: callback
|
|
@@ -23419,40 +23820,6 @@ const noCloneElement = defineRule({
|
|
|
23419
23820
|
} })
|
|
23420
23821
|
});
|
|
23421
23822
|
//#endregion
|
|
23422
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23423
|
-
const hocWrapperCalleeName = (callee) => {
|
|
23424
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23425
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23426
|
-
return null;
|
|
23427
|
-
};
|
|
23428
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23429
|
-
let current = functionNode;
|
|
23430
|
-
for (;;) {
|
|
23431
|
-
const parent = current.parent;
|
|
23432
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23433
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23434
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23435
|
-
current = parent;
|
|
23436
|
-
continue;
|
|
23437
|
-
}
|
|
23438
|
-
}
|
|
23439
|
-
break;
|
|
23440
|
-
}
|
|
23441
|
-
const binding = current.parent;
|
|
23442
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23443
|
-
return null;
|
|
23444
|
-
};
|
|
23445
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23446
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23447
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
23448
|
-
};
|
|
23449
|
-
//#endregion
|
|
23450
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23451
|
-
const enclosingComponentOrHookName = (node) => {
|
|
23452
|
-
const functionNode = findEnclosingFunction(node);
|
|
23453
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23454
|
-
};
|
|
23455
|
-
//#endregion
|
|
23456
23823
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23457
23824
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23458
23825
|
const CONTEXT_MODULES = [
|
|
@@ -24423,11 +24790,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24423
24790
|
return isInitialOnlyPropName(arg.name);
|
|
24424
24791
|
};
|
|
24425
24792
|
//#endregion
|
|
24793
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24794
|
+
const isNoOpStatement = (statement) => {
|
|
24795
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24796
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24797
|
+
const expression = stripParenExpression(statement.expression);
|
|
24798
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24799
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24800
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24801
|
+
return false;
|
|
24802
|
+
};
|
|
24803
|
+
//#endregion
|
|
24426
24804
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24427
24805
|
const getCallbackStatements = (callback) => {
|
|
24428
24806
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24429
|
-
|
|
24430
|
-
return callback.body ? [callback.body] : [];
|
|
24807
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24431
24808
|
};
|
|
24432
24809
|
//#endregion
|
|
24433
24810
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24466,6 +24843,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24466
24843
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24467
24844
|
}
|
|
24468
24845
|
};
|
|
24846
|
+
const flattenGuardedStatements = (statements) => {
|
|
24847
|
+
const flattened = [];
|
|
24848
|
+
for (const statement of statements) {
|
|
24849
|
+
if (isNoOpStatement(statement)) continue;
|
|
24850
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24851
|
+
flattened.push(statement);
|
|
24852
|
+
continue;
|
|
24853
|
+
}
|
|
24854
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24855
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24856
|
+
if (!branch) continue;
|
|
24857
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24858
|
+
if (flattenedBranch === null) return null;
|
|
24859
|
+
flattened.push(...flattenedBranch);
|
|
24860
|
+
}
|
|
24861
|
+
continue;
|
|
24862
|
+
}
|
|
24863
|
+
return null;
|
|
24864
|
+
}
|
|
24865
|
+
return flattened;
|
|
24866
|
+
};
|
|
24469
24867
|
const noDerivedStateEffect = defineRule({
|
|
24470
24868
|
id: "no-derived-state-effect",
|
|
24471
24869
|
title: "Derived state stored in an effect",
|
|
@@ -24497,8 +24895,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24497
24895
|
}
|
|
24498
24896
|
}
|
|
24499
24897
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24500
|
-
const statements = getCallbackStatements(callback);
|
|
24501
|
-
if (statements.length === 0) return;
|
|
24898
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24899
|
+
if (statements === null || statements.length === 0) return;
|
|
24502
24900
|
if (!statements.every((statement) => {
|
|
24503
24901
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24504
24902
|
const expression = statement.expression;
|
|
@@ -25133,7 +25531,7 @@ const noDidMountSetState = defineRule({
|
|
|
25133
25531
|
const { mode } = resolveSettings$20(context.settings);
|
|
25134
25532
|
return { CallExpression(node) {
|
|
25135
25533
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25136
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25534
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25137
25535
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25138
25536
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25139
25537
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25258,7 +25656,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25258
25656
|
const { mode } = resolveSettings$19(context.settings);
|
|
25259
25657
|
return { CallExpression(node) {
|
|
25260
25658
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25261
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25659
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25262
25660
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25263
25661
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25264
25662
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25381,6 +25779,33 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25381
25779
|
}
|
|
25382
25780
|
return producesPlainStateValue(unwrapped);
|
|
25383
25781
|
};
|
|
25782
|
+
const producesOpaqueInstanceValue = (expression) => {
|
|
25783
|
+
if (isNodeOfType(expression, "NewExpression")) return true;
|
|
25784
|
+
return isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "MemberExpression") && !isPlainDataProducerCall(expression);
|
|
25785
|
+
};
|
|
25786
|
+
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25787
|
+
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25788
|
+
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25789
|
+
walkAst(componentBody, (node) => {
|
|
25790
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25791
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25792
|
+
const setterName = node.callee.name;
|
|
25793
|
+
if (!setterNames.has(setterName)) return;
|
|
25794
|
+
const argument = node.arguments?.[0];
|
|
25795
|
+
if (!argument) return;
|
|
25796
|
+
const unwrapped = stripParenExpression(argument);
|
|
25797
|
+
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
25798
|
+
if (producesPlainStateValue(unwrapped)) {
|
|
25799
|
+
plainFedSetterNames.add(setterName);
|
|
25800
|
+
return;
|
|
25801
|
+
}
|
|
25802
|
+
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25803
|
+
});
|
|
25804
|
+
return {
|
|
25805
|
+
plainFedSetterNames,
|
|
25806
|
+
opaqueFedSetterNames
|
|
25807
|
+
};
|
|
25808
|
+
};
|
|
25384
25809
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25385
25810
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25386
25811
|
walkAst(componentBody, (node) => {
|
|
@@ -25450,11 +25875,15 @@ const noDirectStateMutation = defineRule({
|
|
|
25450
25875
|
if (bindings.length === 0) return;
|
|
25451
25876
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25452
25877
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25878
|
+
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25453
25879
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25454
25880
|
for (const binding of bindings) {
|
|
25455
25881
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25456
25882
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25457
|
-
|
|
25883
|
+
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
25884
|
+
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
25885
|
+
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
25886
|
+
plainObjectStateValueNames.add(binding.valueName);
|
|
25458
25887
|
}
|
|
25459
25888
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25460
25889
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -25579,9 +26008,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25579
26008
|
create: (context) => ({ CallExpression(node) {
|
|
25580
26009
|
const callee = node.callee;
|
|
25581
26010
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25582
|
-
|
|
26011
|
+
const receiver = stripParenExpression(callee.object);
|
|
26012
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25583
26013
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25584
|
-
if (context.scopes.symbolFor(
|
|
26014
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25585
26015
|
if (!importsReactViewTransition(node)) return;
|
|
25586
26016
|
context.report({
|
|
25587
26017
|
node,
|
|
@@ -25601,7 +26031,8 @@ const noDocumentWrite = defineRule({
|
|
|
25601
26031
|
create: (context) => ({ CallExpression(node) {
|
|
25602
26032
|
const callee = node.callee;
|
|
25603
26033
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25604
|
-
|
|
26034
|
+
const receiver = stripParenExpression(callee.object);
|
|
26035
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25605
26036
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25606
26037
|
context.report({
|
|
25607
26038
|
node,
|
|
@@ -26233,13 +26664,6 @@ const noEffectEventHandler = defineRule({
|
|
|
26233
26664
|
}
|
|
26234
26665
|
});
|
|
26235
26666
|
//#endregion
|
|
26236
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
26237
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
26238
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
26239
|
-
if (importSource === null) return false;
|
|
26240
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
26241
|
-
};
|
|
26242
|
-
//#endregion
|
|
26243
26667
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26244
26668
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26245
26669
|
const componentBindingStack = [];
|
|
@@ -26293,7 +26717,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26293
26717
|
const initializer = declaratorNode.init;
|
|
26294
26718
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26295
26719
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26296
|
-
if (
|
|
26720
|
+
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
26297
26721
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26298
26722
|
} });
|
|
26299
26723
|
return {
|
|
@@ -28873,7 +29297,7 @@ const noIsMounted = defineRule({
|
|
|
28873
29297
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28874
29298
|
create: (context) => ({ CallExpression(node) {
|
|
28875
29299
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28876
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29300
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28877
29301
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28878
29302
|
if (!getParentComponent(node)) return;
|
|
28879
29303
|
context.report({
|
|
@@ -28888,7 +29312,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28888
29312
|
const isJsonMethodCall = (node, method) => {
|
|
28889
29313
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28890
29314
|
const callee = node.callee;
|
|
28891
|
-
|
|
29315
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29316
|
+
const receiver = stripParenExpression(callee.object);
|
|
29317
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28892
29318
|
};
|
|
28893
29319
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28894
29320
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -28973,7 +29399,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
28973
29399
|
const noJsxElementType = defineRule({
|
|
28974
29400
|
id: "no-jsx-element-type",
|
|
28975
29401
|
title: "No JSX.Element",
|
|
28976
|
-
severity: "
|
|
29402
|
+
severity: "warn",
|
|
28977
29403
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
28978
29404
|
create: (context) => {
|
|
28979
29405
|
let isJsxImported = false;
|
|
@@ -29299,6 +29725,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29299
29725
|
}
|
|
29300
29726
|
});
|
|
29301
29727
|
//#endregion
|
|
29728
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29729
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29730
|
+
"map",
|
|
29731
|
+
"filter",
|
|
29732
|
+
"forEach",
|
|
29733
|
+
"flatMap",
|
|
29734
|
+
"reduce",
|
|
29735
|
+
"reduceRight",
|
|
29736
|
+
"some",
|
|
29737
|
+
"every",
|
|
29738
|
+
"find",
|
|
29739
|
+
"findIndex",
|
|
29740
|
+
"findLast",
|
|
29741
|
+
"findLastIndex",
|
|
29742
|
+
"sort",
|
|
29743
|
+
"toSorted"
|
|
29744
|
+
]);
|
|
29745
|
+
const executesDuringRender = (functionNode) => {
|
|
29746
|
+
const parent = functionNode.parent;
|
|
29747
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29748
|
+
if (parent.callee === functionNode) return true;
|
|
29749
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29750
|
+
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;
|
|
29751
|
+
};
|
|
29752
|
+
//#endregion
|
|
29753
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29754
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29755
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29756
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29757
|
+
return false;
|
|
29758
|
+
};
|
|
29759
|
+
//#endregion
|
|
29760
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29761
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29762
|
+
let ancestor = bindingIdentifier.parent;
|
|
29763
|
+
while (ancestor) {
|
|
29764
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29765
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29766
|
+
ancestor = ancestor.parent ?? null;
|
|
29767
|
+
}
|
|
29768
|
+
return null;
|
|
29769
|
+
};
|
|
29770
|
+
//#endregion
|
|
29771
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29772
|
+
const isFalsyLiteral = (node) => {
|
|
29773
|
+
if (!node) return true;
|
|
29774
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29775
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29776
|
+
};
|
|
29777
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29778
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29779
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29780
|
+
if (!binding) return false;
|
|
29781
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29782
|
+
if (!declarator?.init) return false;
|
|
29783
|
+
const init = stripParenExpression(declarator.init);
|
|
29784
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29785
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29786
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29787
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29788
|
+
};
|
|
29789
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29790
|
+
//#endregion
|
|
29791
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29792
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29793
|
+
let cursor = node;
|
|
29794
|
+
let parent = node.parent;
|
|
29795
|
+
while (parent) {
|
|
29796
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29797
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29798
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29799
|
+
cursor = parent;
|
|
29800
|
+
parent = parent.parent ?? null;
|
|
29801
|
+
}
|
|
29802
|
+
return false;
|
|
29803
|
+
};
|
|
29804
|
+
//#endregion
|
|
29805
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29806
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29807
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29808
|
+
const unwrapped = stripParenExpression(expression);
|
|
29809
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29810
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29811
|
+
const property = unwrapped.property;
|
|
29812
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29813
|
+
}
|
|
29814
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29815
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29816
|
+
return false;
|
|
29817
|
+
};
|
|
29818
|
+
//#endregion
|
|
29819
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29820
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29821
|
+
let cursor = node;
|
|
29822
|
+
let parent = node.parent;
|
|
29823
|
+
while (parent) {
|
|
29824
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29825
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29826
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29827
|
+
cursor = parent;
|
|
29828
|
+
parent = parent.parent ?? null;
|
|
29829
|
+
}
|
|
29830
|
+
return false;
|
|
29831
|
+
};
|
|
29832
|
+
//#endregion
|
|
29833
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29834
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29835
|
+
"toLocaleString",
|
|
29836
|
+
"toLocaleDateString",
|
|
29837
|
+
"toLocaleTimeString"
|
|
29838
|
+
]);
|
|
29839
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29840
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29841
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29842
|
+
"format",
|
|
29843
|
+
"formatToParts",
|
|
29844
|
+
"formatRange"
|
|
29845
|
+
]);
|
|
29846
|
+
const isProvableDateExpression = (expression) => {
|
|
29847
|
+
if (!expression) return false;
|
|
29848
|
+
const unwrapped = stripParenExpression(expression);
|
|
29849
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29850
|
+
};
|
|
29851
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29852
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29853
|
+
if (!expression) return false;
|
|
29854
|
+
const unwrapped = stripParenExpression(expression);
|
|
29855
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29856
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29857
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29858
|
+
return false;
|
|
29859
|
+
};
|
|
29860
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29861
|
+
if (!objectExpression) return false;
|
|
29862
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29863
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29864
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29865
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29866
|
+
if (property.computed) continue;
|
|
29867
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29868
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29869
|
+
}
|
|
29870
|
+
return false;
|
|
29871
|
+
};
|
|
29872
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29873
|
+
if (!argument) return false;
|
|
29874
|
+
const unwrapped = stripParenExpression(argument);
|
|
29875
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29876
|
+
return true;
|
|
29877
|
+
};
|
|
29878
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29879
|
+
const localeArgument = call.arguments?.[0];
|
|
29880
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29881
|
+
const optionsArgument = call.arguments?.[1];
|
|
29882
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29883
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29884
|
+
};
|
|
29885
|
+
const matchLocaleMethodCall = (call) => {
|
|
29886
|
+
const callee = call.callee;
|
|
29887
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29888
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29889
|
+
const methodName = callee.property.name;
|
|
29890
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29891
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29892
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29893
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29894
|
+
return {
|
|
29895
|
+
node: call,
|
|
29896
|
+
display: `${methodName}()`
|
|
29897
|
+
};
|
|
29898
|
+
};
|
|
29899
|
+
const getIntlFormatterName = (expression) => {
|
|
29900
|
+
if (!expression) return null;
|
|
29901
|
+
const unwrapped = stripParenExpression(expression);
|
|
29902
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29903
|
+
const callee = unwrapped.callee;
|
|
29904
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29905
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29906
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29907
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29908
|
+
};
|
|
29909
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29910
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29911
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29912
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29913
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29914
|
+
};
|
|
29915
|
+
const matchIntlFormatCall = (call) => {
|
|
29916
|
+
const callee = call.callee;
|
|
29917
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29918
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29919
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29920
|
+
let construction = stripParenExpression(callee.object);
|
|
29921
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29922
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29923
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29924
|
+
}
|
|
29925
|
+
if (!construction) return null;
|
|
29926
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29927
|
+
if (!formatterName) return null;
|
|
29928
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29929
|
+
return {
|
|
29930
|
+
node: call,
|
|
29931
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29932
|
+
};
|
|
29933
|
+
};
|
|
29934
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29935
|
+
if (!expression) return false;
|
|
29936
|
+
const unwrapped = stripParenExpression(expression);
|
|
29937
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29938
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29939
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29940
|
+
};
|
|
29941
|
+
const matchDateDefaultStringification = (node) => {
|
|
29942
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29943
|
+
const callee = node.callee;
|
|
29944
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29945
|
+
node,
|
|
29946
|
+
display: "Date.prototype.toString()"
|
|
29947
|
+
};
|
|
29948
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29949
|
+
node,
|
|
29950
|
+
display: "String(new Date(…))"
|
|
29951
|
+
};
|
|
29952
|
+
return null;
|
|
29953
|
+
}
|
|
29954
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29955
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29956
|
+
node: expression,
|
|
29957
|
+
display: "`${new Date(…)}`"
|
|
29958
|
+
};
|
|
29959
|
+
}
|
|
29960
|
+
return null;
|
|
29961
|
+
};
|
|
29962
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
29963
|
+
let functionNode = findEnclosingFunction(node);
|
|
29964
|
+
while (functionNode) {
|
|
29965
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29966
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
29967
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
29968
|
+
}
|
|
29969
|
+
return null;
|
|
29970
|
+
};
|
|
29971
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29972
|
+
if (fileHasUseClientDirective) return true;
|
|
29973
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29974
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
29975
|
+
let callsHook = false;
|
|
29976
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29977
|
+
if (callsHook) return false;
|
|
29978
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29979
|
+
callsHook = true;
|
|
29980
|
+
return false;
|
|
29981
|
+
}
|
|
29982
|
+
});
|
|
29983
|
+
return callsHook;
|
|
29984
|
+
};
|
|
29985
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
29986
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29987
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29988
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
29989
|
+
let cursor = node;
|
|
29990
|
+
while (cursor) {
|
|
29991
|
+
ancestors.add(cursor);
|
|
29992
|
+
cursor = cursor.parent ?? null;
|
|
29993
|
+
}
|
|
29994
|
+
for (const statement of body.body ?? []) {
|
|
29995
|
+
if (ancestors.has(statement)) return false;
|
|
29996
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29997
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
29998
|
+
let returnsEarly = false;
|
|
29999
|
+
walkAst(statement.consequent, (child) => {
|
|
30000
|
+
if (isFunctionLike$1(child)) return false;
|
|
30001
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30002
|
+
returnsEarly = true;
|
|
30003
|
+
return false;
|
|
30004
|
+
}
|
|
30005
|
+
});
|
|
30006
|
+
if (returnsEarly) return true;
|
|
30007
|
+
}
|
|
30008
|
+
return false;
|
|
30009
|
+
};
|
|
30010
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30011
|
+
let cursor = node.parent;
|
|
30012
|
+
while (cursor) {
|
|
30013
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30014
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30015
|
+
cursor = cursor.parent ?? null;
|
|
30016
|
+
}
|
|
30017
|
+
return null;
|
|
30018
|
+
};
|
|
30019
|
+
const noLocaleFormatInRender = defineRule({
|
|
30020
|
+
id: "no-locale-format-in-render",
|
|
30021
|
+
title: "Locale/timezone formatting during render",
|
|
30022
|
+
severity: "warn",
|
|
30023
|
+
category: "Correctness",
|
|
30024
|
+
disabledWhen: [
|
|
30025
|
+
"vite",
|
|
30026
|
+
"cra",
|
|
30027
|
+
"expo",
|
|
30028
|
+
"react-native",
|
|
30029
|
+
"unknown"
|
|
30030
|
+
],
|
|
30031
|
+
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.",
|
|
30032
|
+
create: (context) => {
|
|
30033
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30034
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30035
|
+
let fileHasUseClientDirective = false;
|
|
30036
|
+
let fileIsEmailTemplate = false;
|
|
30037
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30038
|
+
const reportIfRenderPhase = (match) => {
|
|
30039
|
+
if (reportedNodes.has(match.node)) return;
|
|
30040
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30041
|
+
if (!componentOrHookNode) return;
|
|
30042
|
+
if (fileIsEmailTemplate) return;
|
|
30043
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30044
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30045
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30046
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30047
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30048
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30049
|
+
reportedNodes.add(match.node);
|
|
30050
|
+
context.report({
|
|
30051
|
+
node: match.node,
|
|
30052
|
+
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.`
|
|
30053
|
+
});
|
|
30054
|
+
};
|
|
30055
|
+
return {
|
|
30056
|
+
Program(node) {
|
|
30057
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30058
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30059
|
+
},
|
|
30060
|
+
CallExpression(node) {
|
|
30061
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30062
|
+
if (match) reportIfRenderPhase(match);
|
|
30063
|
+
},
|
|
30064
|
+
TemplateLiteral(node) {
|
|
30065
|
+
const match = matchDateDefaultStringification(node);
|
|
30066
|
+
if (match) reportIfRenderPhase(match);
|
|
30067
|
+
},
|
|
30068
|
+
JSXExpressionContainer(node) {
|
|
30069
|
+
const expression = stripParenExpression(node.expression);
|
|
30070
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30071
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30072
|
+
const helperName = expression.callee.name;
|
|
30073
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30074
|
+
if (!componentOrHookNode) return;
|
|
30075
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30076
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30077
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30078
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30079
|
+
if (isFunctionLike$1(child)) return false;
|
|
30080
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30081
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30082
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30083
|
+
if (fileIsEmailTemplate) return;
|
|
30084
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30085
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30086
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30087
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30088
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30089
|
+
reportedNodes.add(match.node);
|
|
30090
|
+
context.report({
|
|
30091
|
+
node: match.node,
|
|
30092
|
+
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.`
|
|
30093
|
+
});
|
|
30094
|
+
});
|
|
30095
|
+
}
|
|
30096
|
+
};
|
|
30097
|
+
}
|
|
30098
|
+
});
|
|
30099
|
+
//#endregion
|
|
29302
30100
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29303
30101
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29304
30102
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30112,7 +30910,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30112
30910
|
"reverse",
|
|
30113
30911
|
"sort"
|
|
30114
30912
|
]);
|
|
30115
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30116
30913
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30117
30914
|
"assign",
|
|
30118
30915
|
"defineProperties",
|
|
@@ -30186,7 +30983,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30186
30983
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30187
30984
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30188
30985
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30189
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30190
30986
|
}
|
|
30191
30987
|
}
|
|
30192
30988
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30225,6 +31021,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30225
31021
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30226
31022
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30227
31023
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
31024
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30228
31025
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30229
31026
|
});
|
|
30230
31027
|
return mutations;
|
|
@@ -30457,7 +31254,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30457
31254
|
id: "no-nested-component-definition",
|
|
30458
31255
|
title: "Component defined inside another component",
|
|
30459
31256
|
tags: ["test-noise", "react-jsx-only"],
|
|
30460
|
-
severity: "
|
|
31257
|
+
severity: "warn",
|
|
30461
31258
|
category: "Correctness",
|
|
30462
31259
|
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.",
|
|
30463
31260
|
create: (context) => {
|
|
@@ -31412,12 +32209,12 @@ const noPassDataToParent = defineRule({
|
|
|
31412
32209
|
if (calleeNode === identifier) {
|
|
31413
32210
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31414
32211
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31415
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32212
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31416
32213
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31417
32214
|
if (isCustomHookParameter(ref)) continue;
|
|
31418
32215
|
} else continue;
|
|
31419
32216
|
const methodName = getCallMethodName(calleeNode);
|
|
31420
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32217
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31421
32218
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31422
32219
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31423
32220
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31608,7 +32405,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31608
32405
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31609
32406
|
const calleeNode = callExpr.callee;
|
|
31610
32407
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31611
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32408
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31612
32409
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31613
32410
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31614
32411
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -31936,40 +32733,6 @@ const noPreventDefault = defineRule({
|
|
|
31936
32733
|
}
|
|
31937
32734
|
});
|
|
31938
32735
|
//#endregion
|
|
31939
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31940
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
31941
|
-
let node = callExpression;
|
|
31942
|
-
let parent = node.parent;
|
|
31943
|
-
while (parent) {
|
|
31944
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31945
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31946
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31947
|
-
node = parent;
|
|
31948
|
-
parent = node.parent;
|
|
31949
|
-
continue;
|
|
31950
|
-
}
|
|
31951
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31952
|
-
node = parent;
|
|
31953
|
-
parent = node.parent;
|
|
31954
|
-
continue;
|
|
31955
|
-
}
|
|
31956
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31957
|
-
node = parent;
|
|
31958
|
-
parent = node.parent;
|
|
31959
|
-
continue;
|
|
31960
|
-
}
|
|
31961
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31962
|
-
const expressions = parent.expressions ?? [];
|
|
31963
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
31964
|
-
node = parent;
|
|
31965
|
-
parent = node.parent;
|
|
31966
|
-
continue;
|
|
31967
|
-
}
|
|
31968
|
-
return false;
|
|
31969
|
-
}
|
|
31970
|
-
return false;
|
|
31971
|
-
};
|
|
31972
|
-
//#endregion
|
|
31973
32736
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
31974
32737
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
31975
32738
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32176,7 +32939,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32176
32939
|
return `${callee.name}()`;
|
|
32177
32940
|
}
|
|
32178
32941
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32179
|
-
const receiver = callee.object;
|
|
32942
|
+
const receiver = stripParenExpression(callee.object);
|
|
32180
32943
|
const property = callee.property;
|
|
32181
32944
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32182
32945
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32297,8 +33060,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32297
33060
|
if (typeof sourceValue !== "string") return;
|
|
32298
33061
|
if (handleExtraSource?.(node, context)) return;
|
|
32299
33062
|
if (sourceValue !== source) return;
|
|
33063
|
+
if (isTypeOnlyImport(node)) return;
|
|
32300
33064
|
for (const specifier of node.specifiers ?? []) {
|
|
32301
33065
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33066
|
+
if (specifier.importKind === "type") continue;
|
|
32302
33067
|
const importedName = getImportedName$1(specifier);
|
|
32303
33068
|
if (!importedName) continue;
|
|
32304
33069
|
const message = messages.get(importedName);
|
|
@@ -32317,8 +33082,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32317
33082
|
MemberExpression(node) {
|
|
32318
33083
|
if (namespaceBindings.size === 0) return;
|
|
32319
33084
|
if (node.computed) return;
|
|
32320
|
-
|
|
32321
|
-
if (!
|
|
33085
|
+
const receiver = stripParenExpression(node.object);
|
|
33086
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33087
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32322
33088
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32323
33089
|
const message = messages.get(node.property.name);
|
|
32324
33090
|
if (message) context.report({
|
|
@@ -32383,7 +33149,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32383
33149
|
});
|
|
32384
33150
|
//#endregion
|
|
32385
33151
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32386
|
-
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."]
|
|
33152
|
+
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."]]);
|
|
32387
33153
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32388
33154
|
if (!rawFilename) return false;
|
|
32389
33155
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32421,7 +33187,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32421
33187
|
requires: ["react:19"],
|
|
32422
33188
|
tags: ["test-noise", "migration-hint"],
|
|
32423
33189
|
severity: "warn",
|
|
32424
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33190
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32425
33191
|
create: (context) => {
|
|
32426
33192
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32427
33193
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32745,34 +33511,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32745
33511
|
});
|
|
32746
33512
|
//#endregion
|
|
32747
33513
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32748
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32749
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32750
|
-
visitedSymbols.add(symbol);
|
|
32751
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
32752
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32753
|
-
const source = symbol.initializer;
|
|
32754
|
-
if (!source) return false;
|
|
32755
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32756
|
-
};
|
|
32757
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32758
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32759
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32760
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
32761
|
-
};
|
|
32762
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32763
|
-
let current = node;
|
|
32764
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
32765
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32766
|
-
current = current.object;
|
|
32767
|
-
}
|
|
32768
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32769
|
-
return false;
|
|
32770
|
-
};
|
|
32771
33514
|
const isInsideComponentContext = (node) => {
|
|
32772
33515
|
let cursor = node.parent;
|
|
32773
33516
|
while (cursor) {
|
|
32774
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32775
33517
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33518
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32776
33519
|
cursor = cursor.parent ?? null;
|
|
32777
33520
|
}
|
|
32778
33521
|
return false;
|
|
@@ -32782,24 +33525,28 @@ const functionBodyOf = (node) => {
|
|
|
32782
33525
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32783
33526
|
return null;
|
|
32784
33527
|
};
|
|
33528
|
+
const isHookCallee = (callee) => {
|
|
33529
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33530
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33531
|
+
return false;
|
|
33532
|
+
};
|
|
32785
33533
|
const containsHookCall = (body) => {
|
|
32786
33534
|
let found = false;
|
|
32787
33535
|
walkAst(body, (child) => {
|
|
32788
|
-
if (found) return;
|
|
33536
|
+
if (found) return false;
|
|
33537
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32789
33538
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32790
|
-
|
|
32791
|
-
if (name && isReactHookName(name)) found = true;
|
|
33539
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32792
33540
|
});
|
|
32793
33541
|
return found;
|
|
32794
33542
|
};
|
|
32795
|
-
const
|
|
33543
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32796
33544
|
if (!symbol) return false;
|
|
32797
33545
|
const declaration = symbol.declarationNode;
|
|
32798
33546
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32799
33547
|
const body = functionBodyOf(declaration);
|
|
32800
33548
|
if (!body) return false;
|
|
32801
|
-
|
|
32802
|
-
return !containsHookCall(body);
|
|
33549
|
+
return containsHookCall(body);
|
|
32803
33550
|
};
|
|
32804
33551
|
const noRenderInRender = defineRule({
|
|
32805
33552
|
id: "no-render-in-render",
|
|
@@ -32808,20 +33555,13 @@ const noRenderInRender = defineRule({
|
|
|
32808
33555
|
tags: ["test-noise"],
|
|
32809
33556
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32810
33557
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32811
|
-
const expression = node.expression;
|
|
33558
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32812
33559
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33560
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33561
|
+
const calleeName = expression.callee.name;
|
|
33562
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32817
33563
|
if (!isInsideComponentContext(node)) return;
|
|
32818
|
-
if (
|
|
32819
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32820
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32821
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32822
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32823
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32824
|
-
}
|
|
33564
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32825
33565
|
context.report({
|
|
32826
33566
|
node: expression,
|
|
32827
33567
|
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.`
|
|
@@ -32882,13 +33622,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32882
33622
|
//#endregion
|
|
32883
33623
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32884
33624
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
32885
|
-
const isReactDomRenderCall = (node) =>
|
|
32886
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32887
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32888
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32889
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32890
|
-
return node.callee.property.name === "render";
|
|
32891
|
-
};
|
|
33625
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32892
33626
|
const isUsedAsReturnValue = (parent) => {
|
|
32893
33627
|
if (!parent) return false;
|
|
32894
33628
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33724,7 +34458,7 @@ const noSetState = defineRule({
|
|
|
33724
34458
|
category: "Architecture",
|
|
33725
34459
|
create: (context) => ({ CallExpression(node) {
|
|
33726
34460
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33727
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34461
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33728
34462
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33729
34463
|
if (!getParentComponent(node)) return;
|
|
33730
34464
|
context.report({
|
|
@@ -36170,6 +36904,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36170
36904
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36171
36905
|
return true;
|
|
36172
36906
|
};
|
|
36907
|
+
const isTrivialContainerLiteral = (node) => {
|
|
36908
|
+
if (!node) return false;
|
|
36909
|
+
const innerExpression = stripParenExpression(node);
|
|
36910
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36911
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36912
|
+
return false;
|
|
36913
|
+
};
|
|
36914
|
+
const isNonEscapingRead = (identifier) => {
|
|
36915
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36916
|
+
const memberNode = readRoot.parent;
|
|
36917
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36918
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36919
|
+
const memberUseParent = memberUse.parent;
|
|
36920
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36921
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36922
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36923
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36924
|
+
};
|
|
36925
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36926
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36927
|
+
const parentNode = memoUsageRoot.parent;
|
|
36928
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36929
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36930
|
+
const bindingTarget = parentNode.id;
|
|
36931
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36932
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36933
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
36934
|
+
if (!symbol) return false;
|
|
36935
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36936
|
+
};
|
|
36173
36937
|
const noUsememoSimpleExpression = defineRule({
|
|
36174
36938
|
id: "no-usememo-simple-expression",
|
|
36175
36939
|
title: "useMemo on a cheap value",
|
|
@@ -36191,9 +36955,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36191
36955
|
let returnExpression = null;
|
|
36192
36956
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36193
36957
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36194
|
-
if (returnExpression
|
|
36958
|
+
if (!returnExpression) return;
|
|
36959
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36960
|
+
context.report({
|
|
36961
|
+
node,
|
|
36962
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36963
|
+
});
|
|
36964
|
+
return;
|
|
36965
|
+
}
|
|
36966
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36195
36967
|
node,
|
|
36196
|
-
message: "This
|
|
36968
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36197
36969
|
});
|
|
36198
36970
|
} })
|
|
36199
36971
|
});
|
|
@@ -36299,7 +37071,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36299
37071
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36300
37072
|
return { CallExpression(node) {
|
|
36301
37073
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36302
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37074
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36303
37075
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36304
37076
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36305
37077
|
context.report({
|
|
@@ -36575,8 +37347,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
37347
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
37348
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
37349
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36578
|
-
const
|
|
36579
|
-
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
37350
|
+
const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
|
|
36580
37351
|
const DEFAULT_REACT_HOCS = [
|
|
36581
37352
|
"memo",
|
|
36582
37353
|
"forwardRef",
|
|
@@ -36617,6 +37388,10 @@ const isRouteFactoryCall = (expression) => {
|
|
|
36617
37388
|
}
|
|
36618
37389
|
return false;
|
|
36619
37390
|
};
|
|
37391
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.every((argument) => {
|
|
37392
|
+
const expression = skipTsExpression(argument);
|
|
37393
|
+
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37394
|
+
});
|
|
36620
37395
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
36621
37396
|
const isHocCallee = (callee, state) => {
|
|
36622
37397
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -36647,6 +37422,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
37422
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
37423
|
return false;
|
|
36649
37424
|
};
|
|
37425
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
37426
|
+
for (const property of objectExpression.properties ?? []) {
|
|
37427
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37428
|
+
const value = skipTsExpression(property.value);
|
|
37429
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
37430
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
37431
|
+
continue;
|
|
37432
|
+
}
|
|
37433
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
37434
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
37435
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
37436
|
+
}
|
|
37437
|
+
return false;
|
|
37438
|
+
};
|
|
36650
37439
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
37440
|
if (initializer) {
|
|
36652
37441
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +37466,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
37466
|
reportNode
|
|
36678
37467
|
};
|
|
36679
37468
|
}
|
|
37469
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
37470
|
+
kind: "namespace-object",
|
|
37471
|
+
reportNode
|
|
37472
|
+
};
|
|
36680
37473
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
37474
|
kind: "non-component",
|
|
36682
37475
|
reportNode
|
|
@@ -36693,7 +37486,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
37486
|
walkAst(programRoot, (child) => {
|
|
36694
37487
|
const childType = child.type;
|
|
36695
37488
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
37489
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
37490
|
});
|
|
36698
37491
|
return {
|
|
36699
37492
|
exportNodes,
|
|
@@ -36738,18 +37531,35 @@ const onlyExportComponents = defineRule({
|
|
|
36738
37531
|
category: "Architecture",
|
|
36739
37532
|
create: (context) => {
|
|
36740
37533
|
const settings = resolveSettings$6(context.settings);
|
|
36741
|
-
const state = {
|
|
36742
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36743
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
36744
|
-
allowConstantExport: settings.allowConstantExport
|
|
36745
|
-
};
|
|
36746
37534
|
return { Program(node) {
|
|
36747
37535
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
37536
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
37537
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
37538
|
+
const state = {
|
|
37539
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37540
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
37541
|
+
allowConstantExport: settings.allowConstantExport,
|
|
37542
|
+
localComponentNames,
|
|
37543
|
+
scopes: context.scopes
|
|
37544
|
+
};
|
|
37545
|
+
for (const child of componentCandidates) {
|
|
37546
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37547
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
37548
|
+
}
|
|
37549
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37550
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37551
|
+
}
|
|
37552
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37553
|
+
const initializer = child.init;
|
|
37554
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37555
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37556
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37557
|
+
}
|
|
37558
|
+
}
|
|
37559
|
+
}
|
|
36749
37560
|
const exports = [];
|
|
36750
37561
|
let hasReactExport = false;
|
|
36751
37562
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
37563
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
37564
|
for (const child of exportNodes) {
|
|
36755
37565
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36814,13 +37624,24 @@ const onlyExportComponents = defineRule({
|
|
|
36814
37624
|
return false;
|
|
36815
37625
|
})();
|
|
36816
37626
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37627
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37628
|
+
kind: "non-component",
|
|
37629
|
+
reportNode: stripped
|
|
37630
|
+
});
|
|
36817
37631
|
else context.report({
|
|
36818
37632
|
node: stripped,
|
|
36819
37633
|
message: ANONYMOUS_MESSAGE
|
|
36820
37634
|
});
|
|
36821
37635
|
continue;
|
|
36822
37636
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
37637
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
37638
|
+
context.report({
|
|
37639
|
+
node: stripped,
|
|
37640
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
37641
|
+
});
|
|
37642
|
+
continue;
|
|
37643
|
+
}
|
|
37644
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
37645
|
context.report({
|
|
36825
37646
|
node: stripped,
|
|
36826
37647
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +37694,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
37694
|
let entry;
|
|
36874
37695
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
37696
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
37697
|
+
else {
|
|
37698
|
+
entry = {
|
|
37699
|
+
kind: "non-component",
|
|
37700
|
+
reportNode
|
|
37701
|
+
};
|
|
37702
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37703
|
+
}
|
|
36880
37704
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
37705
|
exports.push(entry);
|
|
36882
37706
|
}
|
|
36883
37707
|
}
|
|
36884
37708
|
}
|
|
36885
37709
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
36886
|
-
const
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
walker = walker.parent ?? null;
|
|
36891
|
-
}
|
|
36892
|
-
return false;
|
|
36893
|
-
};
|
|
36894
|
-
for (const child of componentCandidates) {
|
|
36895
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36896
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36897
|
-
}
|
|
36898
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36899
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36900
|
-
}
|
|
36901
|
-
}
|
|
37710
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
37711
|
+
node: entry.reportNode,
|
|
37712
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
37713
|
+
});
|
|
36902
37714
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
37715
|
if (entry.kind === "non-component") context.report({
|
|
36904
37716
|
node: entry.reportNode,
|
|
@@ -36909,14 +37721,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37721
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37722
|
});
|
|
36911
37723
|
}
|
|
36912
|
-
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36913
|
-
node: local,
|
|
36914
|
-
message: LOCAL_COMPONENT_MESSAGE
|
|
36915
|
-
});
|
|
36916
|
-
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36917
|
-
node: local,
|
|
36918
|
-
message: NO_EXPORT_MESSAGE
|
|
36919
|
-
});
|
|
36920
37724
|
} };
|
|
36921
37725
|
}
|
|
36922
37726
|
});
|
|
@@ -37714,8 +38518,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
38518
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
38519
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
38520
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
38521
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
38522
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
38523
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
38524
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
38525
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -38419,11 +39223,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38419
39223
|
}
|
|
38420
39224
|
return null;
|
|
38421
39225
|
};
|
|
39226
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39227
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39228
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39229
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39230
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39231
|
+
return false;
|
|
39232
|
+
};
|
|
38422
39233
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38423
39234
|
const handlerStatements = getCallbackStatements(handler);
|
|
38424
39235
|
if (handlerStatements.length !== 1) return null;
|
|
38425
39236
|
const onlyStatement = handlerStatements[0];
|
|
38426
|
-
|
|
39237
|
+
let expression = onlyStatement;
|
|
39238
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39239
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39240
|
+
if (!expression) return null;
|
|
39241
|
+
expression = stripParenExpression(expression);
|
|
38427
39242
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38428
39243
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38429
39244
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38433,6 +39248,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38433
39248
|
setterArgument: expression.arguments[0]
|
|
38434
39249
|
};
|
|
38435
39250
|
};
|
|
39251
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39252
|
+
if (!init) return false;
|
|
39253
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39254
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39255
|
+
};
|
|
39256
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39257
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39258
|
+
const firstParam = functionNode.params?.[0];
|
|
39259
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39260
|
+
const listenerParamName = firstParam.name;
|
|
39261
|
+
let registersListener = false;
|
|
39262
|
+
walkAst(functionNode.body, (child) => {
|
|
39263
|
+
if (registersListener) return false;
|
|
39264
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39265
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39266
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39267
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39268
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39269
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39270
|
+
const registeredArgument = child.arguments?.[0];
|
|
39271
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39272
|
+
});
|
|
39273
|
+
return registersListener;
|
|
39274
|
+
};
|
|
39275
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39276
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39277
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39278
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39279
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39280
|
+
mutableBindingNames,
|
|
39281
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39282
|
+
};
|
|
39283
|
+
for (const statement of programRoot.body ?? []) {
|
|
39284
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39285
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39286
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39287
|
+
continue;
|
|
39288
|
+
}
|
|
39289
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39290
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39291
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39292
|
+
const init = declarator.init ?? null;
|
|
39293
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39294
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39295
|
+
continue;
|
|
39296
|
+
}
|
|
39297
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39298
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39299
|
+
continue;
|
|
39300
|
+
}
|
|
39301
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39302
|
+
}
|
|
39303
|
+
}
|
|
39304
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39305
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39306
|
+
return {
|
|
39307
|
+
mutableBindingNames,
|
|
39308
|
+
subscribeFunctionNames
|
|
39309
|
+
};
|
|
39310
|
+
};
|
|
39311
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39312
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39313
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39314
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39315
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39316
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39317
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39318
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39319
|
+
return initialArgument.name;
|
|
39320
|
+
};
|
|
39321
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39322
|
+
if (!argument) return false;
|
|
39323
|
+
const unwrapped = stripParenExpression(argument);
|
|
39324
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39325
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39326
|
+
let callsSetter = false;
|
|
39327
|
+
walkAst(unwrapped.body, (child) => {
|
|
39328
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39329
|
+
callsSetter = true;
|
|
39330
|
+
return false;
|
|
39331
|
+
}
|
|
39332
|
+
});
|
|
39333
|
+
return callsSetter;
|
|
39334
|
+
};
|
|
39335
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39336
|
+
let matchedCall = null;
|
|
39337
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39338
|
+
if (matchedCall) return false;
|
|
39339
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39340
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39341
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39342
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39343
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39344
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39345
|
+
matchedCall = child;
|
|
39346
|
+
return false;
|
|
39347
|
+
}
|
|
39348
|
+
});
|
|
39349
|
+
return matchedCall;
|
|
39350
|
+
};
|
|
38436
39351
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38437
39352
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38438
39353
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38449,6 +39364,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38449
39364
|
severity: "warn",
|
|
38450
39365
|
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.",
|
|
38451
39366
|
create: (context) => {
|
|
39367
|
+
let cachedStoreIndex = null;
|
|
39368
|
+
const storeIndexFor = (node) => {
|
|
39369
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39370
|
+
const programRoot = findProgramRoot(node);
|
|
39371
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39372
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39373
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39374
|
+
};
|
|
39375
|
+
return cachedStoreIndex;
|
|
39376
|
+
};
|
|
38452
39377
|
const checkComponent = (componentBody) => {
|
|
38453
39378
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38454
39379
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38486,6 +39411,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38486
39411
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38487
39412
|
if (!useStateInitializer) continue;
|
|
38488
39413
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39414
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38489
39415
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38490
39416
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38491
39417
|
context.report({
|
|
@@ -38493,14 +39419,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38493
39419
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38494
39420
|
});
|
|
38495
39421
|
}
|
|
39422
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39423
|
+
};
|
|
39424
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39425
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39426
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39427
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39428
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39429
|
+
binding,
|
|
39430
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39431
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39432
|
+
if (snapshotBindings.length === 0) return;
|
|
39433
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39434
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39435
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39436
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39437
|
+
const depsNode = effectCall.arguments[1];
|
|
39438
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39439
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39440
|
+
const callback = getEffectCallback(effectCall);
|
|
39441
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39442
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39443
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39444
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39445
|
+
reportedDeclarators.add(binding.declarator);
|
|
39446
|
+
context.report({
|
|
39447
|
+
node: binding.declarator,
|
|
39448
|
+
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.`
|
|
39449
|
+
});
|
|
39450
|
+
}
|
|
39451
|
+
}
|
|
38496
39452
|
};
|
|
38497
39453
|
return {
|
|
38498
39454
|
FunctionDeclaration(node) {
|
|
38499
|
-
|
|
39455
|
+
const functionName = node.id?.name;
|
|
39456
|
+
if (!functionName) return;
|
|
39457
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38500
39458
|
checkComponent(node.body);
|
|
38501
39459
|
},
|
|
38502
39460
|
VariableDeclarator(node) {
|
|
38503
|
-
|
|
39461
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39462
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38504
39463
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38505
39464
|
checkComponent(node.init.body);
|
|
38506
39465
|
}
|
|
@@ -39110,7 +40069,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39110
40069
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39111
40070
|
const body = queryFnValue.body;
|
|
39112
40071
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39113
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40072
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39114
40073
|
node: queryFnProperty,
|
|
39115
40074
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39116
40075
|
});
|
|
@@ -39617,17 +40576,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39617
40576
|
}
|
|
39618
40577
|
});
|
|
39619
40578
|
//#endregion
|
|
39620
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39621
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39622
|
-
let ancestor = bindingIdentifier.parent;
|
|
39623
|
-
while (ancestor) {
|
|
39624
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39625
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39626
|
-
ancestor = ancestor.parent ?? null;
|
|
39627
|
-
}
|
|
39628
|
-
return null;
|
|
39629
|
-
};
|
|
39630
|
-
//#endregion
|
|
39631
40579
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39632
40580
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39633
40581
|
{
|
|
@@ -39636,19 +40584,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39636
40584
|
},
|
|
39637
40585
|
{
|
|
39638
40586
|
display: "Date.now()",
|
|
39639
|
-
matches: (node) =>
|
|
40587
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39640
40588
|
},
|
|
39641
40589
|
{
|
|
39642
40590
|
display: "Math.random()",
|
|
39643
|
-
matches: (node) =>
|
|
40591
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39644
40592
|
},
|
|
39645
40593
|
{
|
|
39646
40594
|
display: "performance.now()",
|
|
39647
|
-
matches: (node) =>
|
|
40595
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39648
40596
|
},
|
|
39649
40597
|
{
|
|
39650
40598
|
display: "crypto.randomUUID()",
|
|
39651
|
-
matches: (node) =>
|
|
40599
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39652
40600
|
}
|
|
39653
40601
|
];
|
|
39654
40602
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39660,54 +40608,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39660
40608
|
}
|
|
39661
40609
|
return null;
|
|
39662
40610
|
};
|
|
39663
|
-
const executesDuringRender = (functionNode) => {
|
|
39664
|
-
const parent = functionNode.parent;
|
|
39665
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39666
|
-
if (parent.callee === functionNode) return true;
|
|
39667
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39668
|
-
};
|
|
39669
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39670
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39671
|
-
const unwrapped = stripParenExpression(expression);
|
|
39672
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39673
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39674
|
-
const property = unwrapped.property;
|
|
39675
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39676
|
-
}
|
|
39677
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39678
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39679
|
-
return false;
|
|
39680
|
-
};
|
|
39681
|
-
const isFalsyLiteral = (node) => {
|
|
39682
|
-
if (!node) return true;
|
|
39683
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39684
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39685
|
-
};
|
|
39686
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39687
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39688
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39689
|
-
if (!binding) return false;
|
|
39690
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39691
|
-
if (!declarator?.init) return false;
|
|
39692
|
-
const init = stripParenExpression(declarator.init);
|
|
39693
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39694
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39695
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39696
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39697
|
-
};
|
|
39698
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39699
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39700
|
-
let cursor = node;
|
|
39701
|
-
let parent = node.parent;
|
|
39702
|
-
while (parent) {
|
|
39703
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39704
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39705
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39706
|
-
cursor = parent;
|
|
39707
|
-
parent = parent.parent ?? null;
|
|
39708
|
-
}
|
|
39709
|
-
return false;
|
|
39710
|
-
};
|
|
39711
40611
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39712
40612
|
const member = dateNode.parent;
|
|
39713
40613
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39731,23 +40631,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39731
40631
|
}
|
|
39732
40632
|
return false;
|
|
39733
40633
|
};
|
|
39734
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
39735
|
-
let cursor = node;
|
|
39736
|
-
let parent = node.parent;
|
|
39737
|
-
while (parent) {
|
|
39738
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39739
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39740
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39741
|
-
cursor = parent;
|
|
39742
|
-
parent = parent.parent ?? null;
|
|
39743
|
-
}
|
|
39744
|
-
return false;
|
|
39745
|
-
};
|
|
39746
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39747
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39748
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39749
|
-
return false;
|
|
39750
|
-
};
|
|
39751
40634
|
const renderingHydrationMismatchTime = defineRule({
|
|
39752
40635
|
id: "rendering-hydration-mismatch-time",
|
|
39753
40636
|
title: "Time or random value in JSX",
|
|
@@ -39795,6 +40678,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39795
40678
|
}
|
|
39796
40679
|
});
|
|
39797
40680
|
//#endregion
|
|
40681
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40682
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40683
|
+
"toLocaleString",
|
|
40684
|
+
"toLocaleDateString",
|
|
40685
|
+
"toLocaleTimeString",
|
|
40686
|
+
"getTimezoneOffset"
|
|
40687
|
+
]);
|
|
40688
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40689
|
+
let readsLocaleEnvironment = false;
|
|
40690
|
+
walkAst(expression, (child) => {
|
|
40691
|
+
if (readsLocaleEnvironment) return false;
|
|
40692
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40693
|
+
if (child.object.name === "Intl") {
|
|
40694
|
+
readsLocaleEnvironment = true;
|
|
40695
|
+
return false;
|
|
40696
|
+
}
|
|
40697
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40698
|
+
readsLocaleEnvironment = true;
|
|
40699
|
+
return false;
|
|
40700
|
+
}
|
|
40701
|
+
}
|
|
40702
|
+
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)) {
|
|
40703
|
+
readsLocaleEnvironment = true;
|
|
40704
|
+
return false;
|
|
40705
|
+
}
|
|
40706
|
+
});
|
|
40707
|
+
return readsLocaleEnvironment;
|
|
40708
|
+
};
|
|
40709
|
+
//#endregion
|
|
39798
40710
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39799
40711
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39800
40712
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39860,14 +40772,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39860
40772
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39861
40773
|
const callback = getEffectCallback(node);
|
|
39862
40774
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39863
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39864
|
-
if (
|
|
40775
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
40776
|
+
if (bodyStatements.length !== 1) return;
|
|
39865
40777
|
const soleStatement = bodyStatements[0];
|
|
39866
40778
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39867
40779
|
const expression = soleStatement.expression;
|
|
39868
40780
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39869
40781
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39870
40782
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40783
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39871
40784
|
context.report({
|
|
39872
40785
|
node,
|
|
39873
40786
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40686,6 +41599,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40686
41599
|
} })
|
|
40687
41600
|
});
|
|
40688
41601
|
//#endregion
|
|
41602
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41603
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41604
|
+
//#endregion
|
|
40689
41605
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40690
41606
|
const rerenderLazyRefInit = defineRule({
|
|
40691
41607
|
id: "rerender-lazy-ref-init",
|
|
@@ -40696,7 +41612,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40696
41612
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40697
41613
|
create: (context) => ({ CallExpression(node) {
|
|
40698
41614
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40699
|
-
const initializer = node.arguments[0];
|
|
41615
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40700
41616
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40701
41617
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40702
41618
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40704,6 +41620,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
41620
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
41621
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
41622
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41623
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40707
41624
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
41625
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
41626
|
context.report({
|
|
@@ -40736,26 +41653,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
41653
|
"getUTCMilliseconds",
|
|
40737
41654
|
"valueOf"
|
|
40738
41655
|
]);
|
|
40739
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40740
|
-
"Date",
|
|
40741
|
-
"Map",
|
|
40742
|
-
"Set",
|
|
40743
|
-
"WeakMap",
|
|
40744
|
-
"WeakSet",
|
|
40745
|
-
"RegExp",
|
|
40746
|
-
"Error",
|
|
40747
|
-
"URL",
|
|
40748
|
-
"URLSearchParams",
|
|
40749
|
-
"AbortController"
|
|
40750
|
-
]);
|
|
40751
41656
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
41657
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
41658
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40754
|
-
|
|
40755
|
-
if (isNodeOfType(
|
|
40756
|
-
if (isNodeOfType(
|
|
40757
|
-
if (isNodeOfType(
|
|
40758
|
-
if (isNodeOfType(
|
|
41659
|
+
const innerExpression = stripParenExpression(expression);
|
|
41660
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41661
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41662
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41663
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40759
41664
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40760
41665
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40761
41666
|
if (spreadCall) return spreadCall;
|
|
@@ -40778,7 +41683,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40778
41683
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40779
41684
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40780
41685
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40781
|
-
if (
|
|
41686
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40782
41687
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40783
41688
|
if (isReactHookName(calleeName)) return;
|
|
40784
41689
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41445,7 +42350,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41445
42350
|
const body = reactionFn.body;
|
|
41446
42351
|
let singleAssignment = null;
|
|
41447
42352
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41448
|
-
const statements = body.body ?? [];
|
|
42353
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41449
42354
|
if (statements.length !== 1) return;
|
|
41450
42355
|
const onlyStatement = statements[0];
|
|
41451
42356
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41491,7 +42396,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
42396
|
});
|
|
41492
42397
|
//#endregion
|
|
41493
42398
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
42399
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
42400
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
42401
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
42402
|
"tap",
|
|
@@ -41519,7 +42424,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41519
42424
|
"catch",
|
|
41520
42425
|
"finally"
|
|
41521
42426
|
]);
|
|
41522
|
-
const findChainRoot = (
|
|
42427
|
+
const findChainRoot = (wrappedNode) => {
|
|
42428
|
+
const node = stripParenExpression(wrappedNode);
|
|
41523
42429
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41524
42430
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41525
42431
|
calleeName: node.callee.name,
|
|
@@ -41551,7 +42457,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
42457
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
41552
42458
|
create: (context) => {
|
|
41553
42459
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
42460
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
42461
|
return { ExpressionStatement(node) {
|
|
41556
42462
|
const expression = node.expression;
|
|
41557
42463
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42107,20 +43013,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42107
43013
|
};
|
|
42108
43014
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42109
43015
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42110
|
-
|
|
42111
|
-
|
|
43016
|
+
const receiver = stripParenExpression(callee.object);
|
|
43017
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
43018
|
+
const localName = receiver.name;
|
|
42112
43019
|
const binding = findVariableInitializer(node, localName);
|
|
42113
43020
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42114
43021
|
return localName === "Dimensions";
|
|
42115
43022
|
}
|
|
42116
|
-
if (isNodeOfType(
|
|
42117
|
-
if (getInitializerModuleSource(node,
|
|
42118
|
-
const rootName = getRootIdentifierName(
|
|
43023
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
43024
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
43025
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42119
43026
|
if (rootName === null) return false;
|
|
42120
43027
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42121
43028
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42122
43029
|
}
|
|
42123
|
-
if (getRequireCallSource(
|
|
43030
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42124
43031
|
return false;
|
|
42125
43032
|
};
|
|
42126
43033
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42526,7 +43433,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
43433
|
};
|
|
42527
43434
|
//#endregion
|
|
42528
43435
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
43436
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
43437
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
43438
|
const legacyShadowPropertyNames = [];
|
|
42532
43439
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +43456,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
43456
|
severity: "warn",
|
|
42550
43457
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
43458
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
43459
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
43460
|
return {
|
|
42554
43461
|
JSXAttribute(node) {
|
|
42555
43462
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -42564,7 +43471,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42564
43471
|
},
|
|
42565
43472
|
CallExpression(node) {
|
|
42566
43473
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42567
|
-
|
|
43474
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43475
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42568
43476
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42569
43477
|
const stylesArgument = node.arguments?.[0];
|
|
42570
43478
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43414,7 +44322,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43414
44322
|
const callee = node.callee;
|
|
43415
44323
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43416
44324
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43417
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44325
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43418
44326
|
context.report({
|
|
43419
44327
|
node,
|
|
43420
44328
|
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."
|
|
@@ -43470,7 +44378,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
44378
|
};
|
|
43471
44379
|
//#endregion
|
|
43472
44380
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
44381
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
44382
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
44383
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
44384
|
const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
|
|
@@ -43504,7 +44412,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
44412
|
severity: "warn",
|
|
43505
44413
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
44414
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
44415
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
44416
|
const flaggedImports = [];
|
|
43509
44417
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
44418
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43642,14 +44550,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43642
44550
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43643
44551
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43644
44552
|
const methodName = callee.property.name;
|
|
43645
|
-
|
|
44553
|
+
const receiver = stripParenExpression(callee.object);
|
|
44554
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43646
44555
|
factoryName: methodName,
|
|
43647
44556
|
chainMethodNames,
|
|
43648
44557
|
numberOfTapsArgument
|
|
43649
44558
|
};
|
|
43650
44559
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43651
44560
|
chainMethodNames.push(methodName);
|
|
43652
|
-
cursor =
|
|
44561
|
+
cursor = receiver;
|
|
43653
44562
|
}
|
|
43654
44563
|
return null;
|
|
43655
44564
|
};
|
|
@@ -43991,7 +44900,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
44900
|
});
|
|
43992
44901
|
//#endregion
|
|
43993
44902
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
44903
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
44904
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
44905
|
"shadowColor",
|
|
43997
44906
|
"shadowOffset",
|
|
@@ -44077,7 +44986,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
44986
|
severity: "warn",
|
|
44078
44987
|
recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
|
|
44079
44988
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44989
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
44990
|
return {
|
|
44082
44991
|
JSXAttribute(node) {
|
|
44083
44992
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +45083,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
45083
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
45084
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
45085
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
45086
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
45087
|
+
if (roleCandidates === null) return;
|
|
45088
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
45089
|
for (const role of roles) {
|
|
44181
45090
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
45091
|
if (!required) continue;
|
|
@@ -47293,7 +48202,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
48202
|
};
|
|
47294
48203
|
//#endregion
|
|
47295
48204
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
48205
|
+
const buildMessageDefault = (roles, propName) => {
|
|
48206
|
+
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.`;
|
|
48207
|
+
};
|
|
47297
48208
|
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.`;
|
|
47298
48209
|
const roleSupportsAriaProps = defineRule({
|
|
47299
48210
|
id: "role-supports-aria-props",
|
|
@@ -47313,6 +48224,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47313
48224
|
const propName = propRawName.toLowerCase();
|
|
47314
48225
|
if (!propName.startsWith("aria-")) continue;
|
|
47315
48226
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48227
|
+
const attributeValue = attributeNode.value;
|
|
48228
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47316
48229
|
(ariaAttributes ??= []).push({
|
|
47317
48230
|
attribute,
|
|
47318
48231
|
propName
|
|
@@ -47321,17 +48234,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
48234
|
if (!ariaAttributes) return;
|
|
47322
48235
|
const elementType = getElementType(node, context.settings);
|
|
47323
48236
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
48237
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
48238
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
48239
|
+
const supportedSets = [];
|
|
48240
|
+
for (const role of roleCandidates) {
|
|
48241
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48242
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
48243
|
+
if (!supported) return;
|
|
48244
|
+
supportedSets.push(supported);
|
|
48245
|
+
}
|
|
47327
48246
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
48247
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
48248
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
48249
|
context.report({
|
|
47333
48250
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
48251
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
48252
|
});
|
|
47336
48253
|
}
|
|
47337
48254
|
} })
|
|
@@ -47683,11 +48600,10 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47683
48600
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
48601
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
48602
|
};
|
|
47686
|
-
const
|
|
47687
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode) => {
|
|
48603
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47688
48604
|
const initializer = symbol.initializer;
|
|
47689
48605
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
48606
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47691
48607
|
};
|
|
47692
48608
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
48609
|
let current = node.parent;
|
|
@@ -47766,7 +48682,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
48682
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
48683
|
if (!hookContext) return;
|
|
47768
48684
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48685
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
48686
|
context.report({
|
|
47771
48687
|
node: node.callee,
|
|
47772
48688
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47879,7 +48795,7 @@ const rulesOfHooks = defineRule({
|
|
|
47879
48795
|
Identifier(node) {
|
|
47880
48796
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
48797
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
48798
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47883
48799
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
48800
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
48801
|
context.report({
|
|
@@ -48027,7 +48943,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48027
48943
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48028
48944
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48029
48945
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48030
|
-
const
|
|
48946
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
48947
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48031
48948
|
if (!objectName) return;
|
|
48032
48949
|
const methodName = node.callee.property.name;
|
|
48033
48950
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48703,7 +49620,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48703
49620
|
};
|
|
48704
49621
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48705
49622
|
let chainTip = referenceIdentifier;
|
|
48706
|
-
while (chainTip.parent
|
|
49623
|
+
while (chainTip.parent) {
|
|
49624
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49625
|
+
chainTip = chainTip.parent;
|
|
49626
|
+
continue;
|
|
49627
|
+
}
|
|
49628
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49629
|
+
chainTip = chainTip.parent;
|
|
49630
|
+
continue;
|
|
49631
|
+
}
|
|
49632
|
+
break;
|
|
49633
|
+
}
|
|
48707
49634
|
return chainTip;
|
|
48708
49635
|
};
|
|
48709
49636
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48728,7 +49655,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48728
49655
|
const callee = callExpression.callee;
|
|
48729
49656
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48730
49657
|
const methodName = getMemberPropertyName$1(callee);
|
|
48731
|
-
|
|
49658
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49659
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48732
49660
|
}
|
|
48733
49661
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48734
49662
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49458,7 +50386,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49458
50386
|
};
|
|
49459
50387
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49460
50388
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49461
|
-
let currentNode = outerNode.callee.object;
|
|
50389
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49462
50390
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49463
50391
|
const calleeName = getCalleeName$2(currentNode);
|
|
49464
50392
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49469,7 +50397,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49469
50397
|
}
|
|
49470
50398
|
}
|
|
49471
50399
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49472
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50400
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49473
50401
|
else break;
|
|
49474
50402
|
}
|
|
49475
50403
|
return result;
|
|
@@ -50122,7 +51050,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50122
51050
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50123
51051
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50124
51052
|
if (methodName) methodNames.unshift(methodName);
|
|
50125
|
-
currentNode = currentNode.callee.object;
|
|
51053
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50126
51054
|
}
|
|
50127
51055
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50128
51056
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53204,6 +54132,18 @@ const reactDoctorRules = [
|
|
|
53204
54132
|
category: "Bugs"
|
|
53205
54133
|
}
|
|
53206
54134
|
},
|
|
54135
|
+
{
|
|
54136
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54137
|
+
id: "no-locale-format-in-render",
|
|
54138
|
+
source: "react-doctor",
|
|
54139
|
+
originallyExternal: false,
|
|
54140
|
+
rule: {
|
|
54141
|
+
...noLocaleFormatInRender,
|
|
54142
|
+
framework: "global",
|
|
54143
|
+
category: "Bugs",
|
|
54144
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54145
|
+
}
|
|
54146
|
+
},
|
|
53207
54147
|
{
|
|
53208
54148
|
key: "react-doctor/no-long-transition-duration",
|
|
53209
54149
|
id: "no-long-transition-duration",
|
|
@@ -55451,6 +56391,34 @@ const reactDoctorRules = [
|
|
|
55451
56391
|
];
|
|
55452
56392
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
56393
|
//#endregion
|
|
56394
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
56395
|
+
const isNextFileActive = (context) => {
|
|
56396
|
+
const rawFilename = context.filename;
|
|
56397
|
+
if (!rawFilename) return true;
|
|
56398
|
+
const filename = normalizeFilename(rawFilename);
|
|
56399
|
+
const manifest = readNearestPackageManifest(filename);
|
|
56400
|
+
if (!manifest) return true;
|
|
56401
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
56402
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
56403
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
56404
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
56405
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
56406
|
+
return true;
|
|
56407
|
+
};
|
|
56408
|
+
//#endregion
|
|
56409
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
56410
|
+
const EMPTY_VISITORS$1 = {};
|
|
56411
|
+
const wrapNextjsRule = (rule) => {
|
|
56412
|
+
const innerCreate = rule.create.bind(rule);
|
|
56413
|
+
return {
|
|
56414
|
+
...rule,
|
|
56415
|
+
create: (context) => {
|
|
56416
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
56417
|
+
return innerCreate(context);
|
|
56418
|
+
}
|
|
56419
|
+
};
|
|
56420
|
+
};
|
|
56421
|
+
//#endregion
|
|
55454
56422
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
56423
|
const EMPTY_VISITORS = {};
|
|
55456
56424
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +56888,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
56888
|
});
|
|
55921
56889
|
//#endregion
|
|
55922
56890
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56891
|
+
const applyFrameworkGate = (rule) => {
|
|
56892
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56893
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56894
|
+
return rule;
|
|
56895
|
+
};
|
|
55923
56896
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
56897
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56898
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
56899
|
return wrapped;
|
|
55927
56900
|
};
|
|
55928
56901
|
const plugin = {
|