oxlint-plugin-react-doctor 0.7.2-dev.2953b25 → 0.7.2-dev.43d766f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -92
- package/dist/index.js +549 -1764
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,12 +532,6 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
-
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
-
"WebSocket",
|
|
537
|
-
"EventSource",
|
|
538
|
-
"BroadcastChannel",
|
|
539
|
-
"RTCPeerConnection"
|
|
540
|
-
]);
|
|
541
535
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
542
536
|
"location",
|
|
543
537
|
"window",
|
|
@@ -601,19 +595,6 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
601
595
|
"parseInt",
|
|
602
596
|
"parseFloat"
|
|
603
597
|
]);
|
|
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
|
-
]);
|
|
617
598
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
618
599
|
"Boolean",
|
|
619
600
|
"String",
|
|
@@ -693,10 +674,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
693
674
|
"unwatch",
|
|
694
675
|
"unlisten",
|
|
695
676
|
"unsub",
|
|
696
|
-
"abort"
|
|
697
|
-
"disconnect",
|
|
698
|
-
"unobserve",
|
|
699
|
-
"close"
|
|
677
|
+
"abort"
|
|
700
678
|
]);
|
|
701
679
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
702
680
|
"remove",
|
|
@@ -2257,39 +2235,6 @@ const anchorHasContent = defineRule({
|
|
|
2257
2235
|
}
|
|
2258
2236
|
});
|
|
2259
2237
|
//#endregion
|
|
2260
|
-
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
-
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
-
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
-
const expression = stripParenExpression(rawExpression);
|
|
2264
|
-
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
-
return staticValue === null ? null : [staticValue];
|
|
2268
|
-
}
|
|
2269
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
-
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
-
if (consequentValues === null) return null;
|
|
2272
|
-
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
-
if (alternateValues === null) return null;
|
|
2274
|
-
return [...consequentValues, ...alternateValues];
|
|
2275
|
-
}
|
|
2276
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
-
if (remainingHops === 0) return null;
|
|
2278
|
-
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2281
|
-
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2282
|
-
}
|
|
2283
|
-
return null;
|
|
2284
|
-
};
|
|
2285
|
-
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2286
|
-
const value = attribute.value;
|
|
2287
|
-
if (!value) return null;
|
|
2288
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2289
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2290
|
-
return null;
|
|
2291
|
-
};
|
|
2292
|
-
//#endregion
|
|
2293
2238
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2294
2239
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2295
2240
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2317,14 +2262,30 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2317
2262
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2318
2263
|
const isInvalidHref = (value, validHrefs) => {
|
|
2319
2264
|
if (validHrefs.has(value)) return false;
|
|
2320
|
-
|
|
2321
|
-
|
|
2265
|
+
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
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;
|
|
2322
2275
|
};
|
|
2323
|
-
const
|
|
2276
|
+
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2277
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2324
2278
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2325
2279
|
const expression = value.expression;
|
|
2326
2280
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2327
|
-
if (isNodeOfType(expression, "Literal")
|
|
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
|
+
}
|
|
2328
2289
|
}
|
|
2329
2290
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2330
2291
|
return false;
|
|
@@ -2357,10 +2318,9 @@ const anchorIsValid = defineRule({
|
|
|
2357
2318
|
});
|
|
2358
2319
|
return;
|
|
2359
2320
|
}
|
|
2360
|
-
|
|
2361
|
-
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2321
|
+
if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
|
|
2362
2322
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2363
|
-
if (!hasOnClick &&
|
|
2323
|
+
if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
|
|
2364
2324
|
context.report({
|
|
2365
2325
|
node: node.name,
|
|
2366
2326
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3416,25 +3376,6 @@ const ariaRole = defineRule({
|
|
|
3416
3376
|
if (!roleAttribute) return;
|
|
3417
3377
|
const elementType = getElementType(node, context.settings);
|
|
3418
3378
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3419
|
-
const reportFirstInvalidCandidate = (candidates) => {
|
|
3420
|
-
for (const candidate of candidates) {
|
|
3421
|
-
if (candidate.trim().length === 0) {
|
|
3422
|
-
context.report({
|
|
3423
|
-
node: roleAttribute,
|
|
3424
|
-
message: buildBaseMessage("")
|
|
3425
|
-
});
|
|
3426
|
-
return;
|
|
3427
|
-
}
|
|
3428
|
-
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3429
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3430
|
-
context.report({
|
|
3431
|
-
node: roleAttribute,
|
|
3432
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3433
|
-
});
|
|
3434
|
-
return;
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
};
|
|
3438
3379
|
const value = roleAttribute.value;
|
|
3439
3380
|
if (!value) {
|
|
3440
3381
|
context.report({
|
|
@@ -3451,7 +3392,22 @@ const ariaRole = defineRule({
|
|
|
3451
3392
|
});
|
|
3452
3393
|
return;
|
|
3453
3394
|
}
|
|
3454
|
-
|
|
3395
|
+
const stringValue = value.value;
|
|
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
|
+
}
|
|
3455
3411
|
return;
|
|
3456
3412
|
}
|
|
3457
3413
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3472,11 +3428,6 @@ const ariaRole = defineRule({
|
|
|
3472
3428
|
});
|
|
3473
3429
|
return;
|
|
3474
3430
|
}
|
|
3475
|
-
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3476
|
-
if (resolvedCandidates !== null) {
|
|
3477
|
-
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3478
|
-
return;
|
|
3479
|
-
}
|
|
3480
3431
|
return;
|
|
3481
3432
|
}
|
|
3482
3433
|
context.report({
|
|
@@ -5193,7 +5144,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5193
5144
|
const callee = node.callee;
|
|
5194
5145
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5195
5146
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5196
|
-
if (!isWebStorageObject(
|
|
5147
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
5197
5148
|
const keyArgument = node.arguments?.[0];
|
|
5198
5149
|
if (!keyArgument) return;
|
|
5199
5150
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6133,21 +6084,19 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6133
6084
|
})
|
|
6134
6085
|
});
|
|
6135
6086
|
//#endregion
|
|
6136
|
-
//#region src/plugin/utils/is-global-method-call.ts
|
|
6137
|
-
const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
6138
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6139
|
-
const callee = stripParenExpression(node.callee);
|
|
6140
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
6141
|
-
const receiver = stripParenExpression(callee.object);
|
|
6142
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6143
|
-
};
|
|
6144
|
-
//#endregion
|
|
6145
6087
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6146
6088
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6147
6089
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6148
6090
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6149
6091
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6150
|
-
const isJsonStringifyCall = (node) =>
|
|
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
|
+
};
|
|
6151
6100
|
const resolveStringKey = (keyArg, context) => {
|
|
6152
6101
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6153
6102
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6166,9 +6115,8 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6166
6115
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6167
6116
|
create: (context) => ({ CallExpression(node) {
|
|
6168
6117
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6169
|
-
|
|
6170
|
-
if (!
|
|
6171
|
-
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6118
|
+
if (!isNodeOfType(node.callee.object, "Identifier")) return;
|
|
6119
|
+
if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
|
|
6172
6120
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6173
6121
|
if (node.callee.property.name !== "setItem") return;
|
|
6174
6122
|
const keyArg = node.arguments?.[0];
|
|
@@ -6181,7 +6129,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6181
6129
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6182
6130
|
context.report({
|
|
6183
6131
|
node: keyArg,
|
|
6184
|
-
message: `${
|
|
6132
|
+
message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6185
6133
|
});
|
|
6186
6134
|
} })
|
|
6187
6135
|
});
|
|
@@ -7662,102 +7610,6 @@ const displayName = defineRule({
|
|
|
7662
7610
|
}
|
|
7663
7611
|
});
|
|
7664
7612
|
//#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
|
|
7761
7613
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7762
7614
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7763
7615
|
if (!node || typeof node !== "object") return;
|
|
@@ -7909,17 +7761,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7909
7761
|
};
|
|
7910
7762
|
//#endregion
|
|
7911
7763
|
//#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
|
-
};
|
|
7923
7764
|
const findSubscribeLikeUsages = (callback) => {
|
|
7924
7765
|
const usages = [];
|
|
7925
7766
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7931,14 +7772,6 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7931
7772
|
}
|
|
7932
7773
|
walkAst(callback, (child) => {
|
|
7933
7774
|
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
|
-
}
|
|
7942
7775
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7943
7776
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7944
7777
|
usages.push({
|
|
@@ -7948,7 +7781,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7948
7781
|
});
|
|
7949
7782
|
return;
|
|
7950
7783
|
}
|
|
7951
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") &&
|
|
7784
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7952
7785
|
kind: "subscribe",
|
|
7953
7786
|
node: child,
|
|
7954
7787
|
resourceName: child.callee.property.name
|
|
@@ -7964,13 +7797,6 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7964
7797
|
};
|
|
7965
7798
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7966
7799
|
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
|
-
});
|
|
7974
7800
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7975
7801
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7976
7802
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7978,12 +7804,7 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7978
7804
|
const bindingName = declarator.id.name;
|
|
7979
7805
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7980
7806
|
const init = declarator.init;
|
|
7981
|
-
if (!init) continue;
|
|
7982
|
-
if (isSocketConstruction(init)) {
|
|
7983
|
-
bindings.subscriptionNames.add(bindingName);
|
|
7984
|
-
continue;
|
|
7985
|
-
}
|
|
7986
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7807
|
+
if (!init || !isNodeOfType(init, "CallExpression")) continue;
|
|
7987
7808
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7988
7809
|
bindings.subscriptionNames.add(bindingName);
|
|
7989
7810
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -8009,21 +7830,9 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
8009
7830
|
});
|
|
8010
7831
|
return bindings;
|
|
8011
7832
|
};
|
|
8012
|
-
const
|
|
8013
|
-
|
|
8014
|
-
|
|
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
|
-
});
|
|
7833
|
+
const getRangeStart = (node) => {
|
|
7834
|
+
const rangeStart = node.range?.[0];
|
|
7835
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8027
7836
|
};
|
|
8028
7837
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
8029
7838
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
@@ -8047,177 +7856,26 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
8047
7856
|
});
|
|
8048
7857
|
return didFindCleanupReturn;
|
|
8049
7858
|
};
|
|
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
|
-
};
|
|
8174
7859
|
const effectNeedsCleanup = defineRule({
|
|
8175
7860
|
id: "effect-needs-cleanup",
|
|
8176
7861
|
title: "Effect subscription or timer never cleaned up",
|
|
8177
7862
|
severity: "error",
|
|
8178
7863
|
tags: ["test-noise"],
|
|
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,
|
|
8180
|
-
create: (context) => {
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
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
|
-
}
|
|
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) => ({ CallExpression(node) {
|
|
7866
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
7867
|
+
const callback = getEffectCallback(node);
|
|
7868
|
+
if (!callback) return;
|
|
7869
|
+
const usages = findSubscribeLikeUsages(callback);
|
|
7870
|
+
if (usages.length === 0) return;
|
|
7871
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
7872
|
+
const firstUsage = usages[0];
|
|
7873
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
7874
|
+
context.report({
|
|
7875
|
+
node,
|
|
7876
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
7877
|
+
});
|
|
7878
|
+
} })
|
|
8221
7879
|
});
|
|
8222
7880
|
//#endregion
|
|
8223
7881
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8776,6 +8434,17 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8776
8434
|
return computedCaptures;
|
|
8777
8435
|
};
|
|
8778
8436
|
//#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
|
|
8779
8448
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8780
8449
|
const reactHocCalleeName = (callee) => {
|
|
8781
8450
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8989,8 +8658,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8989
8658
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8990
8659
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8991
8660
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8992
|
-
* `symbolHasStableHookOrigin` / `
|
|
8993
|
-
* multiple sites — extracting
|
|
8661
|
+
* `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
|
|
8662
|
+
* `isRecursiveInitializerCapture` at multiple sites — extracting
|
|
8994
8663
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8995
8664
|
* declared logic.
|
|
8996
8665
|
*
|
|
@@ -9046,6 +8715,11 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
9046
8715
|
}
|
|
9047
8716
|
return false;
|
|
9048
8717
|
};
|
|
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
|
+
};
|
|
9049
8723
|
const getFunctionValueNode = (symbol) => {
|
|
9050
8724
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
9051
8725
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -9131,37 +8805,6 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9131
8805
|
};
|
|
9132
8806
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9133
8807
|
//#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
|
|
9165
8808
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
9166
8809
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
9167
8810
|
"useEffect",
|
|
@@ -9812,7 +9455,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9812
9455
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9813
9456
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9814
9457
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9815
|
-
if (depSymbol &&
|
|
9458
|
+
if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
|
|
9816
9459
|
context.report({
|
|
9817
9460
|
node: elementNode,
|
|
9818
9461
|
message: buildEffectEventDepMessage()
|
|
@@ -9973,7 +9616,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9973
9616
|
});
|
|
9974
9617
|
//#endregion
|
|
9975
9618
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9976
|
-
const EMPTY_VISITORS$
|
|
9619
|
+
const EMPTY_VISITORS$5 = {};
|
|
9977
9620
|
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?$)/;
|
|
9978
9621
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9979
9622
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9985,7 +9628,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9985
9628
|
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.",
|
|
9986
9629
|
create: (context) => {
|
|
9987
9630
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9988
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9631
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
9989
9632
|
return {
|
|
9990
9633
|
MemberExpression(node) {
|
|
9991
9634
|
if (!node.computed) return;
|
|
@@ -10238,10 +9881,7 @@ const PRAGMA = "React";
|
|
|
10238
9881
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
10239
9882
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
10240
9883
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
10241
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10242
|
-
const receiver = stripParenExpression(node.callee.object);
|
|
10243
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10244
|
-
}
|
|
9884
|
+
if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
|
|
10245
9885
|
return true;
|
|
10246
9886
|
};
|
|
10247
9887
|
//#endregion
|
|
@@ -11406,8 +11046,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11406
11046
|
if (node.attributes.length === 0) return;
|
|
11407
11047
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11408
11048
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11409
|
-
const
|
|
11410
|
-
if (
|
|
11049
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
11050
|
+
if (!role) return;
|
|
11411
11051
|
let hasInteractiveHandler = false;
|
|
11412
11052
|
for (const attribute of node.attributes) {
|
|
11413
11053
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11421,16 +11061,11 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11421
11061
|
const elementType = getElementType(node, context.settings);
|
|
11422
11062
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11423
11063
|
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;
|
|
11424
11066
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11425
|
-
|
|
11426
|
-
|
|
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);
|
|
11067
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11068
|
+
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
11434
11069
|
context.report({
|
|
11435
11070
|
node,
|
|
11436
11071
|
message
|
|
@@ -11619,6 +11254,16 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11619
11254
|
return names;
|
|
11620
11255
|
};
|
|
11621
11256
|
//#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
|
|
11622
11267
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11623
11268
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11624
11269
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12318,15 +11963,14 @@ const jsCacheStorage = defineRule({
|
|
|
12318
11963
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12319
11964
|
CallExpression(node) {
|
|
12320
11965
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12321
|
-
|
|
12322
|
-
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11966
|
+
if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
|
|
12323
11967
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12324
11968
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12325
11969
|
const storageKey = String(node.arguments[0].value);
|
|
12326
11970
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12327
11971
|
storageReadCounts.set(storageKey, readCount);
|
|
12328
11972
|
if (readCount === 2) {
|
|
12329
|
-
const storageName =
|
|
11973
|
+
const storageName = node.callee.object.name;
|
|
12330
11974
|
context.report({
|
|
12331
11975
|
node,
|
|
12332
11976
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -12340,16 +11984,13 @@ const jsCacheStorage = defineRule({
|
|
|
12340
11984
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12341
11985
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12342
11986
|
const callee = callExpression.callee;
|
|
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;
|
|
12351
|
-
}
|
|
11987
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12352
11988
|
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
11989
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
11990
|
+
const receiver = callee.object;
|
|
11991
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
11992
|
+
return true;
|
|
11993
|
+
}
|
|
12353
11994
|
return false;
|
|
12354
11995
|
};
|
|
12355
11996
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12361,7 +12002,10 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12361
12002
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12362
12003
|
let cursor = receiverNode;
|
|
12363
12004
|
while (cursor) {
|
|
12364
|
-
|
|
12005
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12006
|
+
cursor = cursor.expression;
|
|
12007
|
+
continue;
|
|
12008
|
+
}
|
|
12365
12009
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12366
12010
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12367
12011
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12437,7 +12081,10 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12437
12081
|
let hops = 0;
|
|
12438
12082
|
while (cursor && hops < 12) {
|
|
12439
12083
|
hops += 1;
|
|
12440
|
-
|
|
12084
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12085
|
+
cursor = cursor.expression;
|
|
12086
|
+
continue;
|
|
12087
|
+
}
|
|
12441
12088
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12442
12089
|
const callee = cursor.callee;
|
|
12443
12090
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12461,7 +12108,10 @@ const isSmallLiteralArray = (node) => {
|
|
|
12461
12108
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12462
12109
|
let cursor = receiverNode;
|
|
12463
12110
|
while (cursor) {
|
|
12464
|
-
|
|
12111
|
+
if (isNodeOfType(cursor, "ChainExpression")) {
|
|
12112
|
+
cursor = cursor.expression;
|
|
12113
|
+
continue;
|
|
12114
|
+
}
|
|
12465
12115
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12466
12116
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12467
12117
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12526,7 +12176,7 @@ const jsCombineIterations = defineRule({
|
|
|
12526
12176
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12527
12177
|
const outerMethod = node.callee.property.name;
|
|
12528
12178
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12529
|
-
const innerCall =
|
|
12179
|
+
const innerCall = node.callee.object;
|
|
12530
12180
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12531
12181
|
const innerMethod = innerCall.callee.property.name;
|
|
12532
12182
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12599,10 +12249,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12599
12249
|
if (!filterArgument) return;
|
|
12600
12250
|
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;
|
|
12601
12251
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12602
|
-
const innerCall =
|
|
12252
|
+
const innerCall = node.callee.object;
|
|
12603
12253
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12604
12254
|
if (innerCall.callee.property.name !== "map") return;
|
|
12605
|
-
const receiver =
|
|
12255
|
+
const receiver = innerCall.callee.object;
|
|
12606
12256
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12607
12257
|
const elements = receiver.elements ?? [];
|
|
12608
12258
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13862,7 +13512,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13862
13512
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13863
13513
|
create: (context) => ({ CallExpression(node) {
|
|
13864
13514
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13865
|
-
const receiver =
|
|
13515
|
+
const receiver = node.callee.object;
|
|
13866
13516
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13867
13517
|
const spreadArgument = receiver.elements[0].argument;
|
|
13868
13518
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18899,8 +18549,7 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18899
18549
|
};
|
|
18900
18550
|
const describeClientSideNavigation = (node) => {
|
|
18901
18551
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18902
|
-
const
|
|
18903
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18552
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
18904
18553
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18905
18554
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18906
18555
|
}
|
|
@@ -19520,7 +19169,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19520
19169
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19521
19170
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19522
19171
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19523
|
-
if (!isCookieReceiver(
|
|
19172
|
+
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19524
19173
|
return node.callee.property.name;
|
|
19525
19174
|
};
|
|
19526
19175
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19534,7 +19183,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19534
19183
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19535
19184
|
const { property, object } = node.callee;
|
|
19536
19185
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19537
|
-
if (isSafeReceiverChain(
|
|
19186
|
+
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19538
19187
|
return true;
|
|
19539
19188
|
};
|
|
19540
19189
|
const getDbCallDescription = (node) => {
|
|
@@ -19542,8 +19191,7 @@ const getDbCallDescription = (node) => {
|
|
|
19542
19191
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19543
19192
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19544
19193
|
const methodName = node.callee.property.name;
|
|
19545
|
-
const
|
|
19546
|
-
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19194
|
+
const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
19547
19195
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19548
19196
|
};
|
|
19549
19197
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20943,13 +20591,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20943
20591
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20944
20592
|
let node = ref.identifier;
|
|
20945
20593
|
let parent = node.parent;
|
|
20946
|
-
while (parent &&
|
|
20594
|
+
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20947
20595
|
node = parent;
|
|
20948
20596
|
parent = node.parent;
|
|
20949
20597
|
}
|
|
20950
20598
|
if (current.callee === node) return current;
|
|
20951
20599
|
}
|
|
20952
|
-
if (isNodeOfType(current, "MemberExpression")
|
|
20600
|
+
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20953
20601
|
return null;
|
|
20954
20602
|
};
|
|
20955
20603
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -21084,7 +20732,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
21084
20732
|
const importDeclaration = declarationNode.parent;
|
|
21085
20733
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
21086
20734
|
}));
|
|
21087
|
-
const isHookCallee
|
|
20735
|
+
const isHookCallee = (analysis, node, hookName) => {
|
|
21088
20736
|
if (!node) return false;
|
|
21089
20737
|
if (isNodeOfType(node, "Identifier")) {
|
|
21090
20738
|
if (node.name === hookName) return true;
|
|
@@ -21093,19 +20741,15 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
21093
20741
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
21094
20742
|
return false;
|
|
21095
20743
|
}
|
|
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
|
-
}
|
|
20744
|
+
if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21100
20745
|
return false;
|
|
21101
20746
|
};
|
|
21102
20747
|
const isUseEffect = (node) => {
|
|
21103
20748
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
21104
20749
|
const callee = node.callee;
|
|
21105
20750
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
21106
|
-
if (
|
|
21107
|
-
|
|
21108
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20751
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
|
|
20752
|
+
return false;
|
|
21109
20753
|
};
|
|
21110
20754
|
const getEffectFn = (analysis, node) => {
|
|
21111
20755
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -21133,7 +20777,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21133
20777
|
const node = def.node;
|
|
21134
20778
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21135
20779
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21136
|
-
if (!isHookCallee
|
|
20780
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21137
20781
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21138
20782
|
const elements = node.id.elements ?? [];
|
|
21139
20783
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -21144,7 +20788,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
21144
20788
|
const node = def.node;
|
|
21145
20789
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21146
20790
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21147
|
-
if (!isHookCallee
|
|
20791
|
+
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21148
20792
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21149
20793
|
const elements = node.id.elements ?? [];
|
|
21150
20794
|
if (elements.length !== 2) return false;
|
|
@@ -21191,7 +20835,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21191
20835
|
const node = def.node;
|
|
21192
20836
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21193
20837
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21194
|
-
return isHookCallee
|
|
20838
|
+
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21195
20839
|
}));
|
|
21196
20840
|
const isRefCurrent = (ref) => {
|
|
21197
20841
|
const parent = ref.identifier.parent;
|
|
@@ -21204,15 +20848,11 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
21204
20848
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
21205
20849
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
21206
20850
|
if (!isPropAlias(analysis, ref)) return false;
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21210
|
-
effectiveNode = parent;
|
|
21211
|
-
parent = effectiveNode.parent;
|
|
21212
|
-
}
|
|
20851
|
+
const identifier = ref.identifier;
|
|
20852
|
+
const parent = identifier.parent;
|
|
21213
20853
|
if (!parent) return false;
|
|
21214
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
21215
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
20854
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
|
|
20855
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
|
|
21216
20856
|
const memberParent = parent.parent;
|
|
21217
20857
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
21218
20858
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -21223,7 +20863,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
21223
20863
|
};
|
|
21224
20864
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
21225
20865
|
const getUseStateDecl = (analysis, ref) => {
|
|
21226
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee
|
|
20866
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21227
20867
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
21228
20868
|
return node ?? null;
|
|
21229
20869
|
};
|
|
@@ -21428,7 +21068,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21428
21068
|
const noAdjustStateOnPropChange = defineRule({
|
|
21429
21069
|
id: "no-adjust-state-on-prop-change",
|
|
21430
21070
|
title: "State synced to a prop inside an effect",
|
|
21431
|
-
severity: "
|
|
21071
|
+
severity: "error",
|
|
21432
21072
|
tags: ["test-noise"],
|
|
21433
21073
|
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",
|
|
21434
21074
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21469,23 +21109,7 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21469
21109
|
"summary",
|
|
21470
21110
|
"textarea"
|
|
21471
21111
|
]);
|
|
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
|
-
]);
|
|
21484
21112
|
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
|
-
}
|
|
21489
21113
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21490
21114
|
switch (tagName) {
|
|
21491
21115
|
case "input": {
|
|
@@ -21499,10 +21123,7 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21499
21123
|
case "a":
|
|
21500
21124
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21501
21125
|
case "audio":
|
|
21502
|
-
case "video":
|
|
21503
|
-
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21504
|
-
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21505
|
-
}
|
|
21126
|
+
case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
|
|
21506
21127
|
default: return false;
|
|
21507
21128
|
}
|
|
21508
21129
|
};
|
|
@@ -22466,8 +22087,7 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22466
22087
|
"some"
|
|
22467
22088
|
]);
|
|
22468
22089
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22469
|
-
const isPositionallyStableIterationReceiver = (
|
|
22470
|
-
const receiver = stripParenExpression(receiverNode);
|
|
22090
|
+
const isPositionallyStableIterationReceiver = (receiver) => {
|
|
22471
22091
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22472
22092
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22473
22093
|
const only = receiver.elements[0];
|
|
@@ -22478,13 +22098,17 @@ const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
|
22478
22098
|
}
|
|
22479
22099
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22480
22100
|
const callee = receiver.callee;
|
|
22481
|
-
if (
|
|
22101
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22482
22102
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22483
22103
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22484
22104
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22485
22105
|
return false;
|
|
22486
22106
|
};
|
|
22487
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
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
|
+
};
|
|
22488
22112
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22489
22113
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22490
22114
|
for (const property of source.properties ?? []) {
|
|
@@ -22543,12 +22167,18 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22543
22167
|
return false;
|
|
22544
22168
|
}
|
|
22545
22169
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22546
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(
|
|
22170
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22547
22171
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22548
22172
|
}
|
|
22549
22173
|
return false;
|
|
22550
22174
|
};
|
|
22551
|
-
const isReactCloneElement = (callExpression) =>
|
|
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
|
+
};
|
|
22552
22182
|
const noArrayIndexKey = defineRule({
|
|
22553
22183
|
id: "no-array-index-key",
|
|
22554
22184
|
title: "Array index used as a key",
|
|
@@ -22847,10 +22477,6 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22847
22477
|
const section = manifest[sectionName];
|
|
22848
22478
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22849
22479
|
});
|
|
22850
|
-
const declaresDependency = (manifest, dependencyName) => {
|
|
22851
|
-
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22852
|
-
return false;
|
|
22853
|
-
};
|
|
22854
22480
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22855
22481
|
const classifyPackagePlatform = (filename) => {
|
|
22856
22482
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22867,7 +22493,9 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22867
22493
|
return result;
|
|
22868
22494
|
};
|
|
22869
22495
|
//#endregion
|
|
22870
|
-
//#region src/plugin/utils/is-
|
|
22496
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22497
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22871
22499
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22872
22500
|
const resolveRealDirectory = (directory) => {
|
|
22873
22501
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22888,10 +22516,6 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22888
22516
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22889
22517
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22890
22518
|
};
|
|
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?$/;
|
|
22895
22519
|
const classifyReactNativeFileTarget = (context) => {
|
|
22896
22520
|
const rawFilename = context.filename;
|
|
22897
22521
|
if (!rawFilename) return "unknown";
|
|
@@ -23272,7 +22896,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
23272
22896
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
23273
22897
|
return false;
|
|
23274
22898
|
};
|
|
23275
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
22899
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23276
22900
|
"forEach",
|
|
23277
22901
|
"map",
|
|
23278
22902
|
"filter",
|
|
@@ -23293,51 +22917,31 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
23293
22917
|
if (parent.callee === functionNode) return true;
|
|
23294
22918
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
23295
22919
|
const callee = parent.callee;
|
|
23296
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
22920
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23297
22921
|
};
|
|
23298
22922
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
23299
|
-
const
|
|
23300
|
-
|
|
22923
|
+
const isGuardWithTerminatingBranch = (statement) => {
|
|
22924
|
+
if (!isNodeOfType(statement, "IfStatement")) return null;
|
|
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) => {
|
|
23301
22932
|
let fallThroughCount = 0;
|
|
23302
|
-
let
|
|
22933
|
+
let maxTerminatingPathCount = 0;
|
|
23303
22934
|
for (const statement of statements) {
|
|
23304
|
-
if (
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
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);
|
|
22935
|
+
if (isFunctionLike$1(statement)) continue;
|
|
22936
|
+
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22937
|
+
if (guardBranch) {
|
|
22938
|
+
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
23320
22939
|
continue;
|
|
23321
22940
|
}
|
|
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
|
-
}
|
|
22941
|
+
if (isTerminatingStatement(statement)) break;
|
|
23330
22942
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
23331
22943
|
}
|
|
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);
|
|
22944
|
+
return Math.max(maxTerminatingPathCount, fallThroughCount);
|
|
23341
22945
|
};
|
|
23342
22946
|
const collectLocalHelperFunctions = (root) => {
|
|
23343
22947
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23364,23 +22968,14 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
23364
22968
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
23365
22969
|
return parent.parent === effectCallback.body;
|
|
23366
22970
|
};
|
|
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
|
-
};
|
|
23373
22971
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
23374
22972
|
if (!node || typeof node !== "object") return 0;
|
|
23375
|
-
if (
|
|
22973
|
+
if (isAsyncFunctionLike(node)) return 0;
|
|
23376
22974
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
23377
22975
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
23378
22976
|
const consequent = node.consequent;
|
|
23379
22977
|
const alternate = node.alternate;
|
|
23380
|
-
|
|
23381
|
-
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23382
|
-
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23383
|
-
return testCount + Math.max(thenCount, elseCount);
|
|
22978
|
+
return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
|
|
23384
22979
|
}
|
|
23385
22980
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
23386
22981
|
let maxRunSetters = 0;
|
|
@@ -23410,31 +23005,28 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23410
23005
|
}
|
|
23411
23006
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23412
23007
|
let nestedSettersInArgs = 0;
|
|
23413
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs +=
|
|
23008
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23414
23009
|
return 1 + nestedSettersInArgs;
|
|
23415
23010
|
}
|
|
23416
23011
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23417
23012
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23418
23013
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23419
23014
|
context.activeHelpers.add(helperFunction);
|
|
23420
|
-
let helperCount =
|
|
23015
|
+
let helperCount = countMaxPathSetStateCalls(helperFunction, context);
|
|
23421
23016
|
context.activeHelpers.delete(helperFunction);
|
|
23422
23017
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23423
23018
|
return helperCount;
|
|
23424
23019
|
}
|
|
23425
23020
|
}
|
|
23426
|
-
const
|
|
23427
|
-
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23428
|
-
return countMaxPathSetStateCalls(child, context);
|
|
23429
|
-
};
|
|
23021
|
+
const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
|
|
23430
23022
|
let total = 0;
|
|
23431
23023
|
const nodeRecord = node;
|
|
23432
23024
|
for (const key of Object.keys(nodeRecord)) {
|
|
23433
23025
|
if (key === "parent") continue;
|
|
23434
23026
|
const child = nodeRecord[key];
|
|
23435
23027
|
if (Array.isArray(child)) {
|
|
23436
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) total +=
|
|
23437
|
-
} else if (child && typeof child === "object" && "type" in child) total +=
|
|
23028
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
|
|
23029
|
+
} else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
|
|
23438
23030
|
}
|
|
23439
23031
|
return total;
|
|
23440
23032
|
};
|
|
@@ -23468,9 +23060,7 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23468
23060
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23469
23061
|
const firstStatement = (body.body ?? [])[0];
|
|
23470
23062
|
if (!firstStatement) return false;
|
|
23471
|
-
if (!
|
|
23472
|
-
const consequent = firstStatement.consequent;
|
|
23473
|
-
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23063
|
+
if (!isGuardWithTerminatingBranch(firstStatement)) return false;
|
|
23474
23064
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23475
23065
|
};
|
|
23476
23066
|
const noCascadingSetState = defineRule({
|
|
@@ -23485,7 +23075,7 @@ const noCascadingSetState = defineRule({
|
|
|
23485
23075
|
const callback = getEffectCallback(node);
|
|
23486
23076
|
if (!callback) return;
|
|
23487
23077
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23488
|
-
const setStateCallCount =
|
|
23078
|
+
const setStateCallCount = countMaxPathSetStateCalls(callback, {
|
|
23489
23079
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23490
23080
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23491
23081
|
effectCallback: callback
|
|
@@ -23829,6 +23419,40 @@ const noCloneElement = defineRule({
|
|
|
23829
23419
|
} })
|
|
23830
23420
|
});
|
|
23831
23421
|
//#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
|
|
23832
23456
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23833
23457
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23834
23458
|
const CONTEXT_MODULES = [
|
|
@@ -24799,21 +24423,11 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24799
24423
|
return isInitialOnlyPropName(arg.name);
|
|
24800
24424
|
};
|
|
24801
24425
|
//#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
|
|
24813
24426
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24814
24427
|
const getCallbackStatements = (callback) => {
|
|
24815
24428
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24816
|
-
|
|
24429
|
+
if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
|
|
24430
|
+
return callback.body ? [callback.body] : [];
|
|
24817
24431
|
};
|
|
24818
24432
|
//#endregion
|
|
24819
24433
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24852,27 +24466,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24852
24466
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24853
24467
|
}
|
|
24854
24468
|
};
|
|
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
|
-
};
|
|
24876
24469
|
const noDerivedStateEffect = defineRule({
|
|
24877
24470
|
id: "no-derived-state-effect",
|
|
24878
24471
|
title: "Derived state stored in an effect",
|
|
@@ -24904,8 +24497,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24904
24497
|
}
|
|
24905
24498
|
}
|
|
24906
24499
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24907
|
-
const statements =
|
|
24908
|
-
if (statements
|
|
24500
|
+
const statements = getCallbackStatements(callback);
|
|
24501
|
+
if (statements.length === 0) return;
|
|
24909
24502
|
if (!statements.every((statement) => {
|
|
24910
24503
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24911
24504
|
const expression = statement.expression;
|
|
@@ -25540,7 +25133,7 @@ const noDidMountSetState = defineRule({
|
|
|
25540
25133
|
const { mode } = resolveSettings$20(context.settings);
|
|
25541
25134
|
return { CallExpression(node) {
|
|
25542
25135
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25543
|
-
if (!isNodeOfType(
|
|
25136
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25544
25137
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25545
25138
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25546
25139
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25665,7 +25258,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25665
25258
|
const { mode } = resolveSettings$19(context.settings);
|
|
25666
25259
|
return { CallExpression(node) {
|
|
25667
25260
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25668
|
-
if (!isNodeOfType(
|
|
25261
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25669
25262
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25670
25263
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25671
25264
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25762,45 +25355,18 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25762
25355
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25763
25356
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25764
25357
|
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
|
-
]);
|
|
25785
25358
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25786
25359
|
const isPlainDataProducerCall = (expression) => {
|
|
25787
25360
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25788
25361
|
const callee = expression.callee;
|
|
25789
25362
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25790
25363
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
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
|
-
}
|
|
25364
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25796
25365
|
return producesPlainStateValue(callee.object);
|
|
25797
25366
|
};
|
|
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);
|
|
25800
25367
|
const producesPlainStateValue = (expression) => {
|
|
25801
25368
|
const unwrapped = stripParenExpression(expression);
|
|
25802
25369
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25803
|
-
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25804
25370
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25805
25371
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25806
25372
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25815,38 +25381,6 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25815
25381
|
}
|
|
25816
25382
|
return producesPlainStateValue(unwrapped);
|
|
25817
25383
|
};
|
|
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
|
-
};
|
|
25850
25384
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25851
25385
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25852
25386
|
walkAst(componentBody, (node) => {
|
|
@@ -25916,15 +25450,11 @@ const noDirectStateMutation = defineRule({
|
|
|
25916
25450
|
if (bindings.length === 0) return;
|
|
25917
25451
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25918
25452
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25919
|
-
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25920
25453
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25921
25454
|
for (const binding of bindings) {
|
|
25922
25455
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25923
25456
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25924
|
-
|
|
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);
|
|
25457
|
+
if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
|
|
25928
25458
|
}
|
|
25929
25459
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25930
25460
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -26049,10 +25579,9 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
26049
25579
|
create: (context) => ({ CallExpression(node) {
|
|
26050
25580
|
const callee = node.callee;
|
|
26051
25581
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
26052
|
-
|
|
26053
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25582
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
26054
25583
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
26055
|
-
if (context.scopes.symbolFor(
|
|
25584
|
+
if (context.scopes.symbolFor(callee.object) !== null) return;
|
|
26056
25585
|
if (!importsReactViewTransition(node)) return;
|
|
26057
25586
|
context.report({
|
|
26058
25587
|
node,
|
|
@@ -26072,8 +25601,7 @@ const noDocumentWrite = defineRule({
|
|
|
26072
25601
|
create: (context) => ({ CallExpression(node) {
|
|
26073
25602
|
const callee = node.callee;
|
|
26074
25603
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
26075
|
-
|
|
26076
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25604
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
26077
25605
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
26078
25606
|
context.report({
|
|
26079
25607
|
node,
|
|
@@ -26705,6 +26233,13 @@ const noEffectEventHandler = defineRule({
|
|
|
26705
26233
|
}
|
|
26706
26234
|
});
|
|
26707
26235
|
//#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
|
|
26708
26243
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26709
26244
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26710
26245
|
const componentBindingStack = [];
|
|
@@ -26758,7 +26293,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26758
26293
|
const initializer = declaratorNode.init;
|
|
26759
26294
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26760
26295
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26761
|
-
if (
|
|
26296
|
+
if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26762
26297
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26763
26298
|
} });
|
|
26764
26299
|
return {
|
|
@@ -29338,7 +28873,7 @@ const noIsMounted = defineRule({
|
|
|
29338
28873
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
29339
28874
|
create: (context) => ({ CallExpression(node) {
|
|
29340
28875
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
29341
|
-
if (!isNodeOfType(
|
|
28876
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29342
28877
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
29343
28878
|
if (!getParentComponent(node)) return;
|
|
29344
28879
|
context.report({
|
|
@@ -29353,9 +28888,7 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
29353
28888
|
const isJsonMethodCall = (node, method) => {
|
|
29354
28889
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
29355
28890
|
const callee = node.callee;
|
|
29356
|
-
|
|
29357
|
-
const receiver = stripParenExpression(callee.object);
|
|
29358
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28891
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
29359
28892
|
};
|
|
29360
28893
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
29361
28894
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29440,7 +28973,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29440
28973
|
const noJsxElementType = defineRule({
|
|
29441
28974
|
id: "no-jsx-element-type",
|
|
29442
28975
|
title: "No JSX.Element",
|
|
29443
|
-
severity: "
|
|
28976
|
+
severity: "error",
|
|
29444
28977
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29445
28978
|
create: (context) => {
|
|
29446
28979
|
let isJsxImported = false;
|
|
@@ -29766,378 +29299,6 @@ const noLegacyContextApi = defineRule({
|
|
|
29766
29299
|
}
|
|
29767
29300
|
});
|
|
29768
29301
|
//#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
|
|
30141
29302
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
30142
29303
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
30143
29304
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30951,6 +30112,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30951
30112
|
"reverse",
|
|
30952
30113
|
"sort"
|
|
30953
30114
|
]);
|
|
30115
|
+
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30954
30116
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30955
30117
|
"assign",
|
|
30956
30118
|
"defineProperties",
|
|
@@ -31024,6 +30186,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
31024
30186
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
31025
30187
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
31026
30188
|
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;
|
|
31027
30190
|
}
|
|
31028
30191
|
}
|
|
31029
30192
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -31062,7 +30225,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
31062
30225
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
31063
30226
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
31064
30227
|
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;
|
|
31066
30228
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
31067
30229
|
});
|
|
31068
30230
|
return mutations;
|
|
@@ -31295,7 +30457,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
31295
30457
|
id: "no-nested-component-definition",
|
|
31296
30458
|
title: "Component defined inside another component",
|
|
31297
30459
|
tags: ["test-noise", "react-jsx-only"],
|
|
31298
|
-
severity: "
|
|
30460
|
+
severity: "error",
|
|
31299
30461
|
category: "Correctness",
|
|
31300
30462
|
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.",
|
|
31301
30463
|
create: (context) => {
|
|
@@ -32250,12 +31412,12 @@ const noPassDataToParent = defineRule({
|
|
|
32250
31412
|
if (calleeNode === identifier) {
|
|
32251
31413
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
32252
31414
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
32253
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31415
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
|
|
32254
31416
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
32255
31417
|
if (isCustomHookParameter(ref)) continue;
|
|
32256
31418
|
} else continue;
|
|
32257
31419
|
const methodName = getCallMethodName(calleeNode);
|
|
32258
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31420
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32259
31421
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32260
31422
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
32261
31423
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -32446,7 +31608,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
32446
31608
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
32447
31609
|
const calleeNode = callExpr.callee;
|
|
32448
31610
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
32449
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") &&
|
|
31611
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32450
31612
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32451
31613
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
32452
31614
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32774,6 +31936,40 @@ const noPreventDefault = defineRule({
|
|
|
32774
31936
|
}
|
|
32775
31937
|
});
|
|
32776
31938
|
//#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
|
|
32777
31973
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32778
31974
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32779
31975
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32980,7 +32176,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32980
32176
|
return `${callee.name}()`;
|
|
32981
32177
|
}
|
|
32982
32178
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32983
|
-
const receiver =
|
|
32179
|
+
const receiver = callee.object;
|
|
32984
32180
|
const property = callee.property;
|
|
32985
32181
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32986
32182
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -33101,10 +32297,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33101
32297
|
if (typeof sourceValue !== "string") return;
|
|
33102
32298
|
if (handleExtraSource?.(node, context)) return;
|
|
33103
32299
|
if (sourceValue !== source) return;
|
|
33104
|
-
if (isTypeOnlyImport(node)) return;
|
|
33105
32300
|
for (const specifier of node.specifiers ?? []) {
|
|
33106
32301
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33107
|
-
if (specifier.importKind === "type") continue;
|
|
33108
32302
|
const importedName = getImportedName$1(specifier);
|
|
33109
32303
|
if (!importedName) continue;
|
|
33110
32304
|
const message = messages.get(importedName);
|
|
@@ -33123,9 +32317,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
33123
32317
|
MemberExpression(node) {
|
|
33124
32318
|
if (namespaceBindings.size === 0) return;
|
|
33125
32319
|
if (node.computed) return;
|
|
33126
|
-
|
|
33127
|
-
if (!
|
|
33128
|
-
if (!namespaceBindings.has(receiver.name)) return;
|
|
32320
|
+
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
32321
|
+
if (!namespaceBindings.has(node.object.name)) return;
|
|
33129
32322
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
33130
32323
|
const message = messages.get(node.property.name);
|
|
33131
32324
|
if (message) context.report({
|
|
@@ -33190,7 +32383,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
33190
32383
|
});
|
|
33191
32384
|
//#endregion
|
|
33192
32385
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
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."]]);
|
|
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."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
|
|
33194
32387
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
33195
32388
|
if (!rawFilename) return false;
|
|
33196
32389
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -33228,7 +32421,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
33228
32421
|
requires: ["react:19"],
|
|
33229
32422
|
tags: ["test-noise", "migration-hint"],
|
|
33230
32423
|
severity: "warn",
|
|
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.",
|
|
32424
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
|
|
33232
32425
|
create: (context) => {
|
|
33233
32426
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
33234
32427
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33552,11 +32745,34 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33552
32745
|
});
|
|
33553
32746
|
//#endregion
|
|
33554
32747
|
//#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
|
+
};
|
|
33555
32771
|
const isInsideComponentContext = (node) => {
|
|
33556
32772
|
let cursor = node.parent;
|
|
33557
32773
|
while (cursor) {
|
|
32774
|
+
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33558
32775
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33559
|
-
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33560
32776
|
cursor = cursor.parent ?? null;
|
|
33561
32777
|
}
|
|
33562
32778
|
return false;
|
|
@@ -33566,28 +32782,24 @@ const functionBodyOf = (node) => {
|
|
|
33566
32782
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
33567
32783
|
return null;
|
|
33568
32784
|
};
|
|
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
|
-
};
|
|
33574
32785
|
const containsHookCall = (body) => {
|
|
33575
32786
|
let found = false;
|
|
33576
32787
|
walkAst(body, (child) => {
|
|
33577
|
-
if (found) return
|
|
33578
|
-
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32788
|
+
if (found) return;
|
|
33579
32789
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33580
|
-
|
|
32790
|
+
const name = getCalleeName$2(child);
|
|
32791
|
+
if (name && isReactHookName(name)) found = true;
|
|
33581
32792
|
});
|
|
33582
32793
|
return found;
|
|
33583
32794
|
};
|
|
33584
|
-
const
|
|
32795
|
+
const isModuleScopeHookFreeHelper = (symbol) => {
|
|
33585
32796
|
if (!symbol) return false;
|
|
33586
32797
|
const declaration = symbol.declarationNode;
|
|
33587
32798
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33588
32799
|
const body = functionBodyOf(declaration);
|
|
33589
32800
|
if (!body) return false;
|
|
33590
|
-
|
|
32801
|
+
if (findEnclosingFunction(declaration) !== null) return false;
|
|
32802
|
+
return !containsHookCall(body);
|
|
33591
32803
|
};
|
|
33592
32804
|
const noRenderInRender = defineRule({
|
|
33593
32805
|
id: "no-render-in-render",
|
|
@@ -33596,13 +32808,20 @@ const noRenderInRender = defineRule({
|
|
|
33596
32808
|
tags: ["test-noise"],
|
|
33597
32809
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
33598
32810
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33599
|
-
const expression =
|
|
32811
|
+
const expression = node.expression;
|
|
33600
32812
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33601
|
-
|
|
33602
|
-
|
|
33603
|
-
if (
|
|
32813
|
+
let calleeName = null;
|
|
32814
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
32815
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
32816
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33604
32817
|
if (!isInsideComponentContext(node)) return;
|
|
33605
|
-
if (
|
|
32818
|
+
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
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
|
+
}
|
|
33606
32825
|
context.report({
|
|
33607
32826
|
node: expression,
|
|
33608
32827
|
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.`
|
|
@@ -33663,7 +32882,13 @@ const noRenderPropChildren = defineRule({
|
|
|
33663
32882
|
//#endregion
|
|
33664
32883
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
33665
32884
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
33666
|
-
const isReactDomRenderCall = (node) =>
|
|
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
|
+
};
|
|
33667
32892
|
const isUsedAsReturnValue = (parent) => {
|
|
33668
32893
|
if (!parent) return false;
|
|
33669
32894
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -34499,7 +33724,7 @@ const noSetState = defineRule({
|
|
|
34499
33724
|
category: "Architecture",
|
|
34500
33725
|
create: (context) => ({ CallExpression(node) {
|
|
34501
33726
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
34502
|
-
if (!isNodeOfType(
|
|
33727
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34503
33728
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
34504
33729
|
if (!getParentComponent(node)) return;
|
|
34505
33730
|
context.report({
|
|
@@ -34665,204 +33890,6 @@ const noSideTabBorder = defineRule({
|
|
|
34665
33890
|
})
|
|
34666
33891
|
});
|
|
34667
33892
|
//#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
|
|
34866
33893
|
//#region src/plugin/utils/is-abstract-role.ts
|
|
34867
33894
|
const isAbstractRole = (openingElement, settings) => {
|
|
34868
33895
|
const elementType = getElementType(openingElement, settings);
|
|
@@ -37143,36 +36170,6 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
37143
36170
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
37144
36171
|
return true;
|
|
37145
36172
|
};
|
|
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
|
-
};
|
|
37176
36173
|
const noUsememoSimpleExpression = defineRule({
|
|
37177
36174
|
id: "no-usememo-simple-expression",
|
|
37178
36175
|
title: "useMemo on a cheap value",
|
|
@@ -37194,17 +36191,9 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
37194
36191
|
let returnExpression = null;
|
|
37195
36192
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
37196
36193
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
37197
|
-
if (
|
|
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({
|
|
36194
|
+
if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
|
|
37206
36195
|
node,
|
|
37207
|
-
message: "This
|
|
36196
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37208
36197
|
});
|
|
37209
36198
|
} })
|
|
37210
36199
|
});
|
|
@@ -37310,7 +36299,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
37310
36299
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
37311
36300
|
return { CallExpression(node) {
|
|
37312
36301
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
37313
|
-
if (!isNodeOfType(
|
|
36302
|
+
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37314
36303
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
37315
36304
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
37316
36305
|
context.report({
|
|
@@ -37586,7 +36575,8 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
37586
36575
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
37587
36576
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
37588
36577
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
37589
|
-
const
|
|
36578
|
+
const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
|
|
36579
|
+
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
37590
36580
|
const DEFAULT_REACT_HOCS = [
|
|
37591
36581
|
"memo",
|
|
37592
36582
|
"forwardRef",
|
|
@@ -37627,10 +36617,6 @@ const isRouteFactoryCall = (expression) => {
|
|
|
37627
36617
|
}
|
|
37628
36618
|
return false;
|
|
37629
36619
|
};
|
|
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
|
-
});
|
|
37634
36620
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
37635
36621
|
const isHocCallee = (callee, state) => {
|
|
37636
36622
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -37661,20 +36647,6 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
37661
36647
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
37662
36648
|
return false;
|
|
37663
36649
|
};
|
|
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
|
-
};
|
|
37678
36650
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
37679
36651
|
if (initializer) {
|
|
37680
36652
|
const expression = skipTsExpression(initializer);
|
|
@@ -37705,10 +36677,6 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
37705
36677
|
reportNode
|
|
37706
36678
|
};
|
|
37707
36679
|
}
|
|
37708
|
-
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
37709
|
-
kind: "namespace-object",
|
|
37710
|
-
reportNode
|
|
37711
|
-
};
|
|
37712
36680
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
37713
36681
|
kind: "non-component",
|
|
37714
36682
|
reportNode
|
|
@@ -37725,7 +36693,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
37725
36693
|
walkAst(programRoot, (child) => {
|
|
37726
36694
|
const childType = child.type;
|
|
37727
36695
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
37728
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator"
|
|
36696
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
37729
36697
|
});
|
|
37730
36698
|
return {
|
|
37731
36699
|
exportNodes,
|
|
@@ -37770,35 +36738,18 @@ const onlyExportComponents = defineRule({
|
|
|
37770
36738
|
category: "Architecture",
|
|
37771
36739
|
create: (context) => {
|
|
37772
36740
|
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
|
+
};
|
|
37773
36746
|
return { Program(node) {
|
|
37774
36747
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
37775
36748
|
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
|
-
}
|
|
37799
36749
|
const exports = [];
|
|
37800
36750
|
let hasReactExport = false;
|
|
37801
36751
|
let hasAnyExports = false;
|
|
36752
|
+
const localComponents = [];
|
|
37802
36753
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
37803
36754
|
for (const child of exportNodes) {
|
|
37804
36755
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -37863,24 +36814,13 @@ const onlyExportComponents = defineRule({
|
|
|
37863
36814
|
return false;
|
|
37864
36815
|
})();
|
|
37865
36816
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37866
|
-
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37867
|
-
kind: "non-component",
|
|
37868
|
-
reportNode: stripped
|
|
37869
|
-
});
|
|
37870
36817
|
else context.report({
|
|
37871
36818
|
node: stripped,
|
|
37872
36819
|
message: ANONYMOUS_MESSAGE
|
|
37873
36820
|
});
|
|
37874
36821
|
continue;
|
|
37875
36822
|
}
|
|
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")) {
|
|
36823
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
|
|
37884
36824
|
context.report({
|
|
37885
36825
|
node: stripped,
|
|
37886
36826
|
message: ANONYMOUS_MESSAGE
|
|
@@ -37933,23 +36873,32 @@ const onlyExportComponents = defineRule({
|
|
|
37933
36873
|
let entry;
|
|
37934
36874
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
37935
36875
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
37936
|
-
else {
|
|
37937
|
-
|
|
37938
|
-
|
|
37939
|
-
|
|
37940
|
-
};
|
|
37941
|
-
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37942
|
-
}
|
|
36876
|
+
else entry = {
|
|
36877
|
+
kind: "non-component",
|
|
36878
|
+
reportNode
|
|
36879
|
+
};
|
|
37943
36880
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
37944
36881
|
exports.push(entry);
|
|
37945
36882
|
}
|
|
37946
36883
|
}
|
|
37947
36884
|
}
|
|
37948
36885
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
37949
|
-
|
|
37950
|
-
node
|
|
37951
|
-
|
|
37952
|
-
|
|
36886
|
+
const isInsideExport = (node) => {
|
|
36887
|
+
let walker = node.parent;
|
|
36888
|
+
while (walker) {
|
|
36889
|
+
if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
|
|
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
|
+
}
|
|
37953
36902
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
37954
36903
|
if (entry.kind === "non-component") context.report({
|
|
37955
36904
|
node: entry.reportNode,
|
|
@@ -37960,6 +36909,14 @@ const onlyExportComponents = defineRule({
|
|
|
37960
36909
|
message: REACT_CONTEXT_MESSAGE
|
|
37961
36910
|
});
|
|
37962
36911
|
}
|
|
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
|
+
});
|
|
37963
36920
|
} };
|
|
37964
36921
|
}
|
|
37965
36922
|
});
|
|
@@ -38757,8 +37714,8 @@ const preferHtmlDialog = defineRule({
|
|
|
38757
37714
|
if (!HTML_TAGS.has(tagName)) return;
|
|
38758
37715
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
38759
37716
|
if (roleAttribute) {
|
|
38760
|
-
const
|
|
38761
|
-
if (
|
|
37717
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
37718
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
38762
37719
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
38763
37720
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
38764
37721
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -39462,22 +38419,11 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
39462
38419
|
}
|
|
39463
38420
|
return null;
|
|
39464
38421
|
};
|
|
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
|
-
};
|
|
39472
38422
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
39473
38423
|
const handlerStatements = getCallbackStatements(handler);
|
|
39474
38424
|
if (handlerStatements.length !== 1) return null;
|
|
39475
38425
|
const onlyStatement = handlerStatements[0];
|
|
39476
|
-
|
|
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);
|
|
38426
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
39481
38427
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
39482
38428
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
39483
38429
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -39487,106 +38433,6 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
39487
38433
|
setterArgument: expression.arguments[0]
|
|
39488
38434
|
};
|
|
39489
38435
|
};
|
|
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
|
-
};
|
|
39590
38436
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
39591
38437
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
39592
38438
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -39603,16 +38449,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39603
38449
|
severity: "warn",
|
|
39604
38450
|
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.",
|
|
39605
38451
|
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
|
-
};
|
|
39616
38452
|
const checkComponent = (componentBody) => {
|
|
39617
38453
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
39618
38454
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -39650,7 +38486,6 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39650
38486
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
39651
38487
|
if (!useStateInitializer) continue;
|
|
39652
38488
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39653
|
-
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
39654
38489
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
39655
38490
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
39656
38491
|
context.report({
|
|
@@ -39658,47 +38493,14 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
39658
38493
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
39659
38494
|
});
|
|
39660
38495
|
}
|
|
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
|
-
}
|
|
39691
38496
|
};
|
|
39692
38497
|
return {
|
|
39693
38498
|
FunctionDeclaration(node) {
|
|
39694
|
-
|
|
39695
|
-
if (!functionName) return;
|
|
39696
|
-
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38499
|
+
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
39697
38500
|
checkComponent(node.body);
|
|
39698
38501
|
},
|
|
39699
38502
|
VariableDeclarator(node) {
|
|
39700
|
-
|
|
39701
|
-
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38503
|
+
if (!isComponentAssignment(node)) return;
|
|
39702
38504
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
39703
38505
|
checkComponent(node.init.body);
|
|
39704
38506
|
}
|
|
@@ -40308,7 +39110,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
40308
39110
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
40309
39111
|
const body = queryFnValue.body;
|
|
40310
39112
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
40311
|
-
if ((body.body ?? []).
|
|
39113
|
+
if ((body.body ?? []).length === 0) context.report({
|
|
40312
39114
|
node: queryFnProperty,
|
|
40313
39115
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
40314
39116
|
});
|
|
@@ -40815,6 +39617,17 @@ const renderingHoistJsx = defineRule({
|
|
|
40815
39617
|
}
|
|
40816
39618
|
});
|
|
40817
39619
|
//#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
|
|
40818
39631
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
40819
39632
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
40820
39633
|
{
|
|
@@ -40823,19 +39636,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
40823
39636
|
},
|
|
40824
39637
|
{
|
|
40825
39638
|
display: "Date.now()",
|
|
40826
|
-
matches: (node) =>
|
|
39639
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
|
|
40827
39640
|
},
|
|
40828
39641
|
{
|
|
40829
39642
|
display: "Math.random()",
|
|
40830
|
-
matches: (node) =>
|
|
39643
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
|
|
40831
39644
|
},
|
|
40832
39645
|
{
|
|
40833
39646
|
display: "performance.now()",
|
|
40834
|
-
matches: (node) =>
|
|
39647
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
|
|
40835
39648
|
},
|
|
40836
39649
|
{
|
|
40837
39650
|
display: "crypto.randomUUID()",
|
|
40838
|
-
matches: (node) =>
|
|
39651
|
+
matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
|
|
40839
39652
|
}
|
|
40840
39653
|
];
|
|
40841
39654
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -40847,6 +39660,54 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
40847
39660
|
}
|
|
40848
39661
|
return null;
|
|
40849
39662
|
};
|
|
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
|
+
};
|
|
40850
39711
|
const isYearOnlyDateRead = (dateNode) => {
|
|
40851
39712
|
const member = dateNode.parent;
|
|
40852
39713
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -40870,6 +39731,23 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
40870
39731
|
}
|
|
40871
39732
|
return false;
|
|
40872
39733
|
};
|
|
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
|
+
};
|
|
40873
39751
|
const renderingHydrationMismatchTime = defineRule({
|
|
40874
39752
|
id: "rendering-hydration-mismatch-time",
|
|
40875
39753
|
title: "Time or random value in JSX",
|
|
@@ -40917,35 +39795,6 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
40917
39795
|
}
|
|
40918
39796
|
});
|
|
40919
39797
|
//#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
|
|
40949
39798
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
40950
39799
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
40951
39800
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -41011,15 +39860,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
41011
39860
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
41012
39861
|
const callback = getEffectCallback(node);
|
|
41013
39862
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
41014
|
-
const bodyStatements =
|
|
41015
|
-
if (bodyStatements.length !== 1) return;
|
|
39863
|
+
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39864
|
+
if (!bodyStatements || bodyStatements.length !== 1) return;
|
|
41016
39865
|
const soleStatement = bodyStatements[0];
|
|
41017
39866
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
41018
39867
|
const expression = soleStatement.expression;
|
|
41019
39868
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
41020
39869
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
41021
39870
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
41022
|
-
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
41023
39871
|
context.report({
|
|
41024
39872
|
node,
|
|
41025
39873
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -41838,9 +40686,6 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
41838
40686
|
} })
|
|
41839
40687
|
});
|
|
41840
40688
|
//#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
|
|
41844
40689
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
41845
40690
|
const rerenderLazyRefInit = defineRule({
|
|
41846
40691
|
id: "rerender-lazy-ref-init",
|
|
@@ -41851,7 +40696,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41851
40696
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
41852
40697
|
create: (context) => ({ CallExpression(node) {
|
|
41853
40698
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
41854
|
-
const initializer =
|
|
40699
|
+
const initializer = node.arguments[0];
|
|
41855
40700
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
41856
40701
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
41857
40702
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -41859,7 +40704,6 @@ const rerenderLazyRefInit = defineRule({
|
|
|
41859
40704
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41860
40705
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41861
40706
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41862
|
-
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
41863
40707
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
41864
40708
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
41865
40709
|
context.report({
|
|
@@ -41892,14 +40736,26 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
41892
40736
|
"getUTCMilliseconds",
|
|
41893
40737
|
"valueOf"
|
|
41894
40738
|
]);
|
|
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
|
+
]);
|
|
41895
40751
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
41896
40752
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
41897
40753
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
41898
|
-
|
|
41899
|
-
if (isNodeOfType(
|
|
41900
|
-
if (isNodeOfType(
|
|
41901
|
-
if (isNodeOfType(
|
|
41902
|
-
if (isNodeOfType(
|
|
40754
|
+
if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
|
|
40755
|
+
if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
|
|
40756
|
+
if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
|
|
40757
|
+
if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
|
|
40758
|
+
if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
|
|
41903
40759
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
41904
40760
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
41905
40761
|
if (spreadCall) return spreadCall;
|
|
@@ -41922,7 +40778,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
41922
40778
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41923
40779
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41924
40780
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41925
|
-
if (
|
|
40781
|
+
if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
41926
40782
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
41927
40783
|
if (isReactHookName(calleeName)) return;
|
|
41928
40784
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -42589,7 +41445,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
42589
41445
|
const body = reactionFn.body;
|
|
42590
41446
|
let singleAssignment = null;
|
|
42591
41447
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
42592
|
-
const statements =
|
|
41448
|
+
const statements = body.body ?? [];
|
|
42593
41449
|
if (statements.length !== 1) return;
|
|
42594
41450
|
const onlyStatement = statements[0];
|
|
42595
41451
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -42635,7 +41491,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
42635
41491
|
});
|
|
42636
41492
|
//#endregion
|
|
42637
41493
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
42638
|
-
const EMPTY_VISITORS$
|
|
41494
|
+
const EMPTY_VISITORS$4 = {};
|
|
42639
41495
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
42640
41496
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
42641
41497
|
"tap",
|
|
@@ -42663,8 +41519,7 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
42663
41519
|
"catch",
|
|
42664
41520
|
"finally"
|
|
42665
41521
|
]);
|
|
42666
|
-
const findChainRoot = (
|
|
42667
|
-
const node = stripParenExpression(wrappedNode);
|
|
41522
|
+
const findChainRoot = (node) => {
|
|
42668
41523
|
if (isNodeOfType(node, "CallExpression")) {
|
|
42669
41524
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
42670
41525
|
calleeName: node.callee.name,
|
|
@@ -42696,7 +41551,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
42696
41551
|
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.",
|
|
42697
41552
|
create: (context) => {
|
|
42698
41553
|
const filename = normalizeFilename(context.filename ?? "");
|
|
42699
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41554
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
|
|
42700
41555
|
return { ExpressionStatement(node) {
|
|
42701
41556
|
const expression = node.expression;
|
|
42702
41557
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -43252,21 +42107,20 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
43252
42107
|
};
|
|
43253
42108
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
43254
42109
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
43255
|
-
|
|
43256
|
-
|
|
43257
|
-
const localName = receiver.name;
|
|
42110
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
42111
|
+
const localName = callee.object.name;
|
|
43258
42112
|
const binding = findVariableInitializer(node, localName);
|
|
43259
42113
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
43260
42114
|
return localName === "Dimensions";
|
|
43261
42115
|
}
|
|
43262
|
-
if (isNodeOfType(
|
|
43263
|
-
if (getInitializerModuleSource(node,
|
|
43264
|
-
const rootName = getRootIdentifierName(
|
|
42116
|
+
if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
|
|
42117
|
+
if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
|
|
42118
|
+
const rootName = getRootIdentifierName(callee.object.object);
|
|
43265
42119
|
if (rootName === null) return false;
|
|
43266
42120
|
const importBinding = getImportBindingForName(node, rootName);
|
|
43267
42121
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
43268
42122
|
}
|
|
43269
|
-
if (getRequireCallSource(
|
|
42123
|
+
if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
|
|
43270
42124
|
return false;
|
|
43271
42125
|
};
|
|
43272
42126
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -43672,7 +42526,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
43672
42526
|
};
|
|
43673
42527
|
//#endregion
|
|
43674
42528
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
43675
|
-
const EMPTY_VISITORS$
|
|
42529
|
+
const EMPTY_VISITORS$3 = {};
|
|
43676
42530
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
43677
42531
|
const legacyShadowPropertyNames = [];
|
|
43678
42532
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -43695,7 +42549,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
43695
42549
|
severity: "warn",
|
|
43696
42550
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
43697
42551
|
create: (context) => {
|
|
43698
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42552
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
|
|
43699
42553
|
return {
|
|
43700
42554
|
JSXAttribute(node) {
|
|
43701
42555
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43710,8 +42564,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
43710
42564
|
},
|
|
43711
42565
|
CallExpression(node) {
|
|
43712
42566
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
43713
|
-
|
|
43714
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42567
|
+
if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
|
|
43715
42568
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
43716
42569
|
const stylesArgument = node.arguments?.[0];
|
|
43717
42570
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -44561,7 +43414,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
44561
43414
|
const callee = node.callee;
|
|
44562
43415
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
44563
43416
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
44564
|
-
if (!isStaticMemberNamed(
|
|
43417
|
+
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44565
43418
|
context.report({
|
|
44566
43419
|
node,
|
|
44567
43420
|
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."
|
|
@@ -44617,7 +43470,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
44617
43470
|
};
|
|
44618
43471
|
//#endregion
|
|
44619
43472
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
44620
|
-
const EMPTY_VISITORS$
|
|
43473
|
+
const EMPTY_VISITORS$2 = {};
|
|
44621
43474
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
44622
43475
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
44623
43476
|
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);
|
|
@@ -44651,7 +43504,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
44651
43504
|
severity: "warn",
|
|
44652
43505
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
44653
43506
|
create: (context) => {
|
|
44654
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43507
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
|
|
44655
43508
|
const flaggedImports = [];
|
|
44656
43509
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
44657
43510
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -44789,15 +43642,14 @@ const analyzeGestureChain = (expression) => {
|
|
|
44789
43642
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
44790
43643
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
44791
43644
|
const methodName = callee.property.name;
|
|
44792
|
-
|
|
44793
|
-
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43645
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
44794
43646
|
factoryName: methodName,
|
|
44795
43647
|
chainMethodNames,
|
|
44796
43648
|
numberOfTapsArgument
|
|
44797
43649
|
};
|
|
44798
43650
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
44799
43651
|
chainMethodNames.push(methodName);
|
|
44800
|
-
cursor =
|
|
43652
|
+
cursor = callee.object;
|
|
44801
43653
|
}
|
|
44802
43654
|
return null;
|
|
44803
43655
|
};
|
|
@@ -45139,7 +43991,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
45139
43991
|
});
|
|
45140
43992
|
//#endregion
|
|
45141
43993
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
45142
|
-
const EMPTY_VISITORS$
|
|
43994
|
+
const EMPTY_VISITORS$1 = {};
|
|
45143
43995
|
const IOS_SHADOW_KEYS = new Set([
|
|
45144
43996
|
"shadowColor",
|
|
45145
43997
|
"shadowOffset",
|
|
@@ -45225,7 +44077,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
45225
44077
|
severity: "warn",
|
|
45226
44078
|
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.",
|
|
45227
44079
|
create: (context) => {
|
|
45228
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44080
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
|
|
45229
44081
|
return {
|
|
45230
44082
|
JSXAttribute(node) {
|
|
45231
44083
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -45322,9 +44174,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
45322
44174
|
if (!HTML_TAGS.has(elementType)) return;
|
|
45323
44175
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
45324
44176
|
if (!roleAttribute) return;
|
|
45325
|
-
const
|
|
45326
|
-
if (
|
|
45327
|
-
const roles =
|
|
44177
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
44178
|
+
if (roleValue === null) return;
|
|
44179
|
+
const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
|
|
45328
44180
|
for (const role of roles) {
|
|
45329
44181
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
45330
44182
|
if (!required) continue;
|
|
@@ -48441,9 +47293,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
48441
47293
|
};
|
|
48442
47294
|
//#endregion
|
|
48443
47295
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
48444
|
-
const buildMessageDefault = (
|
|
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
|
-
};
|
|
47296
|
+
const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
|
|
48447
47297
|
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.`;
|
|
48448
47298
|
const roleSupportsAriaProps = defineRule({
|
|
48449
47299
|
id: "role-supports-aria-props",
|
|
@@ -48463,8 +47313,6 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48463
47313
|
const propName = propRawName.toLowerCase();
|
|
48464
47314
|
if (!propName.startsWith("aria-")) continue;
|
|
48465
47315
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48466
|
-
const attributeValue = attributeNode.value;
|
|
48467
|
-
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
48468
47316
|
(ariaAttributes ??= []).push({
|
|
48469
47317
|
attribute,
|
|
48470
47318
|
propName
|
|
@@ -48473,21 +47321,17 @@ const roleSupportsAriaProps = defineRule({
|
|
|
48473
47321
|
if (!ariaAttributes) return;
|
|
48474
47322
|
const elementType = getElementType(node, context.settings);
|
|
48475
47323
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
48476
|
-
const
|
|
48477
|
-
if (
|
|
48478
|
-
|
|
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
|
-
}
|
|
47324
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
47325
|
+
if (!role) return;
|
|
47326
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48485
47327
|
const isImplicit = !roleAttribute;
|
|
47328
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
+
if (!supported) return;
|
|
48486
47330
|
for (const { attribute, propName } of ariaAttributes) {
|
|
48487
|
-
if (
|
|
47331
|
+
if (supported.has(propName)) continue;
|
|
48488
47332
|
context.report({
|
|
48489
47333
|
node: attribute,
|
|
48490
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47334
|
+
message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
|
|
48491
47335
|
});
|
|
48492
47336
|
}
|
|
48493
47337
|
} })
|
|
@@ -48834,6 +47678,17 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
48834
47678
|
}
|
|
48835
47679
|
return false;
|
|
48836
47680
|
};
|
|
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
|
+
};
|
|
48837
47692
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
48838
47693
|
let current = node.parent;
|
|
48839
47694
|
while (current) {
|
|
@@ -48911,7 +47766,7 @@ const rulesOfHooks = defineRule({
|
|
|
48911
47766
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
48912
47767
|
if (!hookContext) return;
|
|
48913
47768
|
const { hookName } = hookContext;
|
|
48914
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node
|
|
47769
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48915
47770
|
context.report({
|
|
48916
47771
|
node: node.callee,
|
|
48917
47772
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -49023,7 +47878,8 @@ const rulesOfHooks = defineRule({
|
|
|
49023
47878
|
},
|
|
49024
47879
|
Identifier(node) {
|
|
49025
47880
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
49026
|
-
if (!symbol || !
|
|
47881
|
+
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
+
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
49027
47883
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
49028
47884
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
49029
47885
|
context.report({
|
|
@@ -49171,8 +48027,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
49171
48027
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
49172
48028
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
49173
48029
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
49174
|
-
const
|
|
49175
|
-
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48030
|
+
const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
|
|
49176
48031
|
if (!objectName) return;
|
|
49177
48032
|
const methodName = node.callee.property.name;
|
|
49178
48033
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -49848,17 +48703,7 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
49848
48703
|
};
|
|
49849
48704
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
49850
48705
|
let chainTip = referenceIdentifier;
|
|
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
|
-
}
|
|
48706
|
+
while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
|
|
49862
48707
|
return chainTip;
|
|
49863
48708
|
};
|
|
49864
48709
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -49883,8 +48728,7 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
49883
48728
|
const callee = callExpression.callee;
|
|
49884
48729
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
49885
48730
|
const methodName = getMemberPropertyName$1(callee);
|
|
49886
|
-
|
|
49887
|
-
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48731
|
+
return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
49888
48732
|
}
|
|
49889
48733
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
49890
48734
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -50614,7 +49458,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50614
49458
|
};
|
|
50615
49459
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
50616
49460
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
50617
|
-
let currentNode =
|
|
49461
|
+
let currentNode = outerNode.callee.object;
|
|
50618
49462
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
50619
49463
|
const calleeName = getCalleeName$2(currentNode);
|
|
50620
49464
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -50625,7 +49469,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
50625
49469
|
}
|
|
50626
49470
|
}
|
|
50627
49471
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
50628
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode =
|
|
49472
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50629
49473
|
else break;
|
|
50630
49474
|
}
|
|
50631
49475
|
return result;
|
|
@@ -51278,7 +50122,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
51278
50122
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
51279
50123
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
51280
50124
|
if (methodName) methodNames.unshift(methodName);
|
|
51281
|
-
currentNode =
|
|
50125
|
+
currentNode = currentNode.callee.object;
|
|
51282
50126
|
}
|
|
51283
50127
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
51284
50128
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -54360,18 +53204,6 @@ const reactDoctorRules = [
|
|
|
54360
53204
|
category: "Bugs"
|
|
54361
53205
|
}
|
|
54362
53206
|
},
|
|
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
|
-
},
|
|
54375
53207
|
{
|
|
54376
53208
|
key: "react-doctor/no-long-transition-duration",
|
|
54377
53209
|
id: "no-long-transition-duration",
|
|
@@ -54800,18 +53632,6 @@ const reactDoctorRules = [
|
|
|
54800
53632
|
category: "Maintainability"
|
|
54801
53633
|
}
|
|
54802
53634
|
},
|
|
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
|
-
},
|
|
54815
53635
|
{
|
|
54816
53636
|
key: "react-doctor/no-static-element-interactions",
|
|
54817
53637
|
id: "no-static-element-interactions",
|
|
@@ -56631,34 +55451,6 @@ const reactDoctorRules = [
|
|
|
56631
55451
|
];
|
|
56632
55452
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
56633
55453
|
//#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
|
|
56662
55454
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
56663
55455
|
const EMPTY_VISITORS = {};
|
|
56664
55456
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -57128,14 +55920,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
57128
55920
|
});
|
|
57129
55921
|
//#endregion
|
|
57130
55922
|
//#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
|
-
};
|
|
57136
55923
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
57137
55924
|
const wrapped = {};
|
|
57138
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
55925
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
|
|
57139
55926
|
return wrapped;
|
|
57140
55927
|
};
|
|
57141
55928
|
const plugin = {
|
|
@@ -57260,7 +56047,6 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
57260
56047
|
"nextjs-no-use-search-params-without-suspense",
|
|
57261
56048
|
"no-dynamic-import-path",
|
|
57262
56049
|
"no-full-lodash-import",
|
|
57263
|
-
"no-locale-format-in-render",
|
|
57264
56050
|
"no-mutating-reducer-state",
|
|
57265
56051
|
"prefer-dynamic-import",
|
|
57266
56052
|
"rendering-hydration-mismatch-time",
|
|
@@ -57395,7 +56181,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
57395
56181
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
57396
56182
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
57397
56183
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57398
|
-
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
57399
56184
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
57400
56185
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
57401
56186
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|