oxlint-plugin-react-doctor 0.7.2-dev.0eb5293 → 0.7.2-dev.2953b25
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 +92 -0
- package/dist/index.js +1764 -549
- 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,102 @@ 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/get-range-start.ts
|
|
7721
|
+
const getRangeStart = (node) => {
|
|
7722
|
+
const rangeStart = node.range?.[0];
|
|
7723
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7724
|
+
};
|
|
7725
|
+
//#endregion
|
|
7726
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7727
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
7728
|
+
let node = callExpression;
|
|
7729
|
+
let parent = node.parent;
|
|
7730
|
+
while (parent) {
|
|
7731
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7732
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7733
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7734
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7735
|
+
node = parent;
|
|
7736
|
+
parent = node.parent;
|
|
7737
|
+
continue;
|
|
7738
|
+
}
|
|
7739
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7740
|
+
node = parent;
|
|
7741
|
+
parent = node.parent;
|
|
7742
|
+
continue;
|
|
7743
|
+
}
|
|
7744
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7745
|
+
node = parent;
|
|
7746
|
+
parent = node.parent;
|
|
7747
|
+
continue;
|
|
7748
|
+
}
|
|
7749
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7750
|
+
const expressions = parent.expressions ?? [];
|
|
7751
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
7752
|
+
node = parent;
|
|
7753
|
+
parent = node.parent;
|
|
7754
|
+
continue;
|
|
7755
|
+
}
|
|
7756
|
+
return false;
|
|
7757
|
+
}
|
|
7758
|
+
return false;
|
|
7759
|
+
};
|
|
7760
|
+
//#endregion
|
|
7613
7761
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7614
7762
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7615
7763
|
if (!node || typeof node !== "object") return;
|
|
@@ -7761,6 +7909,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7761
7909
|
};
|
|
7762
7910
|
//#endregion
|
|
7763
7911
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7912
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7913
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
7914
|
+
subscribe: "subscription",
|
|
7915
|
+
timer: "timer",
|
|
7916
|
+
socket: "connection"
|
|
7917
|
+
};
|
|
7918
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7919
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
7920
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7921
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7922
|
+
};
|
|
7764
7923
|
const findSubscribeLikeUsages = (callback) => {
|
|
7765
7924
|
const usages = [];
|
|
7766
7925
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7772,6 +7931,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7772
7931
|
}
|
|
7773
7932
|
walkAst(callback, (child) => {
|
|
7774
7933
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7934
|
+
if (isSocketConstruction(child)) {
|
|
7935
|
+
usages.push({
|
|
7936
|
+
kind: "socket",
|
|
7937
|
+
node: child,
|
|
7938
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7939
|
+
});
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7775
7942
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7776
7943
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7777
7944
|
usages.push({
|
|
@@ -7781,7 +7948,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7781
7948
|
});
|
|
7782
7949
|
return;
|
|
7783
7950
|
}
|
|
7784
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7951
|
+
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
7952
|
kind: "subscribe",
|
|
7786
7953
|
node: child,
|
|
7787
7954
|
resourceName: child.callee.property.name
|
|
@@ -7797,6 +7964,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7797
7964
|
};
|
|
7798
7965
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7799
7966
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7967
|
+
walkAst(effectCallback.body, (child) => {
|
|
7968
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
7969
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7970
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7971
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7972
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7973
|
+
});
|
|
7800
7974
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7801
7975
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7802
7976
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7804,7 +7978,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7804
7978
|
const bindingName = declarator.id.name;
|
|
7805
7979
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7806
7980
|
const init = declarator.init;
|
|
7807
|
-
if (!init
|
|
7981
|
+
if (!init) continue;
|
|
7982
|
+
if (isSocketConstruction(init)) {
|
|
7983
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7984
|
+
continue;
|
|
7985
|
+
}
|
|
7986
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7808
7987
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7809
7988
|
bindings.subscriptionNames.add(bindingName);
|
|
7810
7989
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7830,9 +8009,21 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7830
8009
|
});
|
|
7831
8010
|
return bindings;
|
|
7832
8011
|
};
|
|
7833
|
-
const
|
|
7834
|
-
|
|
7835
|
-
|
|
8012
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8013
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8014
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8015
|
+
const releaseStarts = [];
|
|
8016
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8017
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8018
|
+
const releaseStart = getRangeStart(child);
|
|
8019
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8020
|
+
});
|
|
8021
|
+
if (releaseStarts.length === 0) return usages;
|
|
8022
|
+
return usages.filter((usage) => {
|
|
8023
|
+
const usageStart = getRangeStart(usage.node);
|
|
8024
|
+
if (usageStart === null) return true;
|
|
8025
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8026
|
+
});
|
|
7836
8027
|
};
|
|
7837
8028
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7838
8029
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
@@ -7856,26 +8047,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7856
8047
|
});
|
|
7857
8048
|
return didFindCleanupReturn;
|
|
7858
8049
|
};
|
|
8050
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8051
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8052
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8053
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8054
|
+
if (keyName === "signal") return true;
|
|
8055
|
+
if (keyName !== "once") return false;
|
|
8056
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8057
|
+
};
|
|
8058
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8059
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8060
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8061
|
+
["addListener", new Set([
|
|
8062
|
+
"removeListener",
|
|
8063
|
+
"off",
|
|
8064
|
+
"abort"
|
|
8065
|
+
])],
|
|
8066
|
+
["on", new Set([
|
|
8067
|
+
"off",
|
|
8068
|
+
"removeListener",
|
|
8069
|
+
"on"
|
|
8070
|
+
])],
|
|
8071
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8072
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8073
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8074
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8075
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8076
|
+
]);
|
|
8077
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8078
|
+
"cleanup",
|
|
8079
|
+
"dispose",
|
|
8080
|
+
"destroy",
|
|
8081
|
+
"teardown"
|
|
8082
|
+
]);
|
|
8083
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8084
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8085
|
+
const getReleaseVerbName = (node) => {
|
|
8086
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8087
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8088
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8089
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8090
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8091
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8092
|
+
return null;
|
|
8093
|
+
};
|
|
8094
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8095
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8096
|
+
let didFindPairedRelease = false;
|
|
8097
|
+
walkAst(body, (child) => {
|
|
8098
|
+
if (didFindPairedRelease) return false;
|
|
8099
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8100
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8101
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8102
|
+
didFindPairedRelease = true;
|
|
8103
|
+
return false;
|
|
8104
|
+
}
|
|
8105
|
+
});
|
|
8106
|
+
return didFindPairedRelease;
|
|
8107
|
+
};
|
|
8108
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8109
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8110
|
+
let programNode = anyNode;
|
|
8111
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8112
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8113
|
+
if (cached) return cached;
|
|
8114
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8115
|
+
walkAst(programNode, (child) => {
|
|
8116
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8117
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8118
|
+
});
|
|
8119
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8120
|
+
return releaseVerbNames;
|
|
8121
|
+
};
|
|
8122
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8123
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8124
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8125
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8126
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8127
|
+
return false;
|
|
8128
|
+
};
|
|
8129
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8130
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8131
|
+
const body = retainedFunction.body;
|
|
8132
|
+
if (!body) return null;
|
|
8133
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8134
|
+
let leak = null;
|
|
8135
|
+
walkAst(body, (child) => {
|
|
8136
|
+
if (leak !== null) return false;
|
|
8137
|
+
if (isFunctionLike$1(child)) return false;
|
|
8138
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8139
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8140
|
+
leak = {
|
|
8141
|
+
kind: "socket",
|
|
8142
|
+
node: child,
|
|
8143
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8144
|
+
};
|
|
8145
|
+
return false;
|
|
8146
|
+
}
|
|
8147
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8148
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8149
|
+
leak = {
|
|
8150
|
+
kind: "timer",
|
|
8151
|
+
node: child,
|
|
8152
|
+
resourceName: "setInterval"
|
|
8153
|
+
};
|
|
8154
|
+
return false;
|
|
8155
|
+
}
|
|
8156
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8157
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8158
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8159
|
+
kind: "subscribe",
|
|
8160
|
+
node: child,
|
|
8161
|
+
resourceName: registrationVerbName
|
|
8162
|
+
};
|
|
8163
|
+
return false;
|
|
8164
|
+
}
|
|
8165
|
+
});
|
|
8166
|
+
return leak;
|
|
8167
|
+
};
|
|
8168
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8169
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8170
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8171
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8172
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8173
|
+
};
|
|
7859
8174
|
const effectNeedsCleanup = defineRule({
|
|
7860
8175
|
id: "effect-needs-cleanup",
|
|
7861
8176
|
title: "Effect subscription or timer never cleaned up",
|
|
7862
8177
|
severity: "error",
|
|
7863
8178
|
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
|
-
|
|
8179
|
+
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.",
|
|
8180
|
+
create: (context) => {
|
|
8181
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8182
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8183
|
+
if (!leak) return;
|
|
8184
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8185
|
+
context.report({
|
|
8186
|
+
node: leak.node,
|
|
8187
|
+
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.`
|
|
8188
|
+
});
|
|
8189
|
+
};
|
|
8190
|
+
return {
|
|
8191
|
+
CallExpression(node) {
|
|
8192
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8193
|
+
const retainedCallback = getEffectCallback(node);
|
|
8194
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8195
|
+
return;
|
|
8196
|
+
}
|
|
8197
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8198
|
+
const callback = getEffectCallback(node);
|
|
8199
|
+
if (!callback) return;
|
|
8200
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8201
|
+
if (usages.length === 0) return;
|
|
8202
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8203
|
+
const firstUsage = usages[0];
|
|
8204
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8205
|
+
context.report({
|
|
8206
|
+
node,
|
|
8207
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8208
|
+
});
|
|
8209
|
+
},
|
|
8210
|
+
FunctionDeclaration(node) {
|
|
8211
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8212
|
+
},
|
|
8213
|
+
ArrowFunctionExpression(node) {
|
|
8214
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8215
|
+
},
|
|
8216
|
+
FunctionExpression(node) {
|
|
8217
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8218
|
+
}
|
|
8219
|
+
};
|
|
8220
|
+
}
|
|
7879
8221
|
});
|
|
7880
8222
|
//#endregion
|
|
7881
8223
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8434,17 +8776,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8434
8776
|
return computedCaptures;
|
|
8435
8777
|
};
|
|
8436
8778
|
//#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
8779
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8449
8780
|
const reactHocCalleeName = (callee) => {
|
|
8450
8781
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8658,8 +8989,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8658
8989
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8659
8990
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8660
8991
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8661
|
-
* `symbolHasStableHookOrigin` / `
|
|
8662
|
-
*
|
|
8992
|
+
* `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
|
|
8993
|
+
* multiple sites — extracting
|
|
8663
8994
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8664
8995
|
* declared logic.
|
|
8665
8996
|
*
|
|
@@ -8715,11 +9046,6 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
8715
9046
|
}
|
|
8716
9047
|
return false;
|
|
8717
9048
|
};
|
|
8718
|
-
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
8719
|
-
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8720
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8721
|
-
return getHookName(initializer.callee) === "useEffectEvent";
|
|
8722
|
-
};
|
|
8723
9049
|
const getFunctionValueNode = (symbol) => {
|
|
8724
9050
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
8725
9051
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -8805,6 +9131,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
8805
9131
|
};
|
|
8806
9132
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
8807
9133
|
//#endregion
|
|
9134
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9135
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9136
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9137
|
+
if (importSource === null) return false;
|
|
9138
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9139
|
+
};
|
|
9140
|
+
//#endregion
|
|
9141
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9142
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9143
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9144
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
9145
|
+
};
|
|
9146
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9147
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9148
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9149
|
+
return false;
|
|
9150
|
+
};
|
|
9151
|
+
//#endregion
|
|
9152
|
+
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9153
|
+
const getUseEffectEventCalleeName = (callee) => {
|
|
9154
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9155
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9156
|
+
return null;
|
|
9157
|
+
};
|
|
9158
|
+
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9159
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9160
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9161
|
+
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9162
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9163
|
+
};
|
|
9164
|
+
//#endregion
|
|
8808
9165
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
8809
9166
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
8810
9167
|
"useEffect",
|
|
@@ -9455,7 +9812,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9455
9812
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9456
9813
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9457
9814
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9458
|
-
if (depSymbol &&
|
|
9815
|
+
if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9459
9816
|
context.report({
|
|
9460
9817
|
node: elementNode,
|
|
9461
9818
|
message: buildEffectEventDepMessage()
|
|
@@ -9616,7 +9973,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9973
|
});
|
|
9617
9974
|
//#endregion
|
|
9618
9975
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9976
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9977
|
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
9978
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9979
|
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 +9985,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9985
|
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
9986
|
create: (context) => {
|
|
9630
9987
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9988
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9989
|
return {
|
|
9633
9990
|
MemberExpression(node) {
|
|
9634
9991
|
if (!node.computed) return;
|
|
@@ -9881,7 +10238,10 @@ const PRAGMA = "React";
|
|
|
9881
10238
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9882
10239
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9883
10240
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9884
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10241
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10242
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10243
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10244
|
+
}
|
|
9885
10245
|
return true;
|
|
9886
10246
|
};
|
|
9887
10247
|
//#endregion
|
|
@@ -11046,8 +11406,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11406
|
if (node.attributes.length === 0) return;
|
|
11047
11407
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11408
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11409
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11410
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11411
|
let hasInteractiveHandler = false;
|
|
11052
11412
|
for (const attribute of node.attributes) {
|
|
11053
11413
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11421,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11421
|
const elementType = getElementType(node, context.settings);
|
|
11062
11422
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11423
|
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
11424
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11425
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11426
|
+
for (const role of roleCandidates) {
|
|
11427
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11428
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11429
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11430
|
+
}
|
|
11431
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11432
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11433
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11434
|
context.report({
|
|
11070
11435
|
node,
|
|
11071
11436
|
message
|
|
@@ -11254,16 +11619,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11254
11619
|
return names;
|
|
11255
11620
|
};
|
|
11256
11621
|
//#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
11622
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11268
11623
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11269
11624
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -11963,14 +12318,15 @@ const jsCacheStorage = defineRule({
|
|
|
11963
12318
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
11964
12319
|
CallExpression(node) {
|
|
11965
12320
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
11966
|
-
|
|
12321
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12322
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11967
12323
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
11968
12324
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
11969
12325
|
const storageKey = String(node.arguments[0].value);
|
|
11970
12326
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
11971
12327
|
storageReadCounts.set(storageKey, readCount);
|
|
11972
12328
|
if (readCount === 2) {
|
|
11973
|
-
const storageName =
|
|
12329
|
+
const storageName = receiver.name;
|
|
11974
12330
|
context.report({
|
|
11975
12331
|
node,
|
|
11976
12332
|
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 +12340,16 @@ const jsCacheStorage = defineRule({
|
|
|
11984
12340
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
11985
12341
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
11986
12342
|
const callee = callExpression.callee;
|
|
11987
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
12343
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12344
|
+
const receiver = stripParenExpression(callee.object);
|
|
12345
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12346
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12347
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12348
|
+
return true;
|
|
12349
|
+
}
|
|
12350
|
+
return false;
|
|
11993
12351
|
}
|
|
12352
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
11994
12353
|
return false;
|
|
11995
12354
|
};
|
|
11996
12355
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12002,10 +12361,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12002
12361
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12003
12362
|
let cursor = receiverNode;
|
|
12004
12363
|
while (cursor) {
|
|
12005
|
-
|
|
12006
|
-
cursor = cursor.expression;
|
|
12007
|
-
continue;
|
|
12008
|
-
}
|
|
12364
|
+
cursor = stripParenExpression(cursor);
|
|
12009
12365
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12010
12366
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12011
12367
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12081,10 +12437,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12081
12437
|
let hops = 0;
|
|
12082
12438
|
while (cursor && hops < 12) {
|
|
12083
12439
|
hops += 1;
|
|
12084
|
-
|
|
12085
|
-
cursor = cursor.expression;
|
|
12086
|
-
continue;
|
|
12087
|
-
}
|
|
12440
|
+
cursor = stripParenExpression(cursor);
|
|
12088
12441
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12089
12442
|
const callee = cursor.callee;
|
|
12090
12443
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12108,10 +12461,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12108
12461
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12109
12462
|
let cursor = receiverNode;
|
|
12110
12463
|
while (cursor) {
|
|
12111
|
-
|
|
12112
|
-
cursor = cursor.expression;
|
|
12113
|
-
continue;
|
|
12114
|
-
}
|
|
12464
|
+
cursor = stripParenExpression(cursor);
|
|
12115
12465
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12116
12466
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12117
12467
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12176,7 +12526,7 @@ const jsCombineIterations = defineRule({
|
|
|
12176
12526
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12177
12527
|
const outerMethod = node.callee.property.name;
|
|
12178
12528
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12179
|
-
const innerCall = node.callee.object;
|
|
12529
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12180
12530
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12181
12531
|
const innerMethod = innerCall.callee.property.name;
|
|
12182
12532
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12249,10 +12599,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12249
12599
|
if (!filterArgument) return;
|
|
12250
12600
|
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
12601
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12252
|
-
const innerCall = node.callee.object;
|
|
12602
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12253
12603
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12254
12604
|
if (innerCall.callee.property.name !== "map") return;
|
|
12255
|
-
const receiver = innerCall.callee.object;
|
|
12605
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12256
12606
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12257
12607
|
const elements = receiver.elements ?? [];
|
|
12258
12608
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13512,7 +13862,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13512
13862
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13513
13863
|
create: (context) => ({ CallExpression(node) {
|
|
13514
13864
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13515
|
-
const receiver = node.callee.object;
|
|
13865
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13516
13866
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13517
13867
|
const spreadArgument = receiver.elements[0].argument;
|
|
13518
13868
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18549,7 +18899,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18549
18899
|
};
|
|
18550
18900
|
const describeClientSideNavigation = (node) => {
|
|
18551
18901
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18552
|
-
const
|
|
18902
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18903
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18553
18904
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18554
18905
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18555
18906
|
}
|
|
@@ -19169,7 +19520,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19169
19520
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19170
19521
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19171
19522
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19172
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19523
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19173
19524
|
return node.callee.property.name;
|
|
19174
19525
|
};
|
|
19175
19526
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19183,7 +19534,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19183
19534
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19184
19535
|
const { property, object } = node.callee;
|
|
19185
19536
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19186
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19537
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19187
19538
|
return true;
|
|
19188
19539
|
};
|
|
19189
19540
|
const getDbCallDescription = (node) => {
|
|
@@ -19191,7 +19542,8 @@ const getDbCallDescription = (node) => {
|
|
|
19191
19542
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19192
19543
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19193
19544
|
const methodName = node.callee.property.name;
|
|
19194
|
-
const
|
|
19545
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19546
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19195
19547
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19196
19548
|
};
|
|
19197
19549
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20591,13 +20943,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20591
20943
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20592
20944
|
let node = ref.identifier;
|
|
20593
20945
|
let parent = node.parent;
|
|
20594
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20946
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20595
20947
|
node = parent;
|
|
20596
20948
|
parent = node.parent;
|
|
20597
20949
|
}
|
|
20598
20950
|
if (current.callee === node) return current;
|
|
20599
20951
|
}
|
|
20600
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20952
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20601
20953
|
return null;
|
|
20602
20954
|
};
|
|
20603
20955
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20732,7 +21084,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20732
21084
|
const importDeclaration = declarationNode.parent;
|
|
20733
21085
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20734
21086
|
}));
|
|
20735
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21087
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20736
21088
|
if (!node) return false;
|
|
20737
21089
|
if (isNodeOfType(node, "Identifier")) {
|
|
20738
21090
|
if (node.name === hookName) return true;
|
|
@@ -20741,15 +21093,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20741
21093
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20742
21094
|
return false;
|
|
20743
21095
|
}
|
|
20744
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21096
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21097
|
+
const receiver = stripParenExpression(node.object);
|
|
21098
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21099
|
+
}
|
|
20745
21100
|
return false;
|
|
20746
21101
|
};
|
|
20747
21102
|
const isUseEffect = (node) => {
|
|
20748
21103
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20749
21104
|
const callee = node.callee;
|
|
20750
21105
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20751
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20752
|
-
|
|
21106
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21107
|
+
const receiver = stripParenExpression(callee.object);
|
|
21108
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20753
21109
|
};
|
|
20754
21110
|
const getEffectFn = (analysis, node) => {
|
|
20755
21111
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20777,7 +21133,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20777
21133
|
const node = def.node;
|
|
20778
21134
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20779
21135
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20780
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21136
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20781
21137
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20782
21138
|
const elements = node.id.elements ?? [];
|
|
20783
21139
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20788,7 +21144,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20788
21144
|
const node = def.node;
|
|
20789
21145
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20790
21146
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20791
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21147
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20792
21148
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20793
21149
|
const elements = node.id.elements ?? [];
|
|
20794
21150
|
if (elements.length !== 2) return false;
|
|
@@ -20835,7 +21191,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20835
21191
|
const node = def.node;
|
|
20836
21192
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20837
21193
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20838
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21194
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20839
21195
|
}));
|
|
20840
21196
|
const isRefCurrent = (ref) => {
|
|
20841
21197
|
const parent = ref.identifier.parent;
|
|
@@ -20848,11 +21204,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20848
21204
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20849
21205
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20850
21206
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20851
|
-
|
|
20852
|
-
|
|
21207
|
+
let effectiveNode = ref.identifier;
|
|
21208
|
+
let parent = effectiveNode.parent;
|
|
21209
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21210
|
+
effectiveNode = parent;
|
|
21211
|
+
parent = effectiveNode.parent;
|
|
21212
|
+
}
|
|
20853
21213
|
if (!parent) return false;
|
|
20854
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20855
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21214
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21215
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20856
21216
|
const memberParent = parent.parent;
|
|
20857
21217
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20858
21218
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20863,7 +21223,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20863
21223
|
};
|
|
20864
21224
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20865
21225
|
const getUseStateDecl = (analysis, ref) => {
|
|
20866
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21226
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20867
21227
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20868
21228
|
return node ?? null;
|
|
20869
21229
|
};
|
|
@@ -21068,7 +21428,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21068
21428
|
const noAdjustStateOnPropChange = defineRule({
|
|
21069
21429
|
id: "no-adjust-state-on-prop-change",
|
|
21070
21430
|
title: "State synced to a prop inside an effect",
|
|
21071
|
-
severity: "
|
|
21431
|
+
severity: "warn",
|
|
21072
21432
|
tags: ["test-noise"],
|
|
21073
21433
|
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
21434
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21109,7 +21469,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21109
21469
|
"summary",
|
|
21110
21470
|
"textarea"
|
|
21111
21471
|
]);
|
|
21472
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21473
|
+
const value = attribute.value;
|
|
21474
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21475
|
+
const expression = value.expression;
|
|
21476
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21477
|
+
};
|
|
21478
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21479
|
+
"button",
|
|
21480
|
+
"input",
|
|
21481
|
+
"select",
|
|
21482
|
+
"textarea"
|
|
21483
|
+
]);
|
|
21112
21484
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21485
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21486
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21487
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21488
|
+
}
|
|
21113
21489
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21114
21490
|
switch (tagName) {
|
|
21115
21491
|
case "input": {
|
|
@@ -21123,7 +21499,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21123
21499
|
case "a":
|
|
21124
21500
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21125
21501
|
case "audio":
|
|
21126
|
-
case "video":
|
|
21502
|
+
case "video": {
|
|
21503
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21504
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21505
|
+
}
|
|
21127
21506
|
default: return false;
|
|
21128
21507
|
}
|
|
21129
21508
|
};
|
|
@@ -22087,7 +22466,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22087
22466
|
"some"
|
|
22088
22467
|
]);
|
|
22089
22468
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22090
|
-
const isPositionallyStableIterationReceiver = (
|
|
22469
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22470
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22091
22471
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22092
22472
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22093
22473
|
const only = receiver.elements[0];
|
|
@@ -22098,17 +22478,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22098
22478
|
}
|
|
22099
22479
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22100
22480
|
const callee = receiver.callee;
|
|
22101
|
-
if (
|
|
22481
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22102
22482
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22103
22483
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22104
22484
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22105
22485
|
return false;
|
|
22106
22486
|
};
|
|
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
|
-
};
|
|
22487
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22112
22488
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22113
22489
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22114
22490
|
for (const property of source.properties ?? []) {
|
|
@@ -22167,18 +22543,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22167
22543
|
return false;
|
|
22168
22544
|
}
|
|
22169
22545
|
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;
|
|
22546
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22171
22547
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22172
22548
|
}
|
|
22173
22549
|
return false;
|
|
22174
22550
|
};
|
|
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
|
-
};
|
|
22551
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22182
22552
|
const noArrayIndexKey = defineRule({
|
|
22183
22553
|
id: "no-array-index-key",
|
|
22184
22554
|
title: "Array index used as a key",
|
|
@@ -22477,6 +22847,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22477
22847
|
const section = manifest[sectionName];
|
|
22478
22848
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22479
22849
|
});
|
|
22850
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22851
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22852
|
+
return false;
|
|
22853
|
+
};
|
|
22480
22854
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22481
22855
|
const classifyPackagePlatform = (filename) => {
|
|
22482
22856
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22493,9 +22867,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22493
22867
|
return result;
|
|
22494
22868
|
};
|
|
22495
22869
|
//#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?$/;
|
|
22870
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22499
22871
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22500
22872
|
const resolveRealDirectory = (directory) => {
|
|
22501
22873
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22516,6 +22888,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22516
22888
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22517
22889
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22518
22890
|
};
|
|
22891
|
+
//#endregion
|
|
22892
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22893
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22894
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22519
22895
|
const classifyReactNativeFileTarget = (context) => {
|
|
22520
22896
|
const rawFilename = context.filename;
|
|
22521
22897
|
if (!rawFilename) return "unknown";
|
|
@@ -22896,7 +23272,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22896
23272
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22897
23273
|
return false;
|
|
22898
23274
|
};
|
|
22899
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23275
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22900
23276
|
"forEach",
|
|
22901
23277
|
"map",
|
|
22902
23278
|
"filter",
|
|
@@ -22917,31 +23293,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22917
23293
|
if (parent.callee === functionNode) return true;
|
|
22918
23294
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22919
23295
|
const callee = parent.callee;
|
|
22920
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23296
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22921
23297
|
};
|
|
22922
23298
|
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) => {
|
|
23299
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23300
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22932
23301
|
let fallThroughCount = 0;
|
|
22933
|
-
let
|
|
23302
|
+
let maxTerminatedCount = 0;
|
|
22934
23303
|
for (const statement of statements) {
|
|
22935
|
-
if (
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
23304
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23305
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23306
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23307
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23308
|
+
fallThroughCount: 0,
|
|
23309
|
+
maxTerminatedCount: 0,
|
|
23310
|
+
doAllPathsTerminate: false
|
|
23311
|
+
};
|
|
23312
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23313
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23314
|
+
fallThroughCount: 0,
|
|
23315
|
+
maxTerminatedCount,
|
|
23316
|
+
doAllPathsTerminate: true
|
|
23317
|
+
};
|
|
23318
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23319
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22939
23320
|
continue;
|
|
22940
23321
|
}
|
|
22941
|
-
if (isTerminatingStatement(statement))
|
|
23322
|
+
if (isTerminatingStatement(statement)) {
|
|
23323
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23324
|
+
return {
|
|
23325
|
+
fallThroughCount: 0,
|
|
23326
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23327
|
+
doAllPathsTerminate: true
|
|
23328
|
+
};
|
|
23329
|
+
}
|
|
22942
23330
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22943
23331
|
}
|
|
22944
|
-
return
|
|
23332
|
+
return {
|
|
23333
|
+
fallThroughCount,
|
|
23334
|
+
maxTerminatedCount,
|
|
23335
|
+
doAllPathsTerminate: false
|
|
23336
|
+
};
|
|
23337
|
+
};
|
|
23338
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23339
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23340
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22945
23341
|
};
|
|
22946
23342
|
const collectLocalHelperFunctions = (root) => {
|
|
22947
23343
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -22968,14 +23364,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22968
23364
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22969
23365
|
return parent.parent === effectCallback.body;
|
|
22970
23366
|
};
|
|
23367
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23368
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23369
|
+
const body = functionNode.body;
|
|
23370
|
+
if (!body) return 0;
|
|
23371
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23372
|
+
};
|
|
22971
23373
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22972
23374
|
if (!node || typeof node !== "object") return 0;
|
|
22973
|
-
if (
|
|
23375
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22974
23376
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22975
23377
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22976
23378
|
const consequent = node.consequent;
|
|
22977
23379
|
const alternate = node.alternate;
|
|
22978
|
-
|
|
23380
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23381
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23382
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23383
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22979
23384
|
}
|
|
22980
23385
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22981
23386
|
let maxRunSetters = 0;
|
|
@@ -23005,28 +23410,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23005
23410
|
}
|
|
23006
23411
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23007
23412
|
let nestedSettersInArgs = 0;
|
|
23008
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23413
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23009
23414
|
return 1 + nestedSettersInArgs;
|
|
23010
23415
|
}
|
|
23011
23416
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23012
23417
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23013
23418
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23014
23419
|
context.activeHelpers.add(helperFunction);
|
|
23015
|
-
let helperCount =
|
|
23420
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23016
23421
|
context.activeHelpers.delete(helperFunction);
|
|
23017
23422
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23018
23423
|
return helperCount;
|
|
23019
23424
|
}
|
|
23020
23425
|
}
|
|
23021
|
-
const
|
|
23426
|
+
const countChild = (child) => {
|
|
23427
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23428
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23429
|
+
};
|
|
23022
23430
|
let total = 0;
|
|
23023
23431
|
const nodeRecord = node;
|
|
23024
23432
|
for (const key of Object.keys(nodeRecord)) {
|
|
23025
23433
|
if (key === "parent") continue;
|
|
23026
23434
|
const child = nodeRecord[key];
|
|
23027
23435
|
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
|
|
23436
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23437
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23030
23438
|
}
|
|
23031
23439
|
return total;
|
|
23032
23440
|
};
|
|
@@ -23060,7 +23468,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23060
23468
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23061
23469
|
const firstStatement = (body.body ?? [])[0];
|
|
23062
23470
|
if (!firstStatement) return false;
|
|
23063
|
-
if (!
|
|
23471
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23472
|
+
const consequent = firstStatement.consequent;
|
|
23473
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23064
23474
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23065
23475
|
};
|
|
23066
23476
|
const noCascadingSetState = defineRule({
|
|
@@ -23075,7 +23485,7 @@ const noCascadingSetState = defineRule({
|
|
|
23075
23485
|
const callback = getEffectCallback(node);
|
|
23076
23486
|
if (!callback) return;
|
|
23077
23487
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23078
|
-
const setStateCallCount =
|
|
23488
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23079
23489
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23080
23490
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23081
23491
|
effectCallback: callback
|
|
@@ -23419,40 +23829,6 @@ const noCloneElement = defineRule({
|
|
|
23419
23829
|
} })
|
|
23420
23830
|
});
|
|
23421
23831
|
//#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
23832
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23457
23833
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23458
23834
|
const CONTEXT_MODULES = [
|
|
@@ -24423,11 +24799,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24423
24799
|
return isInitialOnlyPropName(arg.name);
|
|
24424
24800
|
};
|
|
24425
24801
|
//#endregion
|
|
24802
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24803
|
+
const isNoOpStatement = (statement) => {
|
|
24804
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24805
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24806
|
+
const expression = stripParenExpression(statement.expression);
|
|
24807
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24808
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24809
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24810
|
+
return false;
|
|
24811
|
+
};
|
|
24812
|
+
//#endregion
|
|
24426
24813
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24427
24814
|
const getCallbackStatements = (callback) => {
|
|
24428
24815
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24429
|
-
|
|
24430
|
-
return callback.body ? [callback.body] : [];
|
|
24816
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24431
24817
|
};
|
|
24432
24818
|
//#endregion
|
|
24433
24819
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24466,6 +24852,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24466
24852
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24467
24853
|
}
|
|
24468
24854
|
};
|
|
24855
|
+
const flattenGuardedStatements = (statements) => {
|
|
24856
|
+
const flattened = [];
|
|
24857
|
+
for (const statement of statements) {
|
|
24858
|
+
if (isNoOpStatement(statement)) continue;
|
|
24859
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24860
|
+
flattened.push(statement);
|
|
24861
|
+
continue;
|
|
24862
|
+
}
|
|
24863
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24864
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24865
|
+
if (!branch) continue;
|
|
24866
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24867
|
+
if (flattenedBranch === null) return null;
|
|
24868
|
+
flattened.push(...flattenedBranch);
|
|
24869
|
+
}
|
|
24870
|
+
continue;
|
|
24871
|
+
}
|
|
24872
|
+
return null;
|
|
24873
|
+
}
|
|
24874
|
+
return flattened;
|
|
24875
|
+
};
|
|
24469
24876
|
const noDerivedStateEffect = defineRule({
|
|
24470
24877
|
id: "no-derived-state-effect",
|
|
24471
24878
|
title: "Derived state stored in an effect",
|
|
@@ -24497,8 +24904,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24497
24904
|
}
|
|
24498
24905
|
}
|
|
24499
24906
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24500
|
-
const statements = getCallbackStatements(callback);
|
|
24501
|
-
if (statements.length === 0) return;
|
|
24907
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24908
|
+
if (statements === null || statements.length === 0) return;
|
|
24502
24909
|
if (!statements.every((statement) => {
|
|
24503
24910
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24504
24911
|
const expression = statement.expression;
|
|
@@ -25133,7 +25540,7 @@ const noDidMountSetState = defineRule({
|
|
|
25133
25540
|
const { mode } = resolveSettings$20(context.settings);
|
|
25134
25541
|
return { CallExpression(node) {
|
|
25135
25542
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25136
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25543
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25137
25544
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25138
25545
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25139
25546
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25258,7 +25665,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25258
25665
|
const { mode } = resolveSettings$19(context.settings);
|
|
25259
25666
|
return { CallExpression(node) {
|
|
25260
25667
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25261
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25668
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25262
25669
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25263
25670
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25264
25671
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25355,18 +25762,45 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25355
25762
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25356
25763
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25357
25764
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
25765
|
+
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
25766
|
+
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
25767
|
+
"assign",
|
|
25768
|
+
"entries",
|
|
25769
|
+
"fromEntries",
|
|
25770
|
+
"keys",
|
|
25771
|
+
"values"
|
|
25772
|
+
]);
|
|
25773
|
+
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
25774
|
+
"map",
|
|
25775
|
+
"filter",
|
|
25776
|
+
"slice",
|
|
25777
|
+
"concat",
|
|
25778
|
+
"flat",
|
|
25779
|
+
"flatMap",
|
|
25780
|
+
"toSorted",
|
|
25781
|
+
"toReversed",
|
|
25782
|
+
"toSpliced",
|
|
25783
|
+
"with"
|
|
25784
|
+
]);
|
|
25358
25785
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25359
25786
|
const isPlainDataProducerCall = (expression) => {
|
|
25360
25787
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25361
25788
|
const callee = expression.callee;
|
|
25362
25789
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25363
25790
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25364
|
-
if (isNodeOfType(callee.object, "Identifier")
|
|
25791
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
25792
|
+
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25793
|
+
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
25794
|
+
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
25795
|
+
}
|
|
25365
25796
|
return producesPlainStateValue(callee.object);
|
|
25366
25797
|
};
|
|
25798
|
+
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
25799
|
+
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25367
25800
|
const producesPlainStateValue = (expression) => {
|
|
25368
25801
|
const unwrapped = stripParenExpression(expression);
|
|
25369
25802
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25803
|
+
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25370
25804
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25371
25805
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25372
25806
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25381,6 +25815,38 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25381
25815
|
}
|
|
25382
25816
|
return producesPlainStateValue(unwrapped);
|
|
25383
25817
|
};
|
|
25818
|
+
const producesOpaqueInstanceValue = (expression) => {
|
|
25819
|
+
if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
|
|
25820
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25821
|
+
const callee = expression.callee;
|
|
25822
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
25823
|
+
if (isPlainDataProducerCall(expression)) return false;
|
|
25824
|
+
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
25825
|
+
return true;
|
|
25826
|
+
};
|
|
25827
|
+
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25828
|
+
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25829
|
+
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25830
|
+
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
25831
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25832
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25833
|
+
const setterName = node.callee.name;
|
|
25834
|
+
if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
|
|
25835
|
+
const argument = node.arguments?.[0];
|
|
25836
|
+
if (!argument) return;
|
|
25837
|
+
const unwrapped = stripParenExpression(argument);
|
|
25838
|
+
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
25839
|
+
if (producesPlainStateValue(unwrapped)) {
|
|
25840
|
+
plainFedSetterNames.add(setterName);
|
|
25841
|
+
return;
|
|
25842
|
+
}
|
|
25843
|
+
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25844
|
+
}, true);
|
|
25845
|
+
return {
|
|
25846
|
+
plainFedSetterNames,
|
|
25847
|
+
opaqueFedSetterNames
|
|
25848
|
+
};
|
|
25849
|
+
};
|
|
25384
25850
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25385
25851
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25386
25852
|
walkAst(componentBody, (node) => {
|
|
@@ -25450,11 +25916,15 @@ const noDirectStateMutation = defineRule({
|
|
|
25450
25916
|
if (bindings.length === 0) return;
|
|
25451
25917
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25452
25918
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25919
|
+
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25453
25920
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25454
25921
|
for (const binding of bindings) {
|
|
25455
25922
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25456
25923
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25457
|
-
|
|
25924
|
+
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
25925
|
+
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
25926
|
+
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
25927
|
+
plainObjectStateValueNames.add(binding.valueName);
|
|
25458
25928
|
}
|
|
25459
25929
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25460
25930
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -25579,9 +26049,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25579
26049
|
create: (context) => ({ CallExpression(node) {
|
|
25580
26050
|
const callee = node.callee;
|
|
25581
26051
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25582
|
-
|
|
26052
|
+
const receiver = stripParenExpression(callee.object);
|
|
26053
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25583
26054
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25584
|
-
if (context.scopes.symbolFor(
|
|
26055
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25585
26056
|
if (!importsReactViewTransition(node)) return;
|
|
25586
26057
|
context.report({
|
|
25587
26058
|
node,
|
|
@@ -25601,7 +26072,8 @@ const noDocumentWrite = defineRule({
|
|
|
25601
26072
|
create: (context) => ({ CallExpression(node) {
|
|
25602
26073
|
const callee = node.callee;
|
|
25603
26074
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25604
|
-
|
|
26075
|
+
const receiver = stripParenExpression(callee.object);
|
|
26076
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25605
26077
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25606
26078
|
context.report({
|
|
25607
26079
|
node,
|
|
@@ -26233,13 +26705,6 @@ const noEffectEventHandler = defineRule({
|
|
|
26233
26705
|
}
|
|
26234
26706
|
});
|
|
26235
26707
|
//#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
26708
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26244
26709
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26245
26710
|
const componentBindingStack = [];
|
|
@@ -26293,7 +26758,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26293
26758
|
const initializer = declaratorNode.init;
|
|
26294
26759
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26295
26760
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26296
|
-
if (
|
|
26761
|
+
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
26297
26762
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26298
26763
|
} });
|
|
26299
26764
|
return {
|
|
@@ -28873,7 +29338,7 @@ const noIsMounted = defineRule({
|
|
|
28873
29338
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28874
29339
|
create: (context) => ({ CallExpression(node) {
|
|
28875
29340
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28876
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29341
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28877
29342
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28878
29343
|
if (!getParentComponent(node)) return;
|
|
28879
29344
|
context.report({
|
|
@@ -28888,7 +29353,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28888
29353
|
const isJsonMethodCall = (node, method) => {
|
|
28889
29354
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28890
29355
|
const callee = node.callee;
|
|
28891
|
-
|
|
29356
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29357
|
+
const receiver = stripParenExpression(callee.object);
|
|
29358
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28892
29359
|
};
|
|
28893
29360
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28894
29361
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -28973,7 +29440,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
28973
29440
|
const noJsxElementType = defineRule({
|
|
28974
29441
|
id: "no-jsx-element-type",
|
|
28975
29442
|
title: "No JSX.Element",
|
|
28976
|
-
severity: "
|
|
29443
|
+
severity: "warn",
|
|
28977
29444
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
28978
29445
|
create: (context) => {
|
|
28979
29446
|
let isJsxImported = false;
|
|
@@ -29299,6 +29766,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29299
29766
|
}
|
|
29300
29767
|
});
|
|
29301
29768
|
//#endregion
|
|
29769
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29770
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29771
|
+
"map",
|
|
29772
|
+
"filter",
|
|
29773
|
+
"forEach",
|
|
29774
|
+
"flatMap",
|
|
29775
|
+
"reduce",
|
|
29776
|
+
"reduceRight",
|
|
29777
|
+
"some",
|
|
29778
|
+
"every",
|
|
29779
|
+
"find",
|
|
29780
|
+
"findIndex",
|
|
29781
|
+
"findLast",
|
|
29782
|
+
"findLastIndex",
|
|
29783
|
+
"sort",
|
|
29784
|
+
"toSorted"
|
|
29785
|
+
]);
|
|
29786
|
+
const executesDuringRender = (functionNode) => {
|
|
29787
|
+
const parent = functionNode.parent;
|
|
29788
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29789
|
+
if (parent.callee === functionNode) return true;
|
|
29790
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29791
|
+
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;
|
|
29792
|
+
};
|
|
29793
|
+
//#endregion
|
|
29794
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29795
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29796
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29797
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29798
|
+
return false;
|
|
29799
|
+
};
|
|
29800
|
+
//#endregion
|
|
29801
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29802
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29803
|
+
let ancestor = bindingIdentifier.parent;
|
|
29804
|
+
while (ancestor) {
|
|
29805
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29806
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29807
|
+
ancestor = ancestor.parent ?? null;
|
|
29808
|
+
}
|
|
29809
|
+
return null;
|
|
29810
|
+
};
|
|
29811
|
+
//#endregion
|
|
29812
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29813
|
+
const isFalsyLiteral = (node) => {
|
|
29814
|
+
if (!node) return true;
|
|
29815
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29816
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29817
|
+
};
|
|
29818
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29819
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29820
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29821
|
+
if (!binding) return false;
|
|
29822
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29823
|
+
if (!declarator?.init) return false;
|
|
29824
|
+
const init = stripParenExpression(declarator.init);
|
|
29825
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29826
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29827
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29828
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29829
|
+
};
|
|
29830
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29831
|
+
//#endregion
|
|
29832
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29833
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29834
|
+
let cursor = node;
|
|
29835
|
+
let parent = node.parent;
|
|
29836
|
+
while (parent) {
|
|
29837
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29838
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29839
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29840
|
+
cursor = parent;
|
|
29841
|
+
parent = parent.parent ?? null;
|
|
29842
|
+
}
|
|
29843
|
+
return false;
|
|
29844
|
+
};
|
|
29845
|
+
//#endregion
|
|
29846
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29847
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29848
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29849
|
+
const unwrapped = stripParenExpression(expression);
|
|
29850
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29851
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29852
|
+
const property = unwrapped.property;
|
|
29853
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29854
|
+
}
|
|
29855
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29856
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29857
|
+
return false;
|
|
29858
|
+
};
|
|
29859
|
+
//#endregion
|
|
29860
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29861
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29862
|
+
let cursor = node;
|
|
29863
|
+
let parent = node.parent;
|
|
29864
|
+
while (parent) {
|
|
29865
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29866
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29867
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29868
|
+
cursor = parent;
|
|
29869
|
+
parent = parent.parent ?? null;
|
|
29870
|
+
}
|
|
29871
|
+
return false;
|
|
29872
|
+
};
|
|
29873
|
+
//#endregion
|
|
29874
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29875
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29876
|
+
"toLocaleString",
|
|
29877
|
+
"toLocaleDateString",
|
|
29878
|
+
"toLocaleTimeString"
|
|
29879
|
+
]);
|
|
29880
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29881
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29882
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29883
|
+
"format",
|
|
29884
|
+
"formatToParts",
|
|
29885
|
+
"formatRange"
|
|
29886
|
+
]);
|
|
29887
|
+
const isProvableDateExpression = (expression) => {
|
|
29888
|
+
if (!expression) return false;
|
|
29889
|
+
const unwrapped = stripParenExpression(expression);
|
|
29890
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29891
|
+
};
|
|
29892
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29893
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29894
|
+
if (!expression) return false;
|
|
29895
|
+
const unwrapped = stripParenExpression(expression);
|
|
29896
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29897
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29898
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29899
|
+
return false;
|
|
29900
|
+
};
|
|
29901
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29902
|
+
if (!objectExpression) return false;
|
|
29903
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29904
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29905
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29906
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29907
|
+
if (property.computed) continue;
|
|
29908
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29909
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29910
|
+
}
|
|
29911
|
+
return false;
|
|
29912
|
+
};
|
|
29913
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29914
|
+
if (!argument) return false;
|
|
29915
|
+
const unwrapped = stripParenExpression(argument);
|
|
29916
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29917
|
+
return true;
|
|
29918
|
+
};
|
|
29919
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29920
|
+
const localeArgument = call.arguments?.[0];
|
|
29921
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29922
|
+
const optionsArgument = call.arguments?.[1];
|
|
29923
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29924
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29925
|
+
};
|
|
29926
|
+
const matchLocaleMethodCall = (call) => {
|
|
29927
|
+
const callee = call.callee;
|
|
29928
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29929
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29930
|
+
const methodName = callee.property.name;
|
|
29931
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29932
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29933
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29934
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29935
|
+
return {
|
|
29936
|
+
node: call,
|
|
29937
|
+
display: `${methodName}()`
|
|
29938
|
+
};
|
|
29939
|
+
};
|
|
29940
|
+
const getIntlFormatterName = (expression) => {
|
|
29941
|
+
if (!expression) return null;
|
|
29942
|
+
const unwrapped = stripParenExpression(expression);
|
|
29943
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29944
|
+
const callee = unwrapped.callee;
|
|
29945
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29946
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29947
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29948
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29949
|
+
};
|
|
29950
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29951
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29952
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29953
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29954
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29955
|
+
};
|
|
29956
|
+
const matchIntlFormatCall = (call) => {
|
|
29957
|
+
const callee = call.callee;
|
|
29958
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29959
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29960
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29961
|
+
let construction = stripParenExpression(callee.object);
|
|
29962
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29963
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29964
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29965
|
+
}
|
|
29966
|
+
if (!construction) return null;
|
|
29967
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29968
|
+
if (!formatterName) return null;
|
|
29969
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29970
|
+
return {
|
|
29971
|
+
node: call,
|
|
29972
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29973
|
+
};
|
|
29974
|
+
};
|
|
29975
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29976
|
+
if (!expression) return false;
|
|
29977
|
+
const unwrapped = stripParenExpression(expression);
|
|
29978
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29979
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29980
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29981
|
+
};
|
|
29982
|
+
const matchDateDefaultStringification = (node) => {
|
|
29983
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29984
|
+
const callee = node.callee;
|
|
29985
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29986
|
+
node,
|
|
29987
|
+
display: "Date.prototype.toString()"
|
|
29988
|
+
};
|
|
29989
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29990
|
+
node,
|
|
29991
|
+
display: "String(new Date(…))"
|
|
29992
|
+
};
|
|
29993
|
+
return null;
|
|
29994
|
+
}
|
|
29995
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29996
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29997
|
+
node: expression,
|
|
29998
|
+
display: "`${new Date(…)}`"
|
|
29999
|
+
};
|
|
30000
|
+
}
|
|
30001
|
+
return null;
|
|
30002
|
+
};
|
|
30003
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
30004
|
+
let functionNode = findEnclosingFunction(node);
|
|
30005
|
+
while (functionNode) {
|
|
30006
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30007
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
30008
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
30009
|
+
}
|
|
30010
|
+
return null;
|
|
30011
|
+
};
|
|
30012
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30013
|
+
if (fileHasUseClientDirective) return true;
|
|
30014
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30015
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30016
|
+
let callsHook = false;
|
|
30017
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30018
|
+
if (callsHook) return false;
|
|
30019
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30020
|
+
callsHook = true;
|
|
30021
|
+
return false;
|
|
30022
|
+
}
|
|
30023
|
+
});
|
|
30024
|
+
return callsHook;
|
|
30025
|
+
};
|
|
30026
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30027
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30028
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30029
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30030
|
+
let cursor = node;
|
|
30031
|
+
while (cursor) {
|
|
30032
|
+
ancestors.add(cursor);
|
|
30033
|
+
cursor = cursor.parent ?? null;
|
|
30034
|
+
}
|
|
30035
|
+
for (const statement of body.body ?? []) {
|
|
30036
|
+
if (ancestors.has(statement)) return false;
|
|
30037
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30038
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30039
|
+
let returnsEarly = false;
|
|
30040
|
+
walkAst(statement.consequent, (child) => {
|
|
30041
|
+
if (isFunctionLike$1(child)) return false;
|
|
30042
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30043
|
+
returnsEarly = true;
|
|
30044
|
+
return false;
|
|
30045
|
+
}
|
|
30046
|
+
});
|
|
30047
|
+
if (returnsEarly) return true;
|
|
30048
|
+
}
|
|
30049
|
+
return false;
|
|
30050
|
+
};
|
|
30051
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30052
|
+
let cursor = node.parent;
|
|
30053
|
+
while (cursor) {
|
|
30054
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30055
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30056
|
+
cursor = cursor.parent ?? null;
|
|
30057
|
+
}
|
|
30058
|
+
return null;
|
|
30059
|
+
};
|
|
30060
|
+
const noLocaleFormatInRender = defineRule({
|
|
30061
|
+
id: "no-locale-format-in-render",
|
|
30062
|
+
title: "Locale/timezone formatting during render",
|
|
30063
|
+
severity: "warn",
|
|
30064
|
+
category: "Correctness",
|
|
30065
|
+
disabledWhen: [
|
|
30066
|
+
"vite",
|
|
30067
|
+
"cra",
|
|
30068
|
+
"expo",
|
|
30069
|
+
"react-native",
|
|
30070
|
+
"unknown"
|
|
30071
|
+
],
|
|
30072
|
+
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.",
|
|
30073
|
+
create: (context) => {
|
|
30074
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30075
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30076
|
+
let fileHasUseClientDirective = false;
|
|
30077
|
+
let fileIsEmailTemplate = false;
|
|
30078
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30079
|
+
const reportIfRenderPhase = (match) => {
|
|
30080
|
+
if (reportedNodes.has(match.node)) return;
|
|
30081
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30082
|
+
if (!componentOrHookNode) return;
|
|
30083
|
+
if (fileIsEmailTemplate) return;
|
|
30084
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30085
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30086
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30087
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30088
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30089
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30090
|
+
reportedNodes.add(match.node);
|
|
30091
|
+
context.report({
|
|
30092
|
+
node: match.node,
|
|
30093
|
+
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.`
|
|
30094
|
+
});
|
|
30095
|
+
};
|
|
30096
|
+
return {
|
|
30097
|
+
Program(node) {
|
|
30098
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30099
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30100
|
+
},
|
|
30101
|
+
CallExpression(node) {
|
|
30102
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30103
|
+
if (match) reportIfRenderPhase(match);
|
|
30104
|
+
},
|
|
30105
|
+
TemplateLiteral(node) {
|
|
30106
|
+
const match = matchDateDefaultStringification(node);
|
|
30107
|
+
if (match) reportIfRenderPhase(match);
|
|
30108
|
+
},
|
|
30109
|
+
JSXExpressionContainer(node) {
|
|
30110
|
+
const expression = stripParenExpression(node.expression);
|
|
30111
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30112
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30113
|
+
const helperName = expression.callee.name;
|
|
30114
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30115
|
+
if (!componentOrHookNode) return;
|
|
30116
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30117
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30118
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30119
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30120
|
+
if (isFunctionLike$1(child)) return false;
|
|
30121
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30122
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30123
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30124
|
+
if (fileIsEmailTemplate) return;
|
|
30125
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30126
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30127
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30128
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30129
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30130
|
+
reportedNodes.add(match.node);
|
|
30131
|
+
context.report({
|
|
30132
|
+
node: match.node,
|
|
30133
|
+
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.`
|
|
30134
|
+
});
|
|
30135
|
+
});
|
|
30136
|
+
}
|
|
30137
|
+
};
|
|
30138
|
+
}
|
|
30139
|
+
});
|
|
30140
|
+
//#endregion
|
|
29302
30141
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29303
30142
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29304
30143
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30112,7 +30951,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30112
30951
|
"reverse",
|
|
30113
30952
|
"sort"
|
|
30114
30953
|
]);
|
|
30115
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30116
30954
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30117
30955
|
"assign",
|
|
30118
30956
|
"defineProperties",
|
|
@@ -30186,7 +31024,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30186
31024
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30187
31025
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30188
31026
|
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
31027
|
}
|
|
30191
31028
|
}
|
|
30192
31029
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30225,6 +31062,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30225
31062
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30226
31063
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30227
31064
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
31065
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30228
31066
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30229
31067
|
});
|
|
30230
31068
|
return mutations;
|
|
@@ -30457,7 +31295,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30457
31295
|
id: "no-nested-component-definition",
|
|
30458
31296
|
title: "Component defined inside another component",
|
|
30459
31297
|
tags: ["test-noise", "react-jsx-only"],
|
|
30460
|
-
severity: "
|
|
31298
|
+
severity: "warn",
|
|
30461
31299
|
category: "Correctness",
|
|
30462
31300
|
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
31301
|
create: (context) => {
|
|
@@ -31412,12 +32250,12 @@ const noPassDataToParent = defineRule({
|
|
|
31412
32250
|
if (calleeNode === identifier) {
|
|
31413
32251
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31414
32252
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31415
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32253
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31416
32254
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31417
32255
|
if (isCustomHookParameter(ref)) continue;
|
|
31418
32256
|
} else continue;
|
|
31419
32257
|
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));
|
|
32258
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31421
32259
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31422
32260
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31423
32261
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31608,7 +32446,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31608
32446
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31609
32447
|
const calleeNode = callExpr.callee;
|
|
31610
32448
|
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));
|
|
32449
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31612
32450
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31613
32451
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31614
32452
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -31936,40 +32774,6 @@ const noPreventDefault = defineRule({
|
|
|
31936
32774
|
}
|
|
31937
32775
|
});
|
|
31938
32776
|
//#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
32777
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
31974
32778
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
31975
32779
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32176,7 +32980,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32176
32980
|
return `${callee.name}()`;
|
|
32177
32981
|
}
|
|
32178
32982
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32179
|
-
const receiver = callee.object;
|
|
32983
|
+
const receiver = stripParenExpression(callee.object);
|
|
32180
32984
|
const property = callee.property;
|
|
32181
32985
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32182
32986
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32297,8 +33101,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32297
33101
|
if (typeof sourceValue !== "string") return;
|
|
32298
33102
|
if (handleExtraSource?.(node, context)) return;
|
|
32299
33103
|
if (sourceValue !== source) return;
|
|
33104
|
+
if (isTypeOnlyImport(node)) return;
|
|
32300
33105
|
for (const specifier of node.specifiers ?? []) {
|
|
32301
33106
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33107
|
+
if (specifier.importKind === "type") continue;
|
|
32302
33108
|
const importedName = getImportedName$1(specifier);
|
|
32303
33109
|
if (!importedName) continue;
|
|
32304
33110
|
const message = messages.get(importedName);
|
|
@@ -32317,8 +33123,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32317
33123
|
MemberExpression(node) {
|
|
32318
33124
|
if (namespaceBindings.size === 0) return;
|
|
32319
33125
|
if (node.computed) return;
|
|
32320
|
-
|
|
32321
|
-
if (!
|
|
33126
|
+
const receiver = stripParenExpression(node.object);
|
|
33127
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33128
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32322
33129
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32323
33130
|
const message = messages.get(node.property.name);
|
|
32324
33131
|
if (message) context.report({
|
|
@@ -32383,7 +33190,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32383
33190
|
});
|
|
32384
33191
|
//#endregion
|
|
32385
33192
|
//#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."]
|
|
33193
|
+
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
33194
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32388
33195
|
if (!rawFilename) return false;
|
|
32389
33196
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32421,7 +33228,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32421
33228
|
requires: ["react:19"],
|
|
32422
33229
|
tags: ["test-noise", "migration-hint"],
|
|
32423
33230
|
severity: "warn",
|
|
32424
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33231
|
+
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
33232
|
create: (context) => {
|
|
32426
33233
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32427
33234
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32745,34 +33552,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32745
33552
|
});
|
|
32746
33553
|
//#endregion
|
|
32747
33554
|
//#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
33555
|
const isInsideComponentContext = (node) => {
|
|
32772
33556
|
let cursor = node.parent;
|
|
32773
33557
|
while (cursor) {
|
|
32774
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32775
33558
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33559
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32776
33560
|
cursor = cursor.parent ?? null;
|
|
32777
33561
|
}
|
|
32778
33562
|
return false;
|
|
@@ -32782,24 +33566,28 @@ const functionBodyOf = (node) => {
|
|
|
32782
33566
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32783
33567
|
return null;
|
|
32784
33568
|
};
|
|
33569
|
+
const isHookCallee = (callee) => {
|
|
33570
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33571
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33572
|
+
return false;
|
|
33573
|
+
};
|
|
32785
33574
|
const containsHookCall = (body) => {
|
|
32786
33575
|
let found = false;
|
|
32787
33576
|
walkAst(body, (child) => {
|
|
32788
|
-
if (found) return;
|
|
33577
|
+
if (found) return false;
|
|
33578
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32789
33579
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32790
|
-
|
|
32791
|
-
if (name && isReactHookName(name)) found = true;
|
|
33580
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32792
33581
|
});
|
|
32793
33582
|
return found;
|
|
32794
33583
|
};
|
|
32795
|
-
const
|
|
33584
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32796
33585
|
if (!symbol) return false;
|
|
32797
33586
|
const declaration = symbol.declarationNode;
|
|
32798
33587
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32799
33588
|
const body = functionBodyOf(declaration);
|
|
32800
33589
|
if (!body) return false;
|
|
32801
|
-
|
|
32802
|
-
return !containsHookCall(body);
|
|
33590
|
+
return containsHookCall(body);
|
|
32803
33591
|
};
|
|
32804
33592
|
const noRenderInRender = defineRule({
|
|
32805
33593
|
id: "no-render-in-render",
|
|
@@ -32808,20 +33596,13 @@ const noRenderInRender = defineRule({
|
|
|
32808
33596
|
tags: ["test-noise"],
|
|
32809
33597
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32810
33598
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32811
|
-
const expression = node.expression;
|
|
33599
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32812
33600
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33601
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33602
|
+
const calleeName = expression.callee.name;
|
|
33603
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32817
33604
|
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
|
-
}
|
|
33605
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32825
33606
|
context.report({
|
|
32826
33607
|
node: expression,
|
|
32827
33608
|
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 +33663,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32882
33663
|
//#endregion
|
|
32883
33664
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32884
33665
|
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
|
-
};
|
|
33666
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32892
33667
|
const isUsedAsReturnValue = (parent) => {
|
|
32893
33668
|
if (!parent) return false;
|
|
32894
33669
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33724,7 +34499,7 @@ const noSetState = defineRule({
|
|
|
33724
34499
|
category: "Architecture",
|
|
33725
34500
|
create: (context) => ({ CallExpression(node) {
|
|
33726
34501
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33727
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34502
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33728
34503
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33729
34504
|
if (!getParentComponent(node)) return;
|
|
33730
34505
|
context.report({
|
|
@@ -33890,6 +34665,204 @@ const noSideTabBorder = defineRule({
|
|
|
33890
34665
|
})
|
|
33891
34666
|
});
|
|
33892
34667
|
//#endregion
|
|
34668
|
+
//#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
|
|
34669
|
+
const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
|
|
34670
|
+
"window",
|
|
34671
|
+
"globalThis",
|
|
34672
|
+
"self"
|
|
34673
|
+
]);
|
|
34674
|
+
const NULLISH_COMPARISON_OPERATORS = new Set([
|
|
34675
|
+
"==",
|
|
34676
|
+
"===",
|
|
34677
|
+
"!=",
|
|
34678
|
+
"!=="
|
|
34679
|
+
]);
|
|
34680
|
+
const getGlobalTimerCalleeName = (node) => {
|
|
34681
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34682
|
+
const callee = stripParenExpression(node.callee);
|
|
34683
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
34684
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
34685
|
+
const receiver = stripParenExpression(callee.object);
|
|
34686
|
+
if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
|
|
34687
|
+
}
|
|
34688
|
+
return null;
|
|
34689
|
+
};
|
|
34690
|
+
const getRefCurrentReceiverName = (node) => {
|
|
34691
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
|
|
34692
|
+
if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
34693
|
+
const receiver = stripParenExpression(node.object);
|
|
34694
|
+
return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
34695
|
+
};
|
|
34696
|
+
const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
|
|
34697
|
+
const parseClearCallOnTimerRef = (node) => {
|
|
34698
|
+
const clearCalleeName = getGlobalTimerCalleeName(node);
|
|
34699
|
+
if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
|
|
34700
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34701
|
+
const clearedArgument = node.arguments?.[0];
|
|
34702
|
+
if (!clearedArgument) return null;
|
|
34703
|
+
const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
|
|
34704
|
+
return refName === null ? null : {
|
|
34705
|
+
clearCalleeName,
|
|
34706
|
+
refName
|
|
34707
|
+
};
|
|
34708
|
+
};
|
|
34709
|
+
const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
|
|
34710
|
+
const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
|
|
34711
|
+
const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
|
|
34712
|
+
const isClearGuardIfIdiom = (ifStatement, refName) => {
|
|
34713
|
+
if (ifStatement.alternate) return false;
|
|
34714
|
+
const consequent = ifStatement.consequent;
|
|
34715
|
+
return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
|
|
34716
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
34717
|
+
const expression = stripParenExpression(statement.expression);
|
|
34718
|
+
if (isClearCallOnRef(expression, refName)) return true;
|
|
34719
|
+
return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
|
|
34720
|
+
});
|
|
34721
|
+
};
|
|
34722
|
+
const climbBooleanProjection = (refCurrentMember) => {
|
|
34723
|
+
let cursor = refCurrentMember;
|
|
34724
|
+
const logicalAncestors = [];
|
|
34725
|
+
let didPassBooleanProjection = false;
|
|
34726
|
+
while (cursor.parent) {
|
|
34727
|
+
const parent = cursor.parent;
|
|
34728
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
|
|
34729
|
+
cursor = parent;
|
|
34730
|
+
continue;
|
|
34731
|
+
}
|
|
34732
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
|
|
34733
|
+
didPassBooleanProjection = true;
|
|
34734
|
+
cursor = parent;
|
|
34735
|
+
continue;
|
|
34736
|
+
}
|
|
34737
|
+
if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
|
|
34738
|
+
didPassBooleanProjection = true;
|
|
34739
|
+
cursor = parent;
|
|
34740
|
+
continue;
|
|
34741
|
+
}
|
|
34742
|
+
if (isNodeOfType(parent, "LogicalExpression")) {
|
|
34743
|
+
logicalAncestors.push(parent);
|
|
34744
|
+
cursor = parent;
|
|
34745
|
+
continue;
|
|
34746
|
+
}
|
|
34747
|
+
break;
|
|
34748
|
+
}
|
|
34749
|
+
return {
|
|
34750
|
+
conditionRoot: cursor,
|
|
34751
|
+
logicalAncestors,
|
|
34752
|
+
didPassBooleanProjection
|
|
34753
|
+
};
|
|
34754
|
+
};
|
|
34755
|
+
const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
|
|
34756
|
+
const isPendingSignalRead = (refCurrentMember, refName) => {
|
|
34757
|
+
const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
|
|
34758
|
+
const conditionParent = conditionRoot.parent;
|
|
34759
|
+
if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
|
|
34760
|
+
if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
|
|
34761
|
+
if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
|
|
34762
|
+
return didPassBooleanProjection;
|
|
34763
|
+
};
|
|
34764
|
+
const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
34765
|
+
const facts = {
|
|
34766
|
+
holdsScheduledTimerId: false,
|
|
34767
|
+
hasPendingSignalRead: false
|
|
34768
|
+
};
|
|
34769
|
+
walkAst(ownerScope, (child) => {
|
|
34770
|
+
if (isAssignmentToRefCurrent(child, refName)) {
|
|
34771
|
+
const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
|
|
34772
|
+
if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
|
|
34773
|
+
return;
|
|
34774
|
+
}
|
|
34775
|
+
if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
|
|
34776
|
+
});
|
|
34777
|
+
return facts;
|
|
34778
|
+
};
|
|
34779
|
+
const isEffectCallbackFunction = (functionNode) => {
|
|
34780
|
+
const parent = functionNode.parent;
|
|
34781
|
+
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
34782
|
+
return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
|
|
34783
|
+
};
|
|
34784
|
+
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
34785
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34786
|
+
let isReturnedByName = false;
|
|
34787
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
34788
|
+
if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
34789
|
+
const returned = stripParenExpression(child.argument);
|
|
34790
|
+
if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
|
|
34791
|
+
});
|
|
34792
|
+
return isReturnedByName;
|
|
34793
|
+
};
|
|
34794
|
+
const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
34795
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34796
|
+
if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
|
|
34797
|
+
if (effectCallback.body === functionNode) return true;
|
|
34798
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34799
|
+
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
34800
|
+
};
|
|
34801
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
34802
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34803
|
+
if (cleanupBindingName === null) return false;
|
|
34804
|
+
let isReturnedFromEffect = false;
|
|
34805
|
+
walkAst(ownerScope, (child) => {
|
|
34806
|
+
if (isReturnedFromEffect) return false;
|
|
34807
|
+
if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
34808
|
+
const effectCallback = getEffectCallback(child);
|
|
34809
|
+
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
34810
|
+
});
|
|
34811
|
+
return isReturnedFromEffect;
|
|
34812
|
+
};
|
|
34813
|
+
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
34814
|
+
let functionNode = findEnclosingFunction(node);
|
|
34815
|
+
while (functionNode) {
|
|
34816
|
+
const outerFunction = findEnclosingFunction(functionNode);
|
|
34817
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
34818
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
34819
|
+
functionNode = outerFunction;
|
|
34820
|
+
}
|
|
34821
|
+
return false;
|
|
34822
|
+
};
|
|
34823
|
+
const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
|
|
34824
|
+
const enclosingFunction = findEnclosingFunction(clearCall);
|
|
34825
|
+
if (!isFunctionLike$1(enclosingFunction)) return false;
|
|
34826
|
+
if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
34827
|
+
const clearStart = getRangeStart(clearCall);
|
|
34828
|
+
if (clearStart === null) return false;
|
|
34829
|
+
let didFindLaterReassignment = false;
|
|
34830
|
+
walkInsideStatementBlocks(enclosingFunction.body, (child) => {
|
|
34831
|
+
if (didFindLaterReassignment) return;
|
|
34832
|
+
if (!isAssignmentToRefCurrent(child, refName)) return;
|
|
34833
|
+
const assignmentStart = getRangeStart(child);
|
|
34834
|
+
if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
|
|
34835
|
+
});
|
|
34836
|
+
return didFindLaterReassignment;
|
|
34837
|
+
};
|
|
34838
|
+
const isShadowedTimerGlobal = (clearCall) => {
|
|
34839
|
+
const callee = stripParenExpression(clearCall.callee);
|
|
34840
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34841
|
+
return findVariableInitializer(callee, callee.name) !== null;
|
|
34842
|
+
};
|
|
34843
|
+
const noStaleTimerRef = defineRule({
|
|
34844
|
+
id: "no-stale-timer-ref",
|
|
34845
|
+
title: "Cleared timer ref keeps the stale id",
|
|
34846
|
+
severity: "warn",
|
|
34847
|
+
recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
|
|
34848
|
+
create: (context) => ({ CallExpression(node) {
|
|
34849
|
+
const clearCall = parseClearCallOnTimerRef(node);
|
|
34850
|
+
if (!clearCall) return;
|
|
34851
|
+
if (isShadowedTimerGlobal(node)) return;
|
|
34852
|
+
const { clearCalleeName, refName } = clearCall;
|
|
34853
|
+
const refBinding = findVariableInitializer(node, refName);
|
|
34854
|
+
if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
|
|
34855
|
+
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
34856
|
+
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
34857
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
34858
|
+
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
34859
|
+
context.report({
|
|
34860
|
+
node,
|
|
34861
|
+
message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
|
|
34862
|
+
});
|
|
34863
|
+
} })
|
|
34864
|
+
});
|
|
34865
|
+
//#endregion
|
|
33893
34866
|
//#region src/plugin/utils/is-abstract-role.ts
|
|
33894
34867
|
const isAbstractRole = (openingElement, settings) => {
|
|
33895
34868
|
const elementType = getElementType(openingElement, settings);
|
|
@@ -36170,6 +37143,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36170
37143
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36171
37144
|
return true;
|
|
36172
37145
|
};
|
|
37146
|
+
const isTrivialContainerLiteral = (node) => {
|
|
37147
|
+
if (!node) return false;
|
|
37148
|
+
const innerExpression = stripParenExpression(node);
|
|
37149
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
37150
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
37151
|
+
return false;
|
|
37152
|
+
};
|
|
37153
|
+
const isNonEscapingRead = (identifier) => {
|
|
37154
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
37155
|
+
const memberNode = readRoot.parent;
|
|
37156
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
37157
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
37158
|
+
const memberUseParent = memberUse.parent;
|
|
37159
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
37160
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
37161
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
37162
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
37163
|
+
};
|
|
37164
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
37165
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
37166
|
+
const parentNode = memoUsageRoot.parent;
|
|
37167
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
37168
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
37169
|
+
const bindingTarget = parentNode.id;
|
|
37170
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
37171
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
37172
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
37173
|
+
if (!symbol) return false;
|
|
37174
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
37175
|
+
};
|
|
36173
37176
|
const noUsememoSimpleExpression = defineRule({
|
|
36174
37177
|
id: "no-usememo-simple-expression",
|
|
36175
37178
|
title: "useMemo on a cheap value",
|
|
@@ -36191,9 +37194,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36191
37194
|
let returnExpression = null;
|
|
36192
37195
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36193
37196
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36194
|
-
if (returnExpression
|
|
37197
|
+
if (!returnExpression) return;
|
|
37198
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
37199
|
+
context.report({
|
|
37200
|
+
node,
|
|
37201
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37202
|
+
});
|
|
37203
|
+
return;
|
|
37204
|
+
}
|
|
37205
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36195
37206
|
node,
|
|
36196
|
-
message: "This
|
|
37207
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36197
37208
|
});
|
|
36198
37209
|
} })
|
|
36199
37210
|
});
|
|
@@ -36299,7 +37310,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36299
37310
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36300
37311
|
return { CallExpression(node) {
|
|
36301
37312
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36302
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37313
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36303
37314
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36304
37315
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36305
37316
|
context.report({
|
|
@@ -36575,8 +37586,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
37586
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
37587
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
37588
|
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.";
|
|
37589
|
+
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
37590
|
const DEFAULT_REACT_HOCS = [
|
|
36581
37591
|
"memo",
|
|
36582
37592
|
"forwardRef",
|
|
@@ -36617,6 +37627,10 @@ const isRouteFactoryCall = (expression) => {
|
|
|
36617
37627
|
}
|
|
36618
37628
|
return false;
|
|
36619
37629
|
};
|
|
37630
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
|
|
37631
|
+
const expression = skipTsExpression(argument);
|
|
37632
|
+
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37633
|
+
});
|
|
36620
37634
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
36621
37635
|
const isHocCallee = (callee, state) => {
|
|
36622
37636
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -36647,6 +37661,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
37661
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
37662
|
return false;
|
|
36649
37663
|
};
|
|
37664
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
37665
|
+
for (const property of objectExpression.properties ?? []) {
|
|
37666
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37667
|
+
const value = skipTsExpression(property.value);
|
|
37668
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
37669
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
37670
|
+
continue;
|
|
37671
|
+
}
|
|
37672
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
37673
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
37674
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
37675
|
+
}
|
|
37676
|
+
return false;
|
|
37677
|
+
};
|
|
36650
37678
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
37679
|
if (initializer) {
|
|
36652
37680
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +37705,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
37705
|
reportNode
|
|
36678
37706
|
};
|
|
36679
37707
|
}
|
|
37708
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
37709
|
+
kind: "namespace-object",
|
|
37710
|
+
reportNode
|
|
37711
|
+
};
|
|
36680
37712
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
37713
|
kind: "non-component",
|
|
36682
37714
|
reportNode
|
|
@@ -36693,7 +37725,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
37725
|
walkAst(programRoot, (child) => {
|
|
36694
37726
|
const childType = child.type;
|
|
36695
37727
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
37728
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
37729
|
});
|
|
36698
37730
|
return {
|
|
36699
37731
|
exportNodes,
|
|
@@ -36738,18 +37770,35 @@ const onlyExportComponents = defineRule({
|
|
|
36738
37770
|
category: "Architecture",
|
|
36739
37771
|
create: (context) => {
|
|
36740
37772
|
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
37773
|
return { Program(node) {
|
|
36747
37774
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
37775
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
37776
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
37777
|
+
const state = {
|
|
37778
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37779
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
37780
|
+
allowConstantExport: settings.allowConstantExport,
|
|
37781
|
+
localComponentNames,
|
|
37782
|
+
scopes: context.scopes
|
|
37783
|
+
};
|
|
37784
|
+
for (const child of componentCandidates) {
|
|
37785
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37786
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
37787
|
+
}
|
|
37788
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37789
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37790
|
+
}
|
|
37791
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37792
|
+
const initializer = child.init;
|
|
37793
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37794
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37795
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37796
|
+
}
|
|
37797
|
+
}
|
|
37798
|
+
}
|
|
36749
37799
|
const exports = [];
|
|
36750
37800
|
let hasReactExport = false;
|
|
36751
37801
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
37802
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
37803
|
for (const child of exportNodes) {
|
|
36755
37804
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36814,13 +37863,24 @@ const onlyExportComponents = defineRule({
|
|
|
36814
37863
|
return false;
|
|
36815
37864
|
})();
|
|
36816
37865
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37866
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37867
|
+
kind: "non-component",
|
|
37868
|
+
reportNode: stripped
|
|
37869
|
+
});
|
|
36817
37870
|
else context.report({
|
|
36818
37871
|
node: stripped,
|
|
36819
37872
|
message: ANONYMOUS_MESSAGE
|
|
36820
37873
|
});
|
|
36821
37874
|
continue;
|
|
36822
37875
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
37876
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
37877
|
+
context.report({
|
|
37878
|
+
node: stripped,
|
|
37879
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
37880
|
+
});
|
|
37881
|
+
continue;
|
|
37882
|
+
}
|
|
37883
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
37884
|
context.report({
|
|
36825
37885
|
node: stripped,
|
|
36826
37886
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +37933,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
37933
|
let entry;
|
|
36874
37934
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
37935
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
37936
|
+
else {
|
|
37937
|
+
entry = {
|
|
37938
|
+
kind: "non-component",
|
|
37939
|
+
reportNode
|
|
37940
|
+
};
|
|
37941
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37942
|
+
}
|
|
36880
37943
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
37944
|
exports.push(entry);
|
|
36882
37945
|
}
|
|
36883
37946
|
}
|
|
36884
37947
|
}
|
|
36885
37948
|
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
|
-
}
|
|
37949
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
37950
|
+
node: entry.reportNode,
|
|
37951
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
37952
|
+
});
|
|
36902
37953
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
37954
|
if (entry.kind === "non-component") context.report({
|
|
36904
37955
|
node: entry.reportNode,
|
|
@@ -36909,14 +37960,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37960
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37961
|
});
|
|
36911
37962
|
}
|
|
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
37963
|
} };
|
|
36921
37964
|
}
|
|
36922
37965
|
});
|
|
@@ -37714,8 +38757,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
38757
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
38758
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
38759
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
38760
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
38761
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
38762
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
38763
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
38764
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -38419,11 +39462,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38419
39462
|
}
|
|
38420
39463
|
return null;
|
|
38421
39464
|
};
|
|
39465
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39466
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39467
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39468
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39469
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39470
|
+
return false;
|
|
39471
|
+
};
|
|
38422
39472
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38423
39473
|
const handlerStatements = getCallbackStatements(handler);
|
|
38424
39474
|
if (handlerStatements.length !== 1) return null;
|
|
38425
39475
|
const onlyStatement = handlerStatements[0];
|
|
38426
|
-
|
|
39476
|
+
let expression = onlyStatement;
|
|
39477
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39478
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39479
|
+
if (!expression) return null;
|
|
39480
|
+
expression = stripParenExpression(expression);
|
|
38427
39481
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38428
39482
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38429
39483
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38433,6 +39487,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38433
39487
|
setterArgument: expression.arguments[0]
|
|
38434
39488
|
};
|
|
38435
39489
|
};
|
|
39490
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39491
|
+
if (!init) return false;
|
|
39492
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39493
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39494
|
+
};
|
|
39495
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39496
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39497
|
+
const firstParam = functionNode.params?.[0];
|
|
39498
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39499
|
+
const listenerParamName = firstParam.name;
|
|
39500
|
+
let registersListener = false;
|
|
39501
|
+
walkAst(functionNode.body, (child) => {
|
|
39502
|
+
if (registersListener) return false;
|
|
39503
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39504
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39505
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39506
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39507
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39508
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39509
|
+
const registeredArgument = child.arguments?.[0];
|
|
39510
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39511
|
+
});
|
|
39512
|
+
return registersListener;
|
|
39513
|
+
};
|
|
39514
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39515
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39516
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39517
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39518
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39519
|
+
mutableBindingNames,
|
|
39520
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39521
|
+
};
|
|
39522
|
+
for (const statement of programRoot.body ?? []) {
|
|
39523
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39524
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39525
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39526
|
+
continue;
|
|
39527
|
+
}
|
|
39528
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39529
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39530
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39531
|
+
const init = declarator.init ?? null;
|
|
39532
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39533
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39534
|
+
continue;
|
|
39535
|
+
}
|
|
39536
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39537
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39538
|
+
continue;
|
|
39539
|
+
}
|
|
39540
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39541
|
+
}
|
|
39542
|
+
}
|
|
39543
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39544
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39545
|
+
return {
|
|
39546
|
+
mutableBindingNames,
|
|
39547
|
+
subscribeFunctionNames
|
|
39548
|
+
};
|
|
39549
|
+
};
|
|
39550
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39551
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39552
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39553
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39554
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39555
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39556
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39557
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39558
|
+
return initialArgument.name;
|
|
39559
|
+
};
|
|
39560
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39561
|
+
if (!argument) return false;
|
|
39562
|
+
const unwrapped = stripParenExpression(argument);
|
|
39563
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39564
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39565
|
+
let callsSetter = false;
|
|
39566
|
+
walkAst(unwrapped.body, (child) => {
|
|
39567
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39568
|
+
callsSetter = true;
|
|
39569
|
+
return false;
|
|
39570
|
+
}
|
|
39571
|
+
});
|
|
39572
|
+
return callsSetter;
|
|
39573
|
+
};
|
|
39574
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39575
|
+
let matchedCall = null;
|
|
39576
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39577
|
+
if (matchedCall) return false;
|
|
39578
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39579
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39580
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39581
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39582
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39583
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39584
|
+
matchedCall = child;
|
|
39585
|
+
return false;
|
|
39586
|
+
}
|
|
39587
|
+
});
|
|
39588
|
+
return matchedCall;
|
|
39589
|
+
};
|
|
38436
39590
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38437
39591
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38438
39592
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38449,6 +39603,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38449
39603
|
severity: "warn",
|
|
38450
39604
|
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
39605
|
create: (context) => {
|
|
39606
|
+
let cachedStoreIndex = null;
|
|
39607
|
+
const storeIndexFor = (node) => {
|
|
39608
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39609
|
+
const programRoot = findProgramRoot(node);
|
|
39610
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39611
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39612
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39613
|
+
};
|
|
39614
|
+
return cachedStoreIndex;
|
|
39615
|
+
};
|
|
38452
39616
|
const checkComponent = (componentBody) => {
|
|
38453
39617
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38454
39618
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38486,6 +39650,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38486
39650
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38487
39651
|
if (!useStateInitializer) continue;
|
|
38488
39652
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39653
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38489
39654
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38490
39655
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38491
39656
|
context.report({
|
|
@@ -38493,14 +39658,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38493
39658
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38494
39659
|
});
|
|
38495
39660
|
}
|
|
39661
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39662
|
+
};
|
|
39663
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39664
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39665
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39666
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39667
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39668
|
+
binding,
|
|
39669
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39670
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39671
|
+
if (snapshotBindings.length === 0) return;
|
|
39672
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39673
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39674
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39675
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39676
|
+
const depsNode = effectCall.arguments[1];
|
|
39677
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39678
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39679
|
+
const callback = getEffectCallback(effectCall);
|
|
39680
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39681
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39682
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39683
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39684
|
+
reportedDeclarators.add(binding.declarator);
|
|
39685
|
+
context.report({
|
|
39686
|
+
node: binding.declarator,
|
|
39687
|
+
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.`
|
|
39688
|
+
});
|
|
39689
|
+
}
|
|
39690
|
+
}
|
|
38496
39691
|
};
|
|
38497
39692
|
return {
|
|
38498
39693
|
FunctionDeclaration(node) {
|
|
38499
|
-
|
|
39694
|
+
const functionName = node.id?.name;
|
|
39695
|
+
if (!functionName) return;
|
|
39696
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38500
39697
|
checkComponent(node.body);
|
|
38501
39698
|
},
|
|
38502
39699
|
VariableDeclarator(node) {
|
|
38503
|
-
|
|
39700
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39701
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38504
39702
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38505
39703
|
checkComponent(node.init.body);
|
|
38506
39704
|
}
|
|
@@ -39110,7 +40308,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39110
40308
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39111
40309
|
const body = queryFnValue.body;
|
|
39112
40310
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39113
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40311
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39114
40312
|
node: queryFnProperty,
|
|
39115
40313
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39116
40314
|
});
|
|
@@ -39617,17 +40815,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39617
40815
|
}
|
|
39618
40816
|
});
|
|
39619
40817
|
//#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
40818
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39632
40819
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39633
40820
|
{
|
|
@@ -39636,19 +40823,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39636
40823
|
},
|
|
39637
40824
|
{
|
|
39638
40825
|
display: "Date.now()",
|
|
39639
|
-
matches: (node) =>
|
|
40826
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39640
40827
|
},
|
|
39641
40828
|
{
|
|
39642
40829
|
display: "Math.random()",
|
|
39643
|
-
matches: (node) =>
|
|
40830
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39644
40831
|
},
|
|
39645
40832
|
{
|
|
39646
40833
|
display: "performance.now()",
|
|
39647
|
-
matches: (node) =>
|
|
40834
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39648
40835
|
},
|
|
39649
40836
|
{
|
|
39650
40837
|
display: "crypto.randomUUID()",
|
|
39651
|
-
matches: (node) =>
|
|
40838
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39652
40839
|
}
|
|
39653
40840
|
];
|
|
39654
40841
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39660,54 +40847,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39660
40847
|
}
|
|
39661
40848
|
return null;
|
|
39662
40849
|
};
|
|
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
40850
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39712
40851
|
const member = dateNode.parent;
|
|
39713
40852
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39731,23 +40870,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39731
40870
|
}
|
|
39732
40871
|
return false;
|
|
39733
40872
|
};
|
|
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
40873
|
const renderingHydrationMismatchTime = defineRule({
|
|
39752
40874
|
id: "rendering-hydration-mismatch-time",
|
|
39753
40875
|
title: "Time or random value in JSX",
|
|
@@ -39795,6 +40917,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39795
40917
|
}
|
|
39796
40918
|
});
|
|
39797
40919
|
//#endregion
|
|
40920
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40921
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40922
|
+
"toLocaleString",
|
|
40923
|
+
"toLocaleDateString",
|
|
40924
|
+
"toLocaleTimeString",
|
|
40925
|
+
"getTimezoneOffset"
|
|
40926
|
+
]);
|
|
40927
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40928
|
+
let readsLocaleEnvironment = false;
|
|
40929
|
+
walkAst(expression, (child) => {
|
|
40930
|
+
if (readsLocaleEnvironment) return false;
|
|
40931
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40932
|
+
if (child.object.name === "Intl") {
|
|
40933
|
+
readsLocaleEnvironment = true;
|
|
40934
|
+
return false;
|
|
40935
|
+
}
|
|
40936
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40937
|
+
readsLocaleEnvironment = true;
|
|
40938
|
+
return false;
|
|
40939
|
+
}
|
|
40940
|
+
}
|
|
40941
|
+
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)) {
|
|
40942
|
+
readsLocaleEnvironment = true;
|
|
40943
|
+
return false;
|
|
40944
|
+
}
|
|
40945
|
+
});
|
|
40946
|
+
return readsLocaleEnvironment;
|
|
40947
|
+
};
|
|
40948
|
+
//#endregion
|
|
39798
40949
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39799
40950
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39800
40951
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39860,14 +41011,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39860
41011
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39861
41012
|
const callback = getEffectCallback(node);
|
|
39862
41013
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39863
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39864
|
-
if (
|
|
41014
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
41015
|
+
if (bodyStatements.length !== 1) return;
|
|
39865
41016
|
const soleStatement = bodyStatements[0];
|
|
39866
41017
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39867
41018
|
const expression = soleStatement.expression;
|
|
39868
41019
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39869
41020
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39870
41021
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
41022
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39871
41023
|
context.report({
|
|
39872
41024
|
node,
|
|
39873
41025
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40686,6 +41838,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40686
41838
|
} })
|
|
40687
41839
|
});
|
|
40688
41840
|
//#endregion
|
|
41841
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41842
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41843
|
+
//#endregion
|
|
40689
41844
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40690
41845
|
const rerenderLazyRefInit = defineRule({
|
|
40691
41846
|
id: "rerender-lazy-ref-init",
|
|
@@ -40696,7 +41851,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40696
41851
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40697
41852
|
create: (context) => ({ CallExpression(node) {
|
|
40698
41853
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40699
|
-
const initializer = node.arguments[0];
|
|
41854
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40700
41855
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40701
41856
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40702
41857
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40704,6 +41859,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
41859
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
41860
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
41861
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41862
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40707
41863
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
41864
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
41865
|
context.report({
|
|
@@ -40736,26 +41892,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
41892
|
"getUTCMilliseconds",
|
|
40737
41893
|
"valueOf"
|
|
40738
41894
|
]);
|
|
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
41895
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
41896
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
41897
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40754
|
-
|
|
40755
|
-
if (isNodeOfType(
|
|
40756
|
-
if (isNodeOfType(
|
|
40757
|
-
if (isNodeOfType(
|
|
40758
|
-
if (isNodeOfType(
|
|
41898
|
+
const innerExpression = stripParenExpression(expression);
|
|
41899
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41900
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41901
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41902
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40759
41903
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40760
41904
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40761
41905
|
if (spreadCall) return spreadCall;
|
|
@@ -40778,7 +41922,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40778
41922
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40779
41923
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40780
41924
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40781
|
-
if (
|
|
41925
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40782
41926
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40783
41927
|
if (isReactHookName(calleeName)) return;
|
|
40784
41928
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41445,7 +42589,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41445
42589
|
const body = reactionFn.body;
|
|
41446
42590
|
let singleAssignment = null;
|
|
41447
42591
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41448
|
-
const statements = body.body ?? [];
|
|
42592
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41449
42593
|
if (statements.length !== 1) return;
|
|
41450
42594
|
const onlyStatement = statements[0];
|
|
41451
42595
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41491,7 +42635,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
42635
|
});
|
|
41492
42636
|
//#endregion
|
|
41493
42637
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
42638
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
42639
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
42640
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
42641
|
"tap",
|
|
@@ -41519,7 +42663,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41519
42663
|
"catch",
|
|
41520
42664
|
"finally"
|
|
41521
42665
|
]);
|
|
41522
|
-
const findChainRoot = (
|
|
42666
|
+
const findChainRoot = (wrappedNode) => {
|
|
42667
|
+
const node = stripParenExpression(wrappedNode);
|
|
41523
42668
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41524
42669
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41525
42670
|
calleeName: node.callee.name,
|
|
@@ -41551,7 +42696,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
42696
|
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
42697
|
create: (context) => {
|
|
41553
42698
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
42699
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
42700
|
return { ExpressionStatement(node) {
|
|
41556
42701
|
const expression = node.expression;
|
|
41557
42702
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42107,20 +43252,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42107
43252
|
};
|
|
42108
43253
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42109
43254
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42110
|
-
|
|
42111
|
-
|
|
43255
|
+
const receiver = stripParenExpression(callee.object);
|
|
43256
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
43257
|
+
const localName = receiver.name;
|
|
42112
43258
|
const binding = findVariableInitializer(node, localName);
|
|
42113
43259
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42114
43260
|
return localName === "Dimensions";
|
|
42115
43261
|
}
|
|
42116
|
-
if (isNodeOfType(
|
|
42117
|
-
if (getInitializerModuleSource(node,
|
|
42118
|
-
const rootName = getRootIdentifierName(
|
|
43262
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
43263
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
43264
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42119
43265
|
if (rootName === null) return false;
|
|
42120
43266
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42121
43267
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42122
43268
|
}
|
|
42123
|
-
if (getRequireCallSource(
|
|
43269
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42124
43270
|
return false;
|
|
42125
43271
|
};
|
|
42126
43272
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42526,7 +43672,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
43672
|
};
|
|
42527
43673
|
//#endregion
|
|
42528
43674
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
43675
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
43676
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
43677
|
const legacyShadowPropertyNames = [];
|
|
42532
43678
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +43695,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
43695
|
severity: "warn",
|
|
42550
43696
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
43697
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
43698
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
43699
|
return {
|
|
42554
43700
|
JSXAttribute(node) {
|
|
42555
43701
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -42564,7 +43710,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42564
43710
|
},
|
|
42565
43711
|
CallExpression(node) {
|
|
42566
43712
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42567
|
-
|
|
43713
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43714
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42568
43715
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42569
43716
|
const stylesArgument = node.arguments?.[0];
|
|
42570
43717
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43414,7 +44561,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43414
44561
|
const callee = node.callee;
|
|
43415
44562
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43416
44563
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43417
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44564
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43418
44565
|
context.report({
|
|
43419
44566
|
node,
|
|
43420
44567
|
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 +44617,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
44617
|
};
|
|
43471
44618
|
//#endregion
|
|
43472
44619
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
44620
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
44621
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
44622
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
44623
|
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 +44651,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
44651
|
severity: "warn",
|
|
43505
44652
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
44653
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
44654
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
44655
|
const flaggedImports = [];
|
|
43509
44656
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
44657
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43642,14 +44789,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43642
44789
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43643
44790
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43644
44791
|
const methodName = callee.property.name;
|
|
43645
|
-
|
|
44792
|
+
const receiver = stripParenExpression(callee.object);
|
|
44793
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43646
44794
|
factoryName: methodName,
|
|
43647
44795
|
chainMethodNames,
|
|
43648
44796
|
numberOfTapsArgument
|
|
43649
44797
|
};
|
|
43650
44798
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43651
44799
|
chainMethodNames.push(methodName);
|
|
43652
|
-
cursor =
|
|
44800
|
+
cursor = receiver;
|
|
43653
44801
|
}
|
|
43654
44802
|
return null;
|
|
43655
44803
|
};
|
|
@@ -43991,7 +45139,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
45139
|
});
|
|
43992
45140
|
//#endregion
|
|
43993
45141
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
45142
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
45143
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
45144
|
"shadowColor",
|
|
43997
45145
|
"shadowOffset",
|
|
@@ -44077,7 +45225,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
45225
|
severity: "warn",
|
|
44078
45226
|
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
45227
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
45228
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
45229
|
return {
|
|
44082
45230
|
JSXAttribute(node) {
|
|
44083
45231
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +45322,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
45322
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
45323
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
45324
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
45325
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
45326
|
+
if (roleCandidates === null) return;
|
|
45327
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
45328
|
for (const role of roles) {
|
|
44181
45329
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
45330
|
if (!required) continue;
|
|
@@ -47293,7 +48441,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
48441
|
};
|
|
47294
48442
|
//#endregion
|
|
47295
48443
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
48444
|
+
const buildMessageDefault = (roles, propName) => {
|
|
48445
|
+
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.`;
|
|
48446
|
+
};
|
|
47297
48447
|
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
48448
|
const roleSupportsAriaProps = defineRule({
|
|
47299
48449
|
id: "role-supports-aria-props",
|
|
@@ -47313,6 +48463,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47313
48463
|
const propName = propRawName.toLowerCase();
|
|
47314
48464
|
if (!propName.startsWith("aria-")) continue;
|
|
47315
48465
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48466
|
+
const attributeValue = attributeNode.value;
|
|
48467
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47316
48468
|
(ariaAttributes ??= []).push({
|
|
47317
48469
|
attribute,
|
|
47318
48470
|
propName
|
|
@@ -47321,17 +48473,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
48473
|
if (!ariaAttributes) return;
|
|
47322
48474
|
const elementType = getElementType(node, context.settings);
|
|
47323
48475
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
48476
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
48477
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
48478
|
+
const supportedSets = [];
|
|
48479
|
+
for (const role of roleCandidates) {
|
|
48480
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48481
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
48482
|
+
if (!supported) return;
|
|
48483
|
+
supportedSets.push(supported);
|
|
48484
|
+
}
|
|
47327
48485
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
48486
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
48487
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
48488
|
context.report({
|
|
47333
48489
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
48490
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
48491
|
});
|
|
47336
48492
|
}
|
|
47337
48493
|
} })
|
|
@@ -47678,17 +48834,6 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
47678
48834
|
}
|
|
47679
48835
|
return false;
|
|
47680
48836
|
};
|
|
47681
|
-
const isUseEffectEventSymbol = (symbol) => {
|
|
47682
|
-
const initializer = symbol.initializer;
|
|
47683
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
|
-
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
|
-
};
|
|
47686
|
-
const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
|
|
47687
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode) => {
|
|
47688
|
-
const initializer = symbol.initializer;
|
|
47689
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47691
|
-
};
|
|
47692
48837
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
48838
|
let current = node.parent;
|
|
47694
48839
|
while (current) {
|
|
@@ -47766,7 +48911,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
48911
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
48912
|
if (!hookContext) return;
|
|
47768
48913
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48914
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
48915
|
context.report({
|
|
47771
48916
|
node: node.callee,
|
|
47772
48917
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47878,8 +49023,7 @@ const rulesOfHooks = defineRule({
|
|
|
47878
49023
|
},
|
|
47879
49024
|
Identifier(node) {
|
|
47880
49025
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
|
-
if (!symbol || !
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
49026
|
+
if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
|
|
47883
49027
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
49028
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
49029
|
context.report({
|
|
@@ -48027,7 +49171,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48027
49171
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48028
49172
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48029
49173
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48030
|
-
const
|
|
49174
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
49175
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48031
49176
|
if (!objectName) return;
|
|
48032
49177
|
const methodName = node.callee.property.name;
|
|
48033
49178
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48703,7 +49848,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48703
49848
|
};
|
|
48704
49849
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48705
49850
|
let chainTip = referenceIdentifier;
|
|
48706
|
-
while (chainTip.parent
|
|
49851
|
+
while (chainTip.parent) {
|
|
49852
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49853
|
+
chainTip = chainTip.parent;
|
|
49854
|
+
continue;
|
|
49855
|
+
}
|
|
49856
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49857
|
+
chainTip = chainTip.parent;
|
|
49858
|
+
continue;
|
|
49859
|
+
}
|
|
49860
|
+
break;
|
|
49861
|
+
}
|
|
48707
49862
|
return chainTip;
|
|
48708
49863
|
};
|
|
48709
49864
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48728,7 +49883,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48728
49883
|
const callee = callExpression.callee;
|
|
48729
49884
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48730
49885
|
const methodName = getMemberPropertyName$1(callee);
|
|
48731
|
-
|
|
49886
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49887
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48732
49888
|
}
|
|
48733
49889
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48734
49890
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49458,7 +50614,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49458
50614
|
};
|
|
49459
50615
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49460
50616
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49461
|
-
let currentNode = outerNode.callee.object;
|
|
50617
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49462
50618
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49463
50619
|
const calleeName = getCalleeName$2(currentNode);
|
|
49464
50620
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49469,7 +50625,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49469
50625
|
}
|
|
49470
50626
|
}
|
|
49471
50627
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49472
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50628
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49473
50629
|
else break;
|
|
49474
50630
|
}
|
|
49475
50631
|
return result;
|
|
@@ -50122,7 +51278,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50122
51278
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50123
51279
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50124
51280
|
if (methodName) methodNames.unshift(methodName);
|
|
50125
|
-
currentNode = currentNode.callee.object;
|
|
51281
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50126
51282
|
}
|
|
50127
51283
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50128
51284
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53204,6 +54360,18 @@ const reactDoctorRules = [
|
|
|
53204
54360
|
category: "Bugs"
|
|
53205
54361
|
}
|
|
53206
54362
|
},
|
|
54363
|
+
{
|
|
54364
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54365
|
+
id: "no-locale-format-in-render",
|
|
54366
|
+
source: "react-doctor",
|
|
54367
|
+
originallyExternal: false,
|
|
54368
|
+
rule: {
|
|
54369
|
+
...noLocaleFormatInRender,
|
|
54370
|
+
framework: "global",
|
|
54371
|
+
category: "Bugs",
|
|
54372
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54373
|
+
}
|
|
54374
|
+
},
|
|
53207
54375
|
{
|
|
53208
54376
|
key: "react-doctor/no-long-transition-duration",
|
|
53209
54377
|
id: "no-long-transition-duration",
|
|
@@ -53632,6 +54800,18 @@ const reactDoctorRules = [
|
|
|
53632
54800
|
category: "Maintainability"
|
|
53633
54801
|
}
|
|
53634
54802
|
},
|
|
54803
|
+
{
|
|
54804
|
+
key: "react-doctor/no-stale-timer-ref",
|
|
54805
|
+
id: "no-stale-timer-ref",
|
|
54806
|
+
source: "react-doctor",
|
|
54807
|
+
originallyExternal: false,
|
|
54808
|
+
rule: {
|
|
54809
|
+
...noStaleTimerRef,
|
|
54810
|
+
framework: "global",
|
|
54811
|
+
category: "Bugs",
|
|
54812
|
+
requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
|
|
54813
|
+
}
|
|
54814
|
+
},
|
|
53635
54815
|
{
|
|
53636
54816
|
key: "react-doctor/no-static-element-interactions",
|
|
53637
54817
|
id: "no-static-element-interactions",
|
|
@@ -55451,6 +56631,34 @@ const reactDoctorRules = [
|
|
|
55451
56631
|
];
|
|
55452
56632
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
56633
|
//#endregion
|
|
56634
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
56635
|
+
const isNextFileActive = (context) => {
|
|
56636
|
+
const rawFilename = context.filename;
|
|
56637
|
+
if (!rawFilename) return true;
|
|
56638
|
+
const filename = normalizeFilename(rawFilename);
|
|
56639
|
+
const manifest = readNearestPackageManifest(filename);
|
|
56640
|
+
if (!manifest) return true;
|
|
56641
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
56642
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
56643
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
56644
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
56645
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
56646
|
+
return true;
|
|
56647
|
+
};
|
|
56648
|
+
//#endregion
|
|
56649
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
56650
|
+
const EMPTY_VISITORS$1 = {};
|
|
56651
|
+
const wrapNextjsRule = (rule) => {
|
|
56652
|
+
const innerCreate = rule.create.bind(rule);
|
|
56653
|
+
return {
|
|
56654
|
+
...rule,
|
|
56655
|
+
create: (context) => {
|
|
56656
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
56657
|
+
return innerCreate(context);
|
|
56658
|
+
}
|
|
56659
|
+
};
|
|
56660
|
+
};
|
|
56661
|
+
//#endregion
|
|
55454
56662
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
56663
|
const EMPTY_VISITORS = {};
|
|
55456
56664
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +57128,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
57128
|
});
|
|
55921
57129
|
//#endregion
|
|
55922
57130
|
//#region src/plugin/react-doctor-plugin.ts
|
|
57131
|
+
const applyFrameworkGate = (rule) => {
|
|
57132
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
57133
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
57134
|
+
return rule;
|
|
57135
|
+
};
|
|
55923
57136
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
57137
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
57138
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
57139
|
return wrapped;
|
|
55927
57140
|
};
|
|
55928
57141
|
const plugin = {
|
|
@@ -56047,6 +57260,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
56047
57260
|
"nextjs-no-use-search-params-without-suspense",
|
|
56048
57261
|
"no-dynamic-import-path",
|
|
56049
57262
|
"no-full-lodash-import",
|
|
57263
|
+
"no-locale-format-in-render",
|
|
56050
57264
|
"no-mutating-reducer-state",
|
|
56051
57265
|
"prefer-dynamic-import",
|
|
56052
57266
|
"rendering-hydration-mismatch-time",
|
|
@@ -56181,6 +57395,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
56181
57395
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
56182
57396
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
56183
57397
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57398
|
+
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
56184
57399
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
56185
57400
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
56186
57401
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|