oxlint-plugin-react-doctor 0.7.2-dev.b97a92f → 0.7.2-dev.e872767
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +1456 -520
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
+
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
+
"WebSocket",
|
|
537
|
+
"EventSource",
|
|
538
|
+
"BroadcastChannel",
|
|
539
|
+
"RTCPeerConnection"
|
|
540
|
+
]);
|
|
535
541
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
536
542
|
"location",
|
|
537
543
|
"window",
|
|
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
595
601
|
"parseInt",
|
|
596
602
|
"parseFloat"
|
|
597
603
|
]);
|
|
604
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
605
|
+
"Date",
|
|
606
|
+
"Map",
|
|
607
|
+
"Set",
|
|
608
|
+
"WeakMap",
|
|
609
|
+
"WeakSet",
|
|
610
|
+
"WeakRef",
|
|
611
|
+
"RegExp",
|
|
612
|
+
"Error",
|
|
613
|
+
"URL",
|
|
614
|
+
"URLSearchParams",
|
|
615
|
+
"AbortController"
|
|
616
|
+
]);
|
|
598
617
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
599
618
|
"Boolean",
|
|
600
619
|
"String",
|
|
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
674
693
|
"unwatch",
|
|
675
694
|
"unlisten",
|
|
676
695
|
"unsub",
|
|
677
|
-
"abort"
|
|
696
|
+
"abort",
|
|
697
|
+
"disconnect",
|
|
698
|
+
"unobserve",
|
|
699
|
+
"close"
|
|
678
700
|
]);
|
|
679
701
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
680
702
|
"remove",
|
|
@@ -2235,6 +2257,39 @@ const anchorHasContent = defineRule({
|
|
|
2235
2257
|
}
|
|
2236
2258
|
});
|
|
2237
2259
|
//#endregion
|
|
2260
|
+
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
+
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
+
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
+
const expression = stripParenExpression(rawExpression);
|
|
2264
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
+
return staticValue === null ? null : [staticValue];
|
|
2268
|
+
}
|
|
2269
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
+
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
+
if (consequentValues === null) return null;
|
|
2272
|
+
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
+
if (alternateValues === null) return null;
|
|
2274
|
+
return [...consequentValues, ...alternateValues];
|
|
2275
|
+
}
|
|
2276
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
+
if (remainingHops === 0) return null;
|
|
2278
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2281
|
+
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2282
|
+
}
|
|
2283
|
+
return null;
|
|
2284
|
+
};
|
|
2285
|
+
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2286
|
+
const value = attribute.value;
|
|
2287
|
+
if (!value) return null;
|
|
2288
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2289
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2290
|
+
return null;
|
|
2291
|
+
};
|
|
2292
|
+
//#endregion
|
|
2238
2293
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2239
2294
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2240
2295
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2262,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2262
2317
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2263
2318
|
const isInvalidHref = (value, validHrefs) => {
|
|
2264
2319
|
if (validHrefs.has(value)) return false;
|
|
2265
|
-
|
|
2320
|
+
const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
|
|
2321
|
+
return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
|
|
2266
2322
|
};
|
|
2267
|
-
const
|
|
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;
|
|
2275
|
-
};
|
|
2276
|
-
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2277
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2323
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2278
2324
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2279
2325
|
const expression = value.expression;
|
|
2280
2326
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2281
|
-
if (isNodeOfType(expression, "Literal"))
|
|
2282
|
-
if (expression.value === null) return true;
|
|
2283
|
-
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2284
|
-
}
|
|
2285
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2286
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2287
|
-
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2288
|
-
}
|
|
2327
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2289
2328
|
}
|
|
2290
2329
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2291
2330
|
return false;
|
|
@@ -2318,9 +2357,10 @@ const anchorIsValid = defineRule({
|
|
|
2318
2357
|
});
|
|
2319
2358
|
return;
|
|
2320
2359
|
}
|
|
2321
|
-
|
|
2360
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2361
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2322
2362
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2323
|
-
if (!hasOnClick &&
|
|
2363
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2324
2364
|
context.report({
|
|
2325
2365
|
node: node.name,
|
|
2326
2366
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3376,6 +3416,25 @@ const ariaRole = defineRule({
|
|
|
3376
3416
|
if (!roleAttribute) return;
|
|
3377
3417
|
const elementType = getElementType(node, context.settings);
|
|
3378
3418
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3419
|
+
const reportFirstInvalidCandidate = (candidates) => {
|
|
3420
|
+
for (const candidate of candidates) {
|
|
3421
|
+
if (candidate.trim().length === 0) {
|
|
3422
|
+
context.report({
|
|
3423
|
+
node: roleAttribute,
|
|
3424
|
+
message: buildBaseMessage("")
|
|
3425
|
+
});
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3429
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3430
|
+
context.report({
|
|
3431
|
+
node: roleAttribute,
|
|
3432
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3433
|
+
});
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3379
3438
|
const value = roleAttribute.value;
|
|
3380
3439
|
if (!value) {
|
|
3381
3440
|
context.report({
|
|
@@ -3392,22 +3451,7 @@ const ariaRole = defineRule({
|
|
|
3392
3451
|
});
|
|
3393
3452
|
return;
|
|
3394
3453
|
}
|
|
3395
|
-
|
|
3396
|
-
if (stringValue.trim().length === 0) {
|
|
3397
|
-
context.report({
|
|
3398
|
-
node: roleAttribute,
|
|
3399
|
-
message: buildBaseMessage("")
|
|
3400
|
-
});
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3404
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3405
|
-
context.report({
|
|
3406
|
-
node: roleAttribute,
|
|
3407
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3408
|
-
});
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3454
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3411
3455
|
return;
|
|
3412
3456
|
}
|
|
3413
3457
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3428,6 +3472,11 @@ const ariaRole = defineRule({
|
|
|
3428
3472
|
});
|
|
3429
3473
|
return;
|
|
3430
3474
|
}
|
|
3475
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3476
|
+
if (resolvedCandidates !== null) {
|
|
3477
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3431
3480
|
return;
|
|
3432
3481
|
}
|
|
3433
3482
|
context.report({
|
|
@@ -5144,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5144
5193
|
const callee = node.callee;
|
|
5145
5194
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5146
5195
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5147
|
-
if (!isWebStorageObject(callee.object)) return;
|
|
5196
|
+
if (!isWebStorageObject(stripParenExpression(callee.object))) return;
|
|
5148
5197
|
const keyArgument = node.arguments?.[0];
|
|
5149
5198
|
if (!keyArgument) return;
|
|
5150
5199
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6084,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6084
6133
|
})
|
|
6085
6134
|
});
|
|
6086
6135
|
//#endregion
|
|
6136
|
+
//#region src/plugin/utils/is-global-method-call.ts
|
|
6137
|
+
const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
6138
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6139
|
+
const callee = stripParenExpression(node.callee);
|
|
6140
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
6141
|
+
const receiver = stripParenExpression(callee.object);
|
|
6142
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6143
|
+
};
|
|
6144
|
+
//#endregion
|
|
6087
6145
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6088
6146
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6089
6147
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6090
6148
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6091
6149
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6092
|
-
const isJsonStringifyCall = (node) =>
|
|
6093
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6094
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6095
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6096
|
-
if (node.callee.object.name !== "JSON") return false;
|
|
6097
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6098
|
-
return node.callee.property.name === "stringify";
|
|
6099
|
-
};
|
|
6150
|
+
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6100
6151
|
const resolveStringKey = (keyArg, context) => {
|
|
6101
6152
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6102
6153
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6115,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6115
6166
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6116
6167
|
create: (context) => ({ CallExpression(node) {
|
|
6117
6168
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6118
|
-
|
|
6119
|
-
if (!
|
|
6169
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6170
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6171
|
+
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6120
6172
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6121
6173
|
if (node.callee.property.name !== "setItem") return;
|
|
6122
6174
|
const keyArg = node.arguments?.[0];
|
|
@@ -6129,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6129
6181
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6130
6182
|
context.report({
|
|
6131
6183
|
node: keyArg,
|
|
6132
|
-
message: `${
|
|
6184
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6133
6185
|
});
|
|
6134
6186
|
} })
|
|
6135
6187
|
});
|
|
@@ -7610,6 +7662,96 @@ const displayName = defineRule({
|
|
|
7610
7662
|
}
|
|
7611
7663
|
});
|
|
7612
7664
|
//#endregion
|
|
7665
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7666
|
+
const isReactHookName = (name) => {
|
|
7667
|
+
if (!name.startsWith("use")) return false;
|
|
7668
|
+
if (name.length === 3) return true;
|
|
7669
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
7670
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7671
|
+
};
|
|
7672
|
+
//#endregion
|
|
7673
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7674
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7675
|
+
//#endregion
|
|
7676
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7677
|
+
const hocWrapperCalleeName = (callee) => {
|
|
7678
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7679
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7680
|
+
return null;
|
|
7681
|
+
};
|
|
7682
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7683
|
+
let current = functionNode;
|
|
7684
|
+
for (;;) {
|
|
7685
|
+
const parent = current.parent;
|
|
7686
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7687
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7688
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7689
|
+
current = parent;
|
|
7690
|
+
continue;
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
break;
|
|
7694
|
+
}
|
|
7695
|
+
const binding = current.parent;
|
|
7696
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7697
|
+
return null;
|
|
7698
|
+
};
|
|
7699
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7700
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7701
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
7702
|
+
};
|
|
7703
|
+
//#endregion
|
|
7704
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7705
|
+
const findEnclosingFunction = (node) => {
|
|
7706
|
+
let cursor = node.parent;
|
|
7707
|
+
while (cursor) {
|
|
7708
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
7709
|
+
cursor = cursor.parent ?? null;
|
|
7710
|
+
}
|
|
7711
|
+
return null;
|
|
7712
|
+
};
|
|
7713
|
+
//#endregion
|
|
7714
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7715
|
+
const enclosingComponentOrHookName = (node) => {
|
|
7716
|
+
const functionNode = findEnclosingFunction(node);
|
|
7717
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7718
|
+
};
|
|
7719
|
+
//#endregion
|
|
7720
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7721
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
7722
|
+
let node = callExpression;
|
|
7723
|
+
let parent = node.parent;
|
|
7724
|
+
while (parent) {
|
|
7725
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7726
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7727
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7728
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7729
|
+
node = parent;
|
|
7730
|
+
parent = node.parent;
|
|
7731
|
+
continue;
|
|
7732
|
+
}
|
|
7733
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7734
|
+
node = parent;
|
|
7735
|
+
parent = node.parent;
|
|
7736
|
+
continue;
|
|
7737
|
+
}
|
|
7738
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7739
|
+
node = parent;
|
|
7740
|
+
parent = node.parent;
|
|
7741
|
+
continue;
|
|
7742
|
+
}
|
|
7743
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7744
|
+
const expressions = parent.expressions ?? [];
|
|
7745
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
7746
|
+
node = parent;
|
|
7747
|
+
parent = node.parent;
|
|
7748
|
+
continue;
|
|
7749
|
+
}
|
|
7750
|
+
return false;
|
|
7751
|
+
}
|
|
7752
|
+
return false;
|
|
7753
|
+
};
|
|
7754
|
+
//#endregion
|
|
7613
7755
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7614
7756
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7615
7757
|
if (!node || typeof node !== "object") return;
|
|
@@ -7761,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7761
7903
|
};
|
|
7762
7904
|
//#endregion
|
|
7763
7905
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7906
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7907
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
7908
|
+
subscribe: "subscription",
|
|
7909
|
+
timer: "timer",
|
|
7910
|
+
socket: "connection"
|
|
7911
|
+
};
|
|
7912
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7913
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
7914
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7915
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7916
|
+
};
|
|
7764
7917
|
const findSubscribeLikeUsages = (callback) => {
|
|
7765
7918
|
const usages = [];
|
|
7766
7919
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7772,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7772
7925
|
}
|
|
7773
7926
|
walkAst(callback, (child) => {
|
|
7774
7927
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7928
|
+
if (isSocketConstruction(child)) {
|
|
7929
|
+
usages.push({
|
|
7930
|
+
kind: "socket",
|
|
7931
|
+
node: child,
|
|
7932
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7933
|
+
});
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7775
7936
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7776
7937
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7777
7938
|
usages.push({
|
|
@@ -7781,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7781
7942
|
});
|
|
7782
7943
|
return;
|
|
7783
7944
|
}
|
|
7784
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7945
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
7785
7946
|
kind: "subscribe",
|
|
7786
7947
|
node: child,
|
|
7787
7948
|
resourceName: child.callee.property.name
|
|
@@ -7797,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7797
7958
|
};
|
|
7798
7959
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7799
7960
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7961
|
+
walkAst(effectCallback.body, (child) => {
|
|
7962
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
7963
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7964
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7965
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7966
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7967
|
+
});
|
|
7800
7968
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7801
7969
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7802
7970
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7804,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7804
7972
|
const bindingName = declarator.id.name;
|
|
7805
7973
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7806
7974
|
const init = declarator.init;
|
|
7807
|
-
if (!init
|
|
7975
|
+
if (!init) continue;
|
|
7976
|
+
if (isSocketConstruction(init)) {
|
|
7977
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7978
|
+
continue;
|
|
7979
|
+
}
|
|
7980
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7808
7981
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7809
7982
|
bindings.subscriptionNames.add(bindingName);
|
|
7810
7983
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7834,6 +8007,22 @@ const getRangeStart = (node) => {
|
|
|
7834
8007
|
const rangeStart = node.range?.[0];
|
|
7835
8008
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7836
8009
|
};
|
|
8010
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8011
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8012
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8013
|
+
const releaseStarts = [];
|
|
8014
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8015
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8016
|
+
const releaseStart = getRangeStart(child);
|
|
8017
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8018
|
+
});
|
|
8019
|
+
if (releaseStarts.length === 0) return usages;
|
|
8020
|
+
return usages.filter((usage) => {
|
|
8021
|
+
const usageStart = getRangeStart(usage.node);
|
|
8022
|
+
if (usageStart === null) return true;
|
|
8023
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8024
|
+
});
|
|
8025
|
+
};
|
|
7837
8026
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7838
8027
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
7839
8028
|
const returnStart = getRangeStart(returnStatement);
|
|
@@ -7856,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7856
8045
|
});
|
|
7857
8046
|
return didFindCleanupReturn;
|
|
7858
8047
|
};
|
|
8048
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8049
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8050
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8051
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8052
|
+
if (keyName === "signal") return true;
|
|
8053
|
+
if (keyName !== "once") return false;
|
|
8054
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8055
|
+
};
|
|
8056
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8057
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8058
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8059
|
+
["addListener", new Set([
|
|
8060
|
+
"removeListener",
|
|
8061
|
+
"off",
|
|
8062
|
+
"abort"
|
|
8063
|
+
])],
|
|
8064
|
+
["on", new Set([
|
|
8065
|
+
"off",
|
|
8066
|
+
"removeListener",
|
|
8067
|
+
"on"
|
|
8068
|
+
])],
|
|
8069
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8070
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8071
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8072
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8073
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8074
|
+
]);
|
|
8075
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8076
|
+
"cleanup",
|
|
8077
|
+
"dispose",
|
|
8078
|
+
"destroy",
|
|
8079
|
+
"teardown"
|
|
8080
|
+
]);
|
|
8081
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8082
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8083
|
+
const getReleaseVerbName = (node) => {
|
|
8084
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8085
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8086
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8087
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8088
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8089
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8090
|
+
return null;
|
|
8091
|
+
};
|
|
8092
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8093
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8094
|
+
let didFindPairedRelease = false;
|
|
8095
|
+
walkAst(body, (child) => {
|
|
8096
|
+
if (didFindPairedRelease) return false;
|
|
8097
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8098
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8099
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8100
|
+
didFindPairedRelease = true;
|
|
8101
|
+
return false;
|
|
8102
|
+
}
|
|
8103
|
+
});
|
|
8104
|
+
return didFindPairedRelease;
|
|
8105
|
+
};
|
|
8106
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8107
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8108
|
+
let programNode = anyNode;
|
|
8109
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8110
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8111
|
+
if (cached) return cached;
|
|
8112
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8113
|
+
walkAst(programNode, (child) => {
|
|
8114
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8115
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8116
|
+
});
|
|
8117
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8118
|
+
return releaseVerbNames;
|
|
8119
|
+
};
|
|
8120
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8121
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8122
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8123
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8124
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8125
|
+
return false;
|
|
8126
|
+
};
|
|
8127
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8128
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8129
|
+
const body = retainedFunction.body;
|
|
8130
|
+
if (!body) return null;
|
|
8131
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8132
|
+
let leak = null;
|
|
8133
|
+
walkAst(body, (child) => {
|
|
8134
|
+
if (leak !== null) return false;
|
|
8135
|
+
if (isFunctionLike$1(child)) return false;
|
|
8136
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8137
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8138
|
+
leak = {
|
|
8139
|
+
kind: "socket",
|
|
8140
|
+
node: child,
|
|
8141
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8142
|
+
};
|
|
8143
|
+
return false;
|
|
8144
|
+
}
|
|
8145
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8146
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8147
|
+
leak = {
|
|
8148
|
+
kind: "timer",
|
|
8149
|
+
node: child,
|
|
8150
|
+
resourceName: "setInterval"
|
|
8151
|
+
};
|
|
8152
|
+
return false;
|
|
8153
|
+
}
|
|
8154
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8155
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8156
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8157
|
+
kind: "subscribe",
|
|
8158
|
+
node: child,
|
|
8159
|
+
resourceName: registrationVerbName
|
|
8160
|
+
};
|
|
8161
|
+
return false;
|
|
8162
|
+
}
|
|
8163
|
+
});
|
|
8164
|
+
return leak;
|
|
8165
|
+
};
|
|
8166
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8167
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8168
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8169
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8170
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8171
|
+
};
|
|
7859
8172
|
const effectNeedsCleanup = defineRule({
|
|
7860
8173
|
id: "effect-needs-cleanup",
|
|
7861
8174
|
title: "Effect subscription or timer never cleaned up",
|
|
7862
8175
|
severity: "error",
|
|
7863
8176
|
tags: ["test-noise"],
|
|
7864
|
-
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
7865
|
-
create: (context) =>
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
8177
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8178
|
+
create: (context) => {
|
|
8179
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8180
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8181
|
+
if (!leak) return;
|
|
8182
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8183
|
+
context.report({
|
|
8184
|
+
node: leak.node,
|
|
8185
|
+
message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
|
|
8186
|
+
});
|
|
8187
|
+
};
|
|
8188
|
+
return {
|
|
8189
|
+
CallExpression(node) {
|
|
8190
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8191
|
+
const retainedCallback = getEffectCallback(node);
|
|
8192
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8196
|
+
const callback = getEffectCallback(node);
|
|
8197
|
+
if (!callback) return;
|
|
8198
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8199
|
+
if (usages.length === 0) return;
|
|
8200
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8201
|
+
const firstUsage = usages[0];
|
|
8202
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8203
|
+
context.report({
|
|
8204
|
+
node,
|
|
8205
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8206
|
+
});
|
|
8207
|
+
},
|
|
8208
|
+
FunctionDeclaration(node) {
|
|
8209
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8210
|
+
},
|
|
8211
|
+
ArrowFunctionExpression(node) {
|
|
8212
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8213
|
+
},
|
|
8214
|
+
FunctionExpression(node) {
|
|
8215
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8216
|
+
}
|
|
8217
|
+
};
|
|
8218
|
+
}
|
|
7879
8219
|
});
|
|
7880
8220
|
//#endregion
|
|
7881
8221
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8434,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8434
8774
|
return computedCaptures;
|
|
8435
8775
|
};
|
|
8436
8776
|
//#endregion
|
|
8437
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8438
|
-
const isReactHookName = (name) => {
|
|
8439
|
-
if (!name.startsWith("use")) return false;
|
|
8440
|
-
if (name.length === 3) return true;
|
|
8441
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
8442
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8443
|
-
};
|
|
8444
|
-
//#endregion
|
|
8445
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8446
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8447
|
-
//#endregion
|
|
8448
8777
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8449
8778
|
const reactHocCalleeName = (callee) => {
|
|
8450
8779
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -9616,7 +9945,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9945
|
});
|
|
9617
9946
|
//#endregion
|
|
9618
9947
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9948
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9949
|
const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
|
|
9621
9950
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9951
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9628,7 +9957,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9957
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
9629
9958
|
create: (context) => {
|
|
9630
9959
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9960
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9961
|
return {
|
|
9633
9962
|
MemberExpression(node) {
|
|
9634
9963
|
if (!node.computed) return;
|
|
@@ -9881,7 +10210,10 @@ const PRAGMA = "React";
|
|
|
9881
10210
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9882
10211
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9883
10212
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9884
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10213
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10214
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10215
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10216
|
+
}
|
|
9885
10217
|
return true;
|
|
9886
10218
|
};
|
|
9887
10219
|
//#endregion
|
|
@@ -11046,8 +11378,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11378
|
if (node.attributes.length === 0) return;
|
|
11047
11379
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11380
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11381
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11382
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11383
|
let hasInteractiveHandler = false;
|
|
11052
11384
|
for (const attribute of node.attributes) {
|
|
11053
11385
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11393,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11393
|
const elementType = getElementType(node, context.settings);
|
|
11062
11394
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11395
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11064
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11065
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11066
11396
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11397
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11398
|
+
for (const role of roleCandidates) {
|
|
11399
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11400
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11401
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11402
|
+
}
|
|
11403
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11404
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11405
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11406
|
context.report({
|
|
11070
11407
|
node,
|
|
11071
11408
|
message
|
|
@@ -11254,16 +11591,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11254
11591
|
return names;
|
|
11255
11592
|
};
|
|
11256
11593
|
//#endregion
|
|
11257
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11258
|
-
const findEnclosingFunction = (node) => {
|
|
11259
|
-
let cursor = node.parent;
|
|
11260
|
-
while (cursor) {
|
|
11261
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
11262
|
-
cursor = cursor.parent ?? null;
|
|
11263
|
-
}
|
|
11264
|
-
return null;
|
|
11265
|
-
};
|
|
11266
|
-
//#endregion
|
|
11267
11594
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11268
11595
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11269
11596
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -11963,14 +12290,15 @@ const jsCacheStorage = defineRule({
|
|
|
11963
12290
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
11964
12291
|
CallExpression(node) {
|
|
11965
12292
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
11966
|
-
|
|
12293
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12294
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11967
12295
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
11968
12296
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
11969
12297
|
const storageKey = String(node.arguments[0].value);
|
|
11970
12298
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
11971
12299
|
storageReadCounts.set(storageKey, readCount);
|
|
11972
12300
|
if (readCount === 2) {
|
|
11973
|
-
const storageName =
|
|
12301
|
+
const storageName = receiver.name;
|
|
11974
12302
|
context.report({
|
|
11975
12303
|
node,
|
|
11976
12304
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -11984,13 +12312,16 @@ const jsCacheStorage = defineRule({
|
|
|
11984
12312
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
11985
12313
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
11986
12314
|
const callee = callExpression.callee;
|
|
11987
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
12315
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12316
|
+
const receiver = stripParenExpression(callee.object);
|
|
12317
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12318
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12319
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12320
|
+
return true;
|
|
12321
|
+
}
|
|
12322
|
+
return false;
|
|
11993
12323
|
}
|
|
12324
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
11994
12325
|
return false;
|
|
11995
12326
|
};
|
|
11996
12327
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12002,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12002
12333
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12003
12334
|
let cursor = receiverNode;
|
|
12004
12335
|
while (cursor) {
|
|
12005
|
-
|
|
12006
|
-
cursor = cursor.expression;
|
|
12007
|
-
continue;
|
|
12008
|
-
}
|
|
12336
|
+
cursor = stripParenExpression(cursor);
|
|
12009
12337
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12010
12338
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12011
12339
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12081,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12081
12409
|
let hops = 0;
|
|
12082
12410
|
while (cursor && hops < 12) {
|
|
12083
12411
|
hops += 1;
|
|
12084
|
-
|
|
12085
|
-
cursor = cursor.expression;
|
|
12086
|
-
continue;
|
|
12087
|
-
}
|
|
12412
|
+
cursor = stripParenExpression(cursor);
|
|
12088
12413
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12089
12414
|
const callee = cursor.callee;
|
|
12090
12415
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12108,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12108
12433
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12109
12434
|
let cursor = receiverNode;
|
|
12110
12435
|
while (cursor) {
|
|
12111
|
-
|
|
12112
|
-
cursor = cursor.expression;
|
|
12113
|
-
continue;
|
|
12114
|
-
}
|
|
12436
|
+
cursor = stripParenExpression(cursor);
|
|
12115
12437
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12116
12438
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12117
12439
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12176,7 +12498,7 @@ const jsCombineIterations = defineRule({
|
|
|
12176
12498
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12177
12499
|
const outerMethod = node.callee.property.name;
|
|
12178
12500
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12179
|
-
const innerCall = node.callee.object;
|
|
12501
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12180
12502
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12181
12503
|
const innerMethod = innerCall.callee.property.name;
|
|
12182
12504
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12249,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12249
12571
|
if (!filterArgument) return;
|
|
12250
12572
|
const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
|
|
12251
12573
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12252
|
-
const innerCall = node.callee.object;
|
|
12574
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12253
12575
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12254
12576
|
if (innerCall.callee.property.name !== "map") return;
|
|
12255
|
-
const receiver = innerCall.callee.object;
|
|
12577
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12256
12578
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12257
12579
|
const elements = receiver.elements ?? [];
|
|
12258
12580
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13512,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13512
13834
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13513
13835
|
create: (context) => ({ CallExpression(node) {
|
|
13514
13836
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13515
|
-
const receiver = node.callee.object;
|
|
13837
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13516
13838
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13517
13839
|
const spreadArgument = receiver.elements[0].argument;
|
|
13518
13840
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18549,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18549
18871
|
};
|
|
18550
18872
|
const describeClientSideNavigation = (node) => {
|
|
18551
18873
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18552
|
-
const
|
|
18874
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18875
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18553
18876
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18554
18877
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18555
18878
|
}
|
|
@@ -19169,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19169
19492
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19170
19493
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19171
19494
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19172
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19495
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19173
19496
|
return node.callee.property.name;
|
|
19174
19497
|
};
|
|
19175
19498
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19183,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19183
19506
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19184
19507
|
const { property, object } = node.callee;
|
|
19185
19508
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19186
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19509
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19187
19510
|
return true;
|
|
19188
19511
|
};
|
|
19189
19512
|
const getDbCallDescription = (node) => {
|
|
@@ -19191,7 +19514,8 @@ const getDbCallDescription = (node) => {
|
|
|
19191
19514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19192
19515
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19193
19516
|
const methodName = node.callee.property.name;
|
|
19194
|
-
const
|
|
19517
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19518
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19195
19519
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19196
19520
|
};
|
|
19197
19521
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20591,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20591
20915
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20592
20916
|
let node = ref.identifier;
|
|
20593
20917
|
let parent = node.parent;
|
|
20594
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20918
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20595
20919
|
node = parent;
|
|
20596
20920
|
parent = node.parent;
|
|
20597
20921
|
}
|
|
20598
20922
|
if (current.callee === node) return current;
|
|
20599
20923
|
}
|
|
20600
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20924
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20601
20925
|
return null;
|
|
20602
20926
|
};
|
|
20603
20927
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20732,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20732
21056
|
const importDeclaration = declarationNode.parent;
|
|
20733
21057
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20734
21058
|
}));
|
|
20735
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21059
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20736
21060
|
if (!node) return false;
|
|
20737
21061
|
if (isNodeOfType(node, "Identifier")) {
|
|
20738
21062
|
if (node.name === hookName) return true;
|
|
@@ -20741,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20741
21065
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20742
21066
|
return false;
|
|
20743
21067
|
}
|
|
20744
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21068
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21069
|
+
const receiver = stripParenExpression(node.object);
|
|
21070
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21071
|
+
}
|
|
20745
21072
|
return false;
|
|
20746
21073
|
};
|
|
20747
21074
|
const isUseEffect = (node) => {
|
|
20748
21075
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20749
21076
|
const callee = node.callee;
|
|
20750
21077
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20751
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20752
|
-
|
|
21078
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21079
|
+
const receiver = stripParenExpression(callee.object);
|
|
21080
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20753
21081
|
};
|
|
20754
21082
|
const getEffectFn = (analysis, node) => {
|
|
20755
21083
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20777,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20777
21105
|
const node = def.node;
|
|
20778
21106
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20779
21107
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20780
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21108
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20781
21109
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20782
21110
|
const elements = node.id.elements ?? [];
|
|
20783
21111
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20788,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20788
21116
|
const node = def.node;
|
|
20789
21117
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20790
21118
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20791
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21119
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20792
21120
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20793
21121
|
const elements = node.id.elements ?? [];
|
|
20794
21122
|
if (elements.length !== 2) return false;
|
|
@@ -20835,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20835
21163
|
const node = def.node;
|
|
20836
21164
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20837
21165
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20838
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21166
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20839
21167
|
}));
|
|
20840
21168
|
const isRefCurrent = (ref) => {
|
|
20841
21169
|
const parent = ref.identifier.parent;
|
|
@@ -20848,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20848
21176
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20849
21177
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20850
21178
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20851
|
-
|
|
20852
|
-
|
|
21179
|
+
let effectiveNode = ref.identifier;
|
|
21180
|
+
let parent = effectiveNode.parent;
|
|
21181
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21182
|
+
effectiveNode = parent;
|
|
21183
|
+
parent = effectiveNode.parent;
|
|
21184
|
+
}
|
|
20853
21185
|
if (!parent) return false;
|
|
20854
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20855
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21186
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21187
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20856
21188
|
const memberParent = parent.parent;
|
|
20857
21189
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20858
21190
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20863,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20863
21195
|
};
|
|
20864
21196
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20865
21197
|
const getUseStateDecl = (analysis, ref) => {
|
|
20866
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21198
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20867
21199
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20868
21200
|
return node ?? null;
|
|
20869
21201
|
};
|
|
@@ -21068,7 +21400,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21068
21400
|
const noAdjustStateOnPropChange = defineRule({
|
|
21069
21401
|
id: "no-adjust-state-on-prop-change",
|
|
21070
21402
|
title: "State synced to a prop inside an effect",
|
|
21071
|
-
severity: "
|
|
21403
|
+
severity: "warn",
|
|
21072
21404
|
tags: ["test-noise"],
|
|
21073
21405
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
21074
21406
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21109,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21109
21441
|
"summary",
|
|
21110
21442
|
"textarea"
|
|
21111
21443
|
]);
|
|
21444
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21445
|
+
const value = attribute.value;
|
|
21446
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21447
|
+
const expression = value.expression;
|
|
21448
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21449
|
+
};
|
|
21450
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21451
|
+
"button",
|
|
21452
|
+
"input",
|
|
21453
|
+
"select",
|
|
21454
|
+
"textarea"
|
|
21455
|
+
]);
|
|
21112
21456
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21457
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21458
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21459
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21460
|
+
}
|
|
21113
21461
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21114
21462
|
switch (tagName) {
|
|
21115
21463
|
case "input": {
|
|
@@ -21123,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21123
21471
|
case "a":
|
|
21124
21472
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21125
21473
|
case "audio":
|
|
21126
|
-
case "video":
|
|
21474
|
+
case "video": {
|
|
21475
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21476
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21477
|
+
}
|
|
21127
21478
|
default: return false;
|
|
21128
21479
|
}
|
|
21129
21480
|
};
|
|
@@ -22087,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22087
22438
|
"some"
|
|
22088
22439
|
]);
|
|
22089
22440
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22090
|
-
const isPositionallyStableIterationReceiver = (
|
|
22441
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22442
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22091
22443
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22092
22444
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22093
22445
|
const only = receiver.elements[0];
|
|
@@ -22098,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22098
22450
|
}
|
|
22099
22451
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22100
22452
|
const callee = receiver.callee;
|
|
22101
|
-
if (
|
|
22453
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22102
22454
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22103
22455
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22104
22456
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22105
22457
|
return false;
|
|
22106
22458
|
};
|
|
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
|
-
};
|
|
22459
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22112
22460
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22113
22461
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22114
22462
|
for (const property of source.properties ?? []) {
|
|
@@ -22167,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22167
22515
|
return false;
|
|
22168
22516
|
}
|
|
22169
22517
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22170
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22518
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22171
22519
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22172
22520
|
}
|
|
22173
22521
|
return false;
|
|
22174
22522
|
};
|
|
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
|
-
};
|
|
22523
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22182
22524
|
const noArrayIndexKey = defineRule({
|
|
22183
22525
|
id: "no-array-index-key",
|
|
22184
22526
|
title: "Array index used as a key",
|
|
@@ -22477,6 +22819,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22477
22819
|
const section = manifest[sectionName];
|
|
22478
22820
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22479
22821
|
});
|
|
22822
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22823
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22824
|
+
return false;
|
|
22825
|
+
};
|
|
22480
22826
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22481
22827
|
const classifyPackagePlatform = (filename) => {
|
|
22482
22828
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22493,9 +22839,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22493
22839
|
return result;
|
|
22494
22840
|
};
|
|
22495
22841
|
//#endregion
|
|
22496
|
-
//#region src/plugin/utils/is-
|
|
22497
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22842
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22499
22843
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22500
22844
|
const resolveRealDirectory = (directory) => {
|
|
22501
22845
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22516,6 +22860,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22516
22860
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22517
22861
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22518
22862
|
};
|
|
22863
|
+
//#endregion
|
|
22864
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22865
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22866
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22519
22867
|
const classifyReactNativeFileTarget = (context) => {
|
|
22520
22868
|
const rawFilename = context.filename;
|
|
22521
22869
|
if (!rawFilename) return "unknown";
|
|
@@ -22896,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22896
23244
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22897
23245
|
return false;
|
|
22898
23246
|
};
|
|
22899
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23247
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22900
23248
|
"forEach",
|
|
22901
23249
|
"map",
|
|
22902
23250
|
"filter",
|
|
@@ -22917,31 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22917
23265
|
if (parent.callee === functionNode) return true;
|
|
22918
23266
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22919
23267
|
const callee = parent.callee;
|
|
22920
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23268
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22921
23269
|
};
|
|
22922
23270
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
22923
|
-
const
|
|
22924
|
-
|
|
22925
|
-
if (statement.alternate) return null;
|
|
22926
|
-
const consequent = statement.consequent;
|
|
22927
|
-
if (isTerminatingStatement(consequent)) return consequent;
|
|
22928
|
-
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
22929
|
-
return null;
|
|
22930
|
-
};
|
|
22931
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23271
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23272
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22932
23273
|
let fallThroughCount = 0;
|
|
22933
|
-
let
|
|
23274
|
+
let maxTerminatedCount = 0;
|
|
22934
23275
|
for (const statement of statements) {
|
|
22935
|
-
if (
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
23276
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23277
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23278
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23279
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23280
|
+
fallThroughCount: 0,
|
|
23281
|
+
maxTerminatedCount: 0,
|
|
23282
|
+
doAllPathsTerminate: false
|
|
23283
|
+
};
|
|
23284
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23285
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23286
|
+
fallThroughCount: 0,
|
|
23287
|
+
maxTerminatedCount,
|
|
23288
|
+
doAllPathsTerminate: true
|
|
23289
|
+
};
|
|
23290
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23291
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22939
23292
|
continue;
|
|
22940
23293
|
}
|
|
22941
|
-
if (isTerminatingStatement(statement))
|
|
23294
|
+
if (isTerminatingStatement(statement)) {
|
|
23295
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23296
|
+
return {
|
|
23297
|
+
fallThroughCount: 0,
|
|
23298
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23299
|
+
doAllPathsTerminate: true
|
|
23300
|
+
};
|
|
23301
|
+
}
|
|
22942
23302
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22943
23303
|
}
|
|
22944
|
-
return
|
|
23304
|
+
return {
|
|
23305
|
+
fallThroughCount,
|
|
23306
|
+
maxTerminatedCount,
|
|
23307
|
+
doAllPathsTerminate: false
|
|
23308
|
+
};
|
|
23309
|
+
};
|
|
23310
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23311
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23312
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22945
23313
|
};
|
|
22946
23314
|
const collectLocalHelperFunctions = (root) => {
|
|
22947
23315
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -22968,14 +23336,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22968
23336
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22969
23337
|
return parent.parent === effectCallback.body;
|
|
22970
23338
|
};
|
|
23339
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23340
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23341
|
+
const body = functionNode.body;
|
|
23342
|
+
if (!body) return 0;
|
|
23343
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23344
|
+
};
|
|
22971
23345
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22972
23346
|
if (!node || typeof node !== "object") return 0;
|
|
22973
|
-
if (
|
|
23347
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22974
23348
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22975
23349
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22976
23350
|
const consequent = node.consequent;
|
|
22977
23351
|
const alternate = node.alternate;
|
|
22978
|
-
|
|
23352
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23353
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23354
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23355
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22979
23356
|
}
|
|
22980
23357
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22981
23358
|
let maxRunSetters = 0;
|
|
@@ -23005,28 +23382,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23005
23382
|
}
|
|
23006
23383
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23007
23384
|
let nestedSettersInArgs = 0;
|
|
23008
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23385
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23009
23386
|
return 1 + nestedSettersInArgs;
|
|
23010
23387
|
}
|
|
23011
23388
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23012
23389
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23013
23390
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23014
23391
|
context.activeHelpers.add(helperFunction);
|
|
23015
|
-
let helperCount =
|
|
23392
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23016
23393
|
context.activeHelpers.delete(helperFunction);
|
|
23017
23394
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23018
23395
|
return helperCount;
|
|
23019
23396
|
}
|
|
23020
23397
|
}
|
|
23021
|
-
const
|
|
23398
|
+
const countChild = (child) => {
|
|
23399
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23400
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23401
|
+
};
|
|
23022
23402
|
let total = 0;
|
|
23023
23403
|
const nodeRecord = node;
|
|
23024
23404
|
for (const key of Object.keys(nodeRecord)) {
|
|
23025
23405
|
if (key === "parent") continue;
|
|
23026
23406
|
const child = nodeRecord[key];
|
|
23027
23407
|
if (Array.isArray(child)) {
|
|
23028
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item
|
|
23029
|
-
} else if (child && typeof child === "object" && "type" in child
|
|
23408
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23409
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23030
23410
|
}
|
|
23031
23411
|
return total;
|
|
23032
23412
|
};
|
|
@@ -23060,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23060
23440
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23061
23441
|
const firstStatement = (body.body ?? [])[0];
|
|
23062
23442
|
if (!firstStatement) return false;
|
|
23063
|
-
if (!
|
|
23443
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23444
|
+
const consequent = firstStatement.consequent;
|
|
23445
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23064
23446
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23065
23447
|
};
|
|
23066
23448
|
const noCascadingSetState = defineRule({
|
|
@@ -23075,7 +23457,7 @@ const noCascadingSetState = defineRule({
|
|
|
23075
23457
|
const callback = getEffectCallback(node);
|
|
23076
23458
|
if (!callback) return;
|
|
23077
23459
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23078
|
-
const setStateCallCount =
|
|
23460
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23079
23461
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23080
23462
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23081
23463
|
effectCallback: callback
|
|
@@ -23419,40 +23801,6 @@ const noCloneElement = defineRule({
|
|
|
23419
23801
|
} })
|
|
23420
23802
|
});
|
|
23421
23803
|
//#endregion
|
|
23422
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23423
|
-
const hocWrapperCalleeName = (callee) => {
|
|
23424
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23425
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23426
|
-
return null;
|
|
23427
|
-
};
|
|
23428
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23429
|
-
let current = functionNode;
|
|
23430
|
-
for (;;) {
|
|
23431
|
-
const parent = current.parent;
|
|
23432
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23433
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23434
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23435
|
-
current = parent;
|
|
23436
|
-
continue;
|
|
23437
|
-
}
|
|
23438
|
-
}
|
|
23439
|
-
break;
|
|
23440
|
-
}
|
|
23441
|
-
const binding = current.parent;
|
|
23442
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23443
|
-
return null;
|
|
23444
|
-
};
|
|
23445
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23446
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23447
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
23448
|
-
};
|
|
23449
|
-
//#endregion
|
|
23450
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23451
|
-
const enclosingComponentOrHookName = (node) => {
|
|
23452
|
-
const functionNode = findEnclosingFunction(node);
|
|
23453
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23454
|
-
};
|
|
23455
|
-
//#endregion
|
|
23456
23804
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23457
23805
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23458
23806
|
const CONTEXT_MODULES = [
|
|
@@ -24423,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24423
24771
|
return isInitialOnlyPropName(arg.name);
|
|
24424
24772
|
};
|
|
24425
24773
|
//#endregion
|
|
24774
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24775
|
+
const isNoOpStatement = (statement) => {
|
|
24776
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24777
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24778
|
+
const expression = stripParenExpression(statement.expression);
|
|
24779
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24780
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24781
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24782
|
+
return false;
|
|
24783
|
+
};
|
|
24784
|
+
//#endregion
|
|
24426
24785
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24427
24786
|
const getCallbackStatements = (callback) => {
|
|
24428
24787
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24429
|
-
|
|
24430
|
-
return callback.body ? [callback.body] : [];
|
|
24788
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24431
24789
|
};
|
|
24432
24790
|
//#endregion
|
|
24433
24791
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24466,6 +24824,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24466
24824
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24467
24825
|
}
|
|
24468
24826
|
};
|
|
24827
|
+
const flattenGuardedStatements = (statements) => {
|
|
24828
|
+
const flattened = [];
|
|
24829
|
+
for (const statement of statements) {
|
|
24830
|
+
if (isNoOpStatement(statement)) continue;
|
|
24831
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24832
|
+
flattened.push(statement);
|
|
24833
|
+
continue;
|
|
24834
|
+
}
|
|
24835
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24836
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24837
|
+
if (!branch) continue;
|
|
24838
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24839
|
+
if (flattenedBranch === null) return null;
|
|
24840
|
+
flattened.push(...flattenedBranch);
|
|
24841
|
+
}
|
|
24842
|
+
continue;
|
|
24843
|
+
}
|
|
24844
|
+
return null;
|
|
24845
|
+
}
|
|
24846
|
+
return flattened;
|
|
24847
|
+
};
|
|
24469
24848
|
const noDerivedStateEffect = defineRule({
|
|
24470
24849
|
id: "no-derived-state-effect",
|
|
24471
24850
|
title: "Derived state stored in an effect",
|
|
@@ -24497,8 +24876,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24497
24876
|
}
|
|
24498
24877
|
}
|
|
24499
24878
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24500
|
-
const statements = getCallbackStatements(callback);
|
|
24501
|
-
if (statements.length === 0) return;
|
|
24879
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24880
|
+
if (statements === null || statements.length === 0) return;
|
|
24502
24881
|
if (!statements.every((statement) => {
|
|
24503
24882
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24504
24883
|
const expression = statement.expression;
|
|
@@ -25133,7 +25512,7 @@ const noDidMountSetState = defineRule({
|
|
|
25133
25512
|
const { mode } = resolveSettings$20(context.settings);
|
|
25134
25513
|
return { CallExpression(node) {
|
|
25135
25514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25136
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25515
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25137
25516
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25138
25517
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25139
25518
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25258,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25258
25637
|
const { mode } = resolveSettings$19(context.settings);
|
|
25259
25638
|
return { CallExpression(node) {
|
|
25260
25639
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25261
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25640
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25262
25641
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25263
25642
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25264
25643
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25579,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25579
25958
|
create: (context) => ({ CallExpression(node) {
|
|
25580
25959
|
const callee = node.callee;
|
|
25581
25960
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25582
|
-
|
|
25961
|
+
const receiver = stripParenExpression(callee.object);
|
|
25962
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25583
25963
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25584
|
-
if (context.scopes.symbolFor(
|
|
25964
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25585
25965
|
if (!importsReactViewTransition(node)) return;
|
|
25586
25966
|
context.report({
|
|
25587
25967
|
node,
|
|
@@ -25601,7 +25981,8 @@ const noDocumentWrite = defineRule({
|
|
|
25601
25981
|
create: (context) => ({ CallExpression(node) {
|
|
25602
25982
|
const callee = node.callee;
|
|
25603
25983
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25604
|
-
|
|
25984
|
+
const receiver = stripParenExpression(callee.object);
|
|
25985
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25605
25986
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25606
25987
|
context.report({
|
|
25607
25988
|
node,
|
|
@@ -26293,7 +26674,12 @@ const noEffectEventInDeps = defineRule({
|
|
|
26293
26674
|
const initializer = declaratorNode.init;
|
|
26294
26675
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26295
26676
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26296
|
-
if (isNodeOfType(initializer.callee, "Identifier")
|
|
26677
|
+
if (isNodeOfType(initializer.callee, "Identifier")) {
|
|
26678
|
+
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26679
|
+
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26680
|
+
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26681
|
+
}
|
|
26682
|
+
if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
|
|
26297
26683
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26298
26684
|
} });
|
|
26299
26685
|
return {
|
|
@@ -28873,7 +29259,7 @@ const noIsMounted = defineRule({
|
|
|
28873
29259
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28874
29260
|
create: (context) => ({ CallExpression(node) {
|
|
28875
29261
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28876
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29262
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28877
29263
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28878
29264
|
if (!getParentComponent(node)) return;
|
|
28879
29265
|
context.report({
|
|
@@ -28888,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28888
29274
|
const isJsonMethodCall = (node, method) => {
|
|
28889
29275
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28890
29276
|
const callee = node.callee;
|
|
28891
|
-
|
|
29277
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29278
|
+
const receiver = stripParenExpression(callee.object);
|
|
29279
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28892
29280
|
};
|
|
28893
29281
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28894
29282
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -28973,7 +29361,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
28973
29361
|
const noJsxElementType = defineRule({
|
|
28974
29362
|
id: "no-jsx-element-type",
|
|
28975
29363
|
title: "No JSX.Element",
|
|
28976
|
-
severity: "
|
|
29364
|
+
severity: "warn",
|
|
28977
29365
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
28978
29366
|
create: (context) => {
|
|
28979
29367
|
let isJsxImported = false;
|
|
@@ -29299,6 +29687,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29299
29687
|
}
|
|
29300
29688
|
});
|
|
29301
29689
|
//#endregion
|
|
29690
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29691
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29692
|
+
"map",
|
|
29693
|
+
"filter",
|
|
29694
|
+
"forEach",
|
|
29695
|
+
"flatMap",
|
|
29696
|
+
"reduce",
|
|
29697
|
+
"reduceRight",
|
|
29698
|
+
"some",
|
|
29699
|
+
"every",
|
|
29700
|
+
"find",
|
|
29701
|
+
"findIndex",
|
|
29702
|
+
"findLast",
|
|
29703
|
+
"findLastIndex",
|
|
29704
|
+
"sort",
|
|
29705
|
+
"toSorted"
|
|
29706
|
+
]);
|
|
29707
|
+
const executesDuringRender = (functionNode) => {
|
|
29708
|
+
const parent = functionNode.parent;
|
|
29709
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29710
|
+
if (parent.callee === functionNode) return true;
|
|
29711
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29712
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
29713
|
+
};
|
|
29714
|
+
//#endregion
|
|
29715
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29716
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29717
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29718
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29719
|
+
return false;
|
|
29720
|
+
};
|
|
29721
|
+
//#endregion
|
|
29722
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29723
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29724
|
+
let ancestor = bindingIdentifier.parent;
|
|
29725
|
+
while (ancestor) {
|
|
29726
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29727
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29728
|
+
ancestor = ancestor.parent ?? null;
|
|
29729
|
+
}
|
|
29730
|
+
return null;
|
|
29731
|
+
};
|
|
29732
|
+
//#endregion
|
|
29733
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29734
|
+
const isFalsyLiteral = (node) => {
|
|
29735
|
+
if (!node) return true;
|
|
29736
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29737
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29738
|
+
};
|
|
29739
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29740
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29741
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29742
|
+
if (!binding) return false;
|
|
29743
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29744
|
+
if (!declarator?.init) return false;
|
|
29745
|
+
const init = stripParenExpression(declarator.init);
|
|
29746
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29747
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29748
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29749
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29750
|
+
};
|
|
29751
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29752
|
+
//#endregion
|
|
29753
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29754
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29755
|
+
let cursor = node;
|
|
29756
|
+
let parent = node.parent;
|
|
29757
|
+
while (parent) {
|
|
29758
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29759
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29760
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29761
|
+
cursor = parent;
|
|
29762
|
+
parent = parent.parent ?? null;
|
|
29763
|
+
}
|
|
29764
|
+
return false;
|
|
29765
|
+
};
|
|
29766
|
+
//#endregion
|
|
29767
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29768
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29769
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29770
|
+
const unwrapped = stripParenExpression(expression);
|
|
29771
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29772
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29773
|
+
const property = unwrapped.property;
|
|
29774
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29775
|
+
}
|
|
29776
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29777
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29778
|
+
return false;
|
|
29779
|
+
};
|
|
29780
|
+
//#endregion
|
|
29781
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29782
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29783
|
+
let cursor = node;
|
|
29784
|
+
let parent = node.parent;
|
|
29785
|
+
while (parent) {
|
|
29786
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29787
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29788
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29789
|
+
cursor = parent;
|
|
29790
|
+
parent = parent.parent ?? null;
|
|
29791
|
+
}
|
|
29792
|
+
return false;
|
|
29793
|
+
};
|
|
29794
|
+
//#endregion
|
|
29795
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29796
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29797
|
+
"toLocaleString",
|
|
29798
|
+
"toLocaleDateString",
|
|
29799
|
+
"toLocaleTimeString"
|
|
29800
|
+
]);
|
|
29801
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29802
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29803
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29804
|
+
"format",
|
|
29805
|
+
"formatToParts",
|
|
29806
|
+
"formatRange"
|
|
29807
|
+
]);
|
|
29808
|
+
const isProvableDateExpression = (expression) => {
|
|
29809
|
+
if (!expression) return false;
|
|
29810
|
+
const unwrapped = stripParenExpression(expression);
|
|
29811
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29812
|
+
};
|
|
29813
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29814
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29815
|
+
if (!expression) return false;
|
|
29816
|
+
const unwrapped = stripParenExpression(expression);
|
|
29817
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29818
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29819
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29820
|
+
return false;
|
|
29821
|
+
};
|
|
29822
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29823
|
+
if (!objectExpression) return false;
|
|
29824
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29825
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29826
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29827
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29828
|
+
if (property.computed) continue;
|
|
29829
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29830
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29831
|
+
}
|
|
29832
|
+
return false;
|
|
29833
|
+
};
|
|
29834
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29835
|
+
if (!argument) return false;
|
|
29836
|
+
const unwrapped = stripParenExpression(argument);
|
|
29837
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29838
|
+
return true;
|
|
29839
|
+
};
|
|
29840
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29841
|
+
const localeArgument = call.arguments?.[0];
|
|
29842
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29843
|
+
const optionsArgument = call.arguments?.[1];
|
|
29844
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29845
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29846
|
+
};
|
|
29847
|
+
const matchLocaleMethodCall = (call) => {
|
|
29848
|
+
const callee = call.callee;
|
|
29849
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29850
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29851
|
+
const methodName = callee.property.name;
|
|
29852
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29853
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29854
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29855
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29856
|
+
return {
|
|
29857
|
+
node: call,
|
|
29858
|
+
display: `${methodName}()`
|
|
29859
|
+
};
|
|
29860
|
+
};
|
|
29861
|
+
const getIntlFormatterName = (expression) => {
|
|
29862
|
+
if (!expression) return null;
|
|
29863
|
+
const unwrapped = stripParenExpression(expression);
|
|
29864
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29865
|
+
const callee = unwrapped.callee;
|
|
29866
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29867
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29868
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29869
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29870
|
+
};
|
|
29871
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29872
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29873
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29874
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29875
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29876
|
+
};
|
|
29877
|
+
const matchIntlFormatCall = (call) => {
|
|
29878
|
+
const callee = call.callee;
|
|
29879
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29880
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29881
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29882
|
+
let construction = stripParenExpression(callee.object);
|
|
29883
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29884
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29885
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29886
|
+
}
|
|
29887
|
+
if (!construction) return null;
|
|
29888
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29889
|
+
if (!formatterName) return null;
|
|
29890
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29891
|
+
return {
|
|
29892
|
+
node: call,
|
|
29893
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29894
|
+
};
|
|
29895
|
+
};
|
|
29896
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29897
|
+
if (!expression) return false;
|
|
29898
|
+
const unwrapped = stripParenExpression(expression);
|
|
29899
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29900
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29901
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29902
|
+
};
|
|
29903
|
+
const matchDateDefaultStringification = (node) => {
|
|
29904
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29905
|
+
const callee = node.callee;
|
|
29906
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29907
|
+
node,
|
|
29908
|
+
display: "Date.prototype.toString()"
|
|
29909
|
+
};
|
|
29910
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29911
|
+
node,
|
|
29912
|
+
display: "String(new Date(…))"
|
|
29913
|
+
};
|
|
29914
|
+
return null;
|
|
29915
|
+
}
|
|
29916
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29917
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29918
|
+
node: expression,
|
|
29919
|
+
display: "`${new Date(…)}`"
|
|
29920
|
+
};
|
|
29921
|
+
}
|
|
29922
|
+
return null;
|
|
29923
|
+
};
|
|
29924
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
29925
|
+
let functionNode = findEnclosingFunction(node);
|
|
29926
|
+
while (functionNode) {
|
|
29927
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29928
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
29929
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
29930
|
+
}
|
|
29931
|
+
return null;
|
|
29932
|
+
};
|
|
29933
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29934
|
+
if (fileHasUseClientDirective) return true;
|
|
29935
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29936
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
29937
|
+
let callsHook = false;
|
|
29938
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29939
|
+
if (callsHook) return false;
|
|
29940
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29941
|
+
callsHook = true;
|
|
29942
|
+
return false;
|
|
29943
|
+
}
|
|
29944
|
+
});
|
|
29945
|
+
return callsHook;
|
|
29946
|
+
};
|
|
29947
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
29948
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29949
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29950
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
29951
|
+
let cursor = node;
|
|
29952
|
+
while (cursor) {
|
|
29953
|
+
ancestors.add(cursor);
|
|
29954
|
+
cursor = cursor.parent ?? null;
|
|
29955
|
+
}
|
|
29956
|
+
for (const statement of body.body ?? []) {
|
|
29957
|
+
if (ancestors.has(statement)) return false;
|
|
29958
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29959
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
29960
|
+
let returnsEarly = false;
|
|
29961
|
+
walkAst(statement.consequent, (child) => {
|
|
29962
|
+
if (isFunctionLike$1(child)) return false;
|
|
29963
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
29964
|
+
returnsEarly = true;
|
|
29965
|
+
return false;
|
|
29966
|
+
}
|
|
29967
|
+
});
|
|
29968
|
+
if (returnsEarly) return true;
|
|
29969
|
+
}
|
|
29970
|
+
return false;
|
|
29971
|
+
};
|
|
29972
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
29973
|
+
let cursor = node.parent;
|
|
29974
|
+
while (cursor) {
|
|
29975
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
29976
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
29977
|
+
cursor = cursor.parent ?? null;
|
|
29978
|
+
}
|
|
29979
|
+
return null;
|
|
29980
|
+
};
|
|
29981
|
+
const noLocaleFormatInRender = defineRule({
|
|
29982
|
+
id: "no-locale-format-in-render",
|
|
29983
|
+
title: "Locale/timezone formatting during render",
|
|
29984
|
+
severity: "warn",
|
|
29985
|
+
category: "Correctness",
|
|
29986
|
+
disabledWhen: [
|
|
29987
|
+
"vite",
|
|
29988
|
+
"cra",
|
|
29989
|
+
"expo",
|
|
29990
|
+
"react-native",
|
|
29991
|
+
"unknown"
|
|
29992
|
+
],
|
|
29993
|
+
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
29994
|
+
create: (context) => {
|
|
29995
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
29996
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
29997
|
+
let fileHasUseClientDirective = false;
|
|
29998
|
+
let fileIsEmailTemplate = false;
|
|
29999
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30000
|
+
const reportIfRenderPhase = (match) => {
|
|
30001
|
+
if (reportedNodes.has(match.node)) return;
|
|
30002
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30003
|
+
if (!componentOrHookNode) return;
|
|
30004
|
+
if (fileIsEmailTemplate) return;
|
|
30005
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30006
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30007
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30008
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30009
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30010
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30011
|
+
reportedNodes.add(match.node);
|
|
30012
|
+
context.report({
|
|
30013
|
+
node: match.node,
|
|
30014
|
+
message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30015
|
+
});
|
|
30016
|
+
};
|
|
30017
|
+
return {
|
|
30018
|
+
Program(node) {
|
|
30019
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30020
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30021
|
+
},
|
|
30022
|
+
CallExpression(node) {
|
|
30023
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30024
|
+
if (match) reportIfRenderPhase(match);
|
|
30025
|
+
},
|
|
30026
|
+
TemplateLiteral(node) {
|
|
30027
|
+
const match = matchDateDefaultStringification(node);
|
|
30028
|
+
if (match) reportIfRenderPhase(match);
|
|
30029
|
+
},
|
|
30030
|
+
JSXExpressionContainer(node) {
|
|
30031
|
+
const expression = stripParenExpression(node.expression);
|
|
30032
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30033
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30034
|
+
const helperName = expression.callee.name;
|
|
30035
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30036
|
+
if (!componentOrHookNode) return;
|
|
30037
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30038
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30039
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30040
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30041
|
+
if (isFunctionLike$1(child)) return false;
|
|
30042
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30043
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30044
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30045
|
+
if (fileIsEmailTemplate) return;
|
|
30046
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30047
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30048
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30049
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30050
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30051
|
+
reportedNodes.add(match.node);
|
|
30052
|
+
context.report({
|
|
30053
|
+
node: match.node,
|
|
30054
|
+
message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30055
|
+
});
|
|
30056
|
+
});
|
|
30057
|
+
}
|
|
30058
|
+
};
|
|
30059
|
+
}
|
|
30060
|
+
});
|
|
30061
|
+
//#endregion
|
|
29302
30062
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29303
30063
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29304
30064
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30112,7 +30872,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30112
30872
|
"reverse",
|
|
30113
30873
|
"sort"
|
|
30114
30874
|
]);
|
|
30115
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30116
30875
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30117
30876
|
"assign",
|
|
30118
30877
|
"defineProperties",
|
|
@@ -30186,7 +30945,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30186
30945
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30187
30946
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30188
30947
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30189
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30190
30948
|
}
|
|
30191
30949
|
}
|
|
30192
30950
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30225,6 +30983,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30225
30983
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30226
30984
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30227
30985
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30986
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30228
30987
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30229
30988
|
});
|
|
30230
30989
|
return mutations;
|
|
@@ -30457,7 +31216,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30457
31216
|
id: "no-nested-component-definition",
|
|
30458
31217
|
title: "Component defined inside another component",
|
|
30459
31218
|
tags: ["test-noise", "react-jsx-only"],
|
|
30460
|
-
severity: "
|
|
31219
|
+
severity: "warn",
|
|
30461
31220
|
category: "Correctness",
|
|
30462
31221
|
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
30463
31222
|
create: (context) => {
|
|
@@ -31412,12 +32171,12 @@ const noPassDataToParent = defineRule({
|
|
|
31412
32171
|
if (calleeNode === identifier) {
|
|
31413
32172
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31414
32173
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31415
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32174
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31416
32175
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31417
32176
|
if (isCustomHookParameter(ref)) continue;
|
|
31418
32177
|
} else continue;
|
|
31419
32178
|
const methodName = getCallMethodName(calleeNode);
|
|
31420
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32179
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31421
32180
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31422
32181
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31423
32182
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31608,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31608
32367
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31609
32368
|
const calleeNode = callExpr.callee;
|
|
31610
32369
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31611
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32370
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31612
32371
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31613
32372
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31614
32373
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -31936,40 +32695,6 @@ const noPreventDefault = defineRule({
|
|
|
31936
32695
|
}
|
|
31937
32696
|
});
|
|
31938
32697
|
//#endregion
|
|
31939
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31940
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
31941
|
-
let node = callExpression;
|
|
31942
|
-
let parent = node.parent;
|
|
31943
|
-
while (parent) {
|
|
31944
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31945
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31946
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31947
|
-
node = parent;
|
|
31948
|
-
parent = node.parent;
|
|
31949
|
-
continue;
|
|
31950
|
-
}
|
|
31951
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31952
|
-
node = parent;
|
|
31953
|
-
parent = node.parent;
|
|
31954
|
-
continue;
|
|
31955
|
-
}
|
|
31956
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31957
|
-
node = parent;
|
|
31958
|
-
parent = node.parent;
|
|
31959
|
-
continue;
|
|
31960
|
-
}
|
|
31961
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31962
|
-
const expressions = parent.expressions ?? [];
|
|
31963
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
31964
|
-
node = parent;
|
|
31965
|
-
parent = node.parent;
|
|
31966
|
-
continue;
|
|
31967
|
-
}
|
|
31968
|
-
return false;
|
|
31969
|
-
}
|
|
31970
|
-
return false;
|
|
31971
|
-
};
|
|
31972
|
-
//#endregion
|
|
31973
32698
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
31974
32699
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
31975
32700
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32176,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32176
32901
|
return `${callee.name}()`;
|
|
32177
32902
|
}
|
|
32178
32903
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32179
|
-
const receiver = callee.object;
|
|
32904
|
+
const receiver = stripParenExpression(callee.object);
|
|
32180
32905
|
const property = callee.property;
|
|
32181
32906
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32182
32907
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32297,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32297
33022
|
if (typeof sourceValue !== "string") return;
|
|
32298
33023
|
if (handleExtraSource?.(node, context)) return;
|
|
32299
33024
|
if (sourceValue !== source) return;
|
|
33025
|
+
if (isTypeOnlyImport(node)) return;
|
|
32300
33026
|
for (const specifier of node.specifiers ?? []) {
|
|
32301
33027
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33028
|
+
if (specifier.importKind === "type") continue;
|
|
32302
33029
|
const importedName = getImportedName$1(specifier);
|
|
32303
33030
|
if (!importedName) continue;
|
|
32304
33031
|
const message = messages.get(importedName);
|
|
@@ -32317,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32317
33044
|
MemberExpression(node) {
|
|
32318
33045
|
if (namespaceBindings.size === 0) return;
|
|
32319
33046
|
if (node.computed) return;
|
|
32320
|
-
|
|
32321
|
-
if (!
|
|
33047
|
+
const receiver = stripParenExpression(node.object);
|
|
33048
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33049
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32322
33050
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32323
33051
|
const message = messages.get(node.property.name);
|
|
32324
33052
|
if (message) context.report({
|
|
@@ -32383,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32383
33111
|
});
|
|
32384
33112
|
//#endregion
|
|
32385
33113
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32386
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]
|
|
33114
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
32387
33115
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32388
33116
|
if (!rawFilename) return false;
|
|
32389
33117
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32421,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32421
33149
|
requires: ["react:19"],
|
|
32422
33150
|
tags: ["test-noise", "migration-hint"],
|
|
32423
33151
|
severity: "warn",
|
|
32424
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33152
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32425
33153
|
create: (context) => {
|
|
32426
33154
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32427
33155
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32745,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32745
33473
|
});
|
|
32746
33474
|
//#endregion
|
|
32747
33475
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32748
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32749
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32750
|
-
visitedSymbols.add(symbol);
|
|
32751
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
32752
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32753
|
-
const source = symbol.initializer;
|
|
32754
|
-
if (!source) return false;
|
|
32755
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32756
|
-
};
|
|
32757
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32758
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32759
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32760
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
32761
|
-
};
|
|
32762
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32763
|
-
let current = node;
|
|
32764
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
32765
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32766
|
-
current = current.object;
|
|
32767
|
-
}
|
|
32768
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32769
|
-
return false;
|
|
32770
|
-
};
|
|
32771
33476
|
const isInsideComponentContext = (node) => {
|
|
32772
33477
|
let cursor = node.parent;
|
|
32773
33478
|
while (cursor) {
|
|
32774
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32775
33479
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33480
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32776
33481
|
cursor = cursor.parent ?? null;
|
|
32777
33482
|
}
|
|
32778
33483
|
return false;
|
|
@@ -32782,24 +33487,28 @@ const functionBodyOf = (node) => {
|
|
|
32782
33487
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32783
33488
|
return null;
|
|
32784
33489
|
};
|
|
33490
|
+
const isHookCallee = (callee) => {
|
|
33491
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33492
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33493
|
+
return false;
|
|
33494
|
+
};
|
|
32785
33495
|
const containsHookCall = (body) => {
|
|
32786
33496
|
let found = false;
|
|
32787
33497
|
walkAst(body, (child) => {
|
|
32788
|
-
if (found) return;
|
|
33498
|
+
if (found) return false;
|
|
33499
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32789
33500
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32790
|
-
|
|
32791
|
-
if (name && isReactHookName(name)) found = true;
|
|
33501
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32792
33502
|
});
|
|
32793
33503
|
return found;
|
|
32794
33504
|
};
|
|
32795
|
-
const
|
|
33505
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32796
33506
|
if (!symbol) return false;
|
|
32797
33507
|
const declaration = symbol.declarationNode;
|
|
32798
33508
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32799
33509
|
const body = functionBodyOf(declaration);
|
|
32800
33510
|
if (!body) return false;
|
|
32801
|
-
|
|
32802
|
-
return !containsHookCall(body);
|
|
33511
|
+
return containsHookCall(body);
|
|
32803
33512
|
};
|
|
32804
33513
|
const noRenderInRender = defineRule({
|
|
32805
33514
|
id: "no-render-in-render",
|
|
@@ -32808,20 +33517,13 @@ const noRenderInRender = defineRule({
|
|
|
32808
33517
|
tags: ["test-noise"],
|
|
32809
33518
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32810
33519
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32811
|
-
const expression = node.expression;
|
|
33520
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32812
33521
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33522
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33523
|
+
const calleeName = expression.callee.name;
|
|
33524
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32817
33525
|
if (!isInsideComponentContext(node)) return;
|
|
32818
|
-
if (
|
|
32819
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32820
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32821
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32822
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32823
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32824
|
-
}
|
|
33526
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32825
33527
|
context.report({
|
|
32826
33528
|
node: expression,
|
|
32827
33529
|
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
@@ -32882,13 +33584,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32882
33584
|
//#endregion
|
|
32883
33585
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32884
33586
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
32885
|
-
const isReactDomRenderCall = (node) =>
|
|
32886
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32887
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32888
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32889
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32890
|
-
return node.callee.property.name === "render";
|
|
32891
|
-
};
|
|
33587
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32892
33588
|
const isUsedAsReturnValue = (parent) => {
|
|
32893
33589
|
if (!parent) return false;
|
|
32894
33590
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33724,7 +34420,7 @@ const noSetState = defineRule({
|
|
|
33724
34420
|
category: "Architecture",
|
|
33725
34421
|
create: (context) => ({ CallExpression(node) {
|
|
33726
34422
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33727
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34423
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33728
34424
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33729
34425
|
if (!getParentComponent(node)) return;
|
|
33730
34426
|
context.report({
|
|
@@ -36170,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36170
36866
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36171
36867
|
return true;
|
|
36172
36868
|
};
|
|
36869
|
+
const isTrivialContainerLiteral = (node) => {
|
|
36870
|
+
if (!node) return false;
|
|
36871
|
+
const innerExpression = stripParenExpression(node);
|
|
36872
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36873
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36874
|
+
return false;
|
|
36875
|
+
};
|
|
36876
|
+
const isNonEscapingRead = (identifier) => {
|
|
36877
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36878
|
+
const memberNode = readRoot.parent;
|
|
36879
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36880
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36881
|
+
const memberUseParent = memberUse.parent;
|
|
36882
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36883
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36884
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36885
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36886
|
+
};
|
|
36887
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36888
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36889
|
+
const parentNode = memoUsageRoot.parent;
|
|
36890
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36891
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36892
|
+
const bindingTarget = parentNode.id;
|
|
36893
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36894
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36895
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
36896
|
+
if (!symbol) return false;
|
|
36897
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36898
|
+
};
|
|
36173
36899
|
const noUsememoSimpleExpression = defineRule({
|
|
36174
36900
|
id: "no-usememo-simple-expression",
|
|
36175
36901
|
title: "useMemo on a cheap value",
|
|
@@ -36191,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36191
36917
|
let returnExpression = null;
|
|
36192
36918
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36193
36919
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36194
|
-
if (returnExpression
|
|
36920
|
+
if (!returnExpression) return;
|
|
36921
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36922
|
+
context.report({
|
|
36923
|
+
node,
|
|
36924
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36925
|
+
});
|
|
36926
|
+
return;
|
|
36927
|
+
}
|
|
36928
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36195
36929
|
node,
|
|
36196
|
-
message: "This
|
|
36930
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36197
36931
|
});
|
|
36198
36932
|
} })
|
|
36199
36933
|
});
|
|
@@ -36299,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36299
37033
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36300
37034
|
return { CallExpression(node) {
|
|
36301
37035
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36302
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37036
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36303
37037
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36304
37038
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36305
37039
|
context.report({
|
|
@@ -36575,8 +37309,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
37309
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
37310
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
37311
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36578
|
-
const
|
|
36579
|
-
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
37312
|
+
const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
|
|
36580
37313
|
const DEFAULT_REACT_HOCS = [
|
|
36581
37314
|
"memo",
|
|
36582
37315
|
"forwardRef",
|
|
@@ -36647,6 +37380,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
37380
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
37381
|
return false;
|
|
36649
37382
|
};
|
|
37383
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
37384
|
+
for (const property of objectExpression.properties ?? []) {
|
|
37385
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37386
|
+
const value = skipTsExpression(property.value);
|
|
37387
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
37388
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
37389
|
+
continue;
|
|
37390
|
+
}
|
|
37391
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
37392
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
37393
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
37394
|
+
}
|
|
37395
|
+
return false;
|
|
37396
|
+
};
|
|
36650
37397
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
37398
|
if (initializer) {
|
|
36652
37399
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +37424,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
37424
|
reportNode
|
|
36678
37425
|
};
|
|
36679
37426
|
}
|
|
37427
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
37428
|
+
kind: "namespace-object",
|
|
37429
|
+
reportNode
|
|
37430
|
+
};
|
|
36680
37431
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
37432
|
kind: "non-component",
|
|
36682
37433
|
reportNode
|
|
@@ -36693,7 +37444,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
37444
|
walkAst(programRoot, (child) => {
|
|
36694
37445
|
const childType = child.type;
|
|
36695
37446
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
37447
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
37448
|
});
|
|
36698
37449
|
return {
|
|
36699
37450
|
exportNodes,
|
|
@@ -36738,18 +37489,35 @@ const onlyExportComponents = defineRule({
|
|
|
36738
37489
|
category: "Architecture",
|
|
36739
37490
|
create: (context) => {
|
|
36740
37491
|
const settings = resolveSettings$6(context.settings);
|
|
36741
|
-
const state = {
|
|
36742
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36743
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
36744
|
-
allowConstantExport: settings.allowConstantExport
|
|
36745
|
-
};
|
|
36746
37492
|
return { Program(node) {
|
|
36747
37493
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
37494
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
37495
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
37496
|
+
const state = {
|
|
37497
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37498
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
37499
|
+
allowConstantExport: settings.allowConstantExport,
|
|
37500
|
+
localComponentNames,
|
|
37501
|
+
scopes: context.scopes
|
|
37502
|
+
};
|
|
37503
|
+
for (const child of componentCandidates) {
|
|
37504
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37505
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
37506
|
+
}
|
|
37507
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37508
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37509
|
+
}
|
|
37510
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37511
|
+
const initializer = child.init;
|
|
37512
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37513
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37514
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37515
|
+
}
|
|
37516
|
+
}
|
|
37517
|
+
}
|
|
36749
37518
|
const exports = [];
|
|
36750
37519
|
let hasReactExport = false;
|
|
36751
37520
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
37521
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
37522
|
for (const child of exportNodes) {
|
|
36755
37523
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36820,7 +37588,14 @@ const onlyExportComponents = defineRule({
|
|
|
36820
37588
|
});
|
|
36821
37589
|
continue;
|
|
36822
37590
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
37591
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
37592
|
+
context.report({
|
|
37593
|
+
node: stripped,
|
|
37594
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
37595
|
+
});
|
|
37596
|
+
continue;
|
|
37597
|
+
}
|
|
37598
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
37599
|
context.report({
|
|
36825
37600
|
node: stripped,
|
|
36826
37601
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +37648,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
37648
|
let entry;
|
|
36874
37649
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
37650
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
37651
|
+
else {
|
|
37652
|
+
entry = {
|
|
37653
|
+
kind: "non-component",
|
|
37654
|
+
reportNode
|
|
37655
|
+
};
|
|
37656
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37657
|
+
}
|
|
36880
37658
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
37659
|
exports.push(entry);
|
|
36882
37660
|
}
|
|
36883
37661
|
}
|
|
36884
37662
|
}
|
|
36885
37663
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
36886
|
-
const
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
walker = walker.parent ?? null;
|
|
36891
|
-
}
|
|
36892
|
-
return false;
|
|
36893
|
-
};
|
|
36894
|
-
for (const child of componentCandidates) {
|
|
36895
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36896
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36897
|
-
}
|
|
36898
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36899
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36900
|
-
}
|
|
36901
|
-
}
|
|
37664
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
37665
|
+
node: entry.reportNode,
|
|
37666
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
37667
|
+
});
|
|
36902
37668
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
37669
|
if (entry.kind === "non-component") context.report({
|
|
36904
37670
|
node: entry.reportNode,
|
|
@@ -36909,14 +37675,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37675
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37676
|
});
|
|
36911
37677
|
}
|
|
36912
|
-
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36913
|
-
node: local,
|
|
36914
|
-
message: LOCAL_COMPONENT_MESSAGE
|
|
36915
|
-
});
|
|
36916
|
-
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36917
|
-
node: local,
|
|
36918
|
-
message: NO_EXPORT_MESSAGE
|
|
36919
|
-
});
|
|
36920
37678
|
} };
|
|
36921
37679
|
}
|
|
36922
37680
|
});
|
|
@@ -37714,8 +38472,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
38472
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
38473
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
38474
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
38475
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
38476
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
38477
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
38478
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
38479
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -38419,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38419
39177
|
}
|
|
38420
39178
|
return null;
|
|
38421
39179
|
};
|
|
39180
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39181
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39182
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39183
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39184
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39185
|
+
return false;
|
|
39186
|
+
};
|
|
38422
39187
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38423
39188
|
const handlerStatements = getCallbackStatements(handler);
|
|
38424
39189
|
if (handlerStatements.length !== 1) return null;
|
|
38425
39190
|
const onlyStatement = handlerStatements[0];
|
|
38426
|
-
|
|
39191
|
+
let expression = onlyStatement;
|
|
39192
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39193
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39194
|
+
if (!expression) return null;
|
|
39195
|
+
expression = stripParenExpression(expression);
|
|
38427
39196
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38428
39197
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38429
39198
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38433,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38433
39202
|
setterArgument: expression.arguments[0]
|
|
38434
39203
|
};
|
|
38435
39204
|
};
|
|
39205
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39206
|
+
if (!init) return false;
|
|
39207
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39208
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39209
|
+
};
|
|
39210
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39211
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39212
|
+
const firstParam = functionNode.params?.[0];
|
|
39213
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39214
|
+
const listenerParamName = firstParam.name;
|
|
39215
|
+
let registersListener = false;
|
|
39216
|
+
walkAst(functionNode.body, (child) => {
|
|
39217
|
+
if (registersListener) return false;
|
|
39218
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39219
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39220
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39221
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39222
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39223
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39224
|
+
const registeredArgument = child.arguments?.[0];
|
|
39225
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39226
|
+
});
|
|
39227
|
+
return registersListener;
|
|
39228
|
+
};
|
|
39229
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39230
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39231
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39232
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39233
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39234
|
+
mutableBindingNames,
|
|
39235
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39236
|
+
};
|
|
39237
|
+
for (const statement of programRoot.body ?? []) {
|
|
39238
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39239
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39240
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39241
|
+
continue;
|
|
39242
|
+
}
|
|
39243
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39244
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39245
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39246
|
+
const init = declarator.init ?? null;
|
|
39247
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39248
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39249
|
+
continue;
|
|
39250
|
+
}
|
|
39251
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39252
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39253
|
+
continue;
|
|
39254
|
+
}
|
|
39255
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39256
|
+
}
|
|
39257
|
+
}
|
|
39258
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39259
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39260
|
+
return {
|
|
39261
|
+
mutableBindingNames,
|
|
39262
|
+
subscribeFunctionNames
|
|
39263
|
+
};
|
|
39264
|
+
};
|
|
39265
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39266
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39267
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39268
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39269
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39270
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39271
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39272
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39273
|
+
return initialArgument.name;
|
|
39274
|
+
};
|
|
39275
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39276
|
+
if (!argument) return false;
|
|
39277
|
+
const unwrapped = stripParenExpression(argument);
|
|
39278
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39279
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39280
|
+
let callsSetter = false;
|
|
39281
|
+
walkAst(unwrapped.body, (child) => {
|
|
39282
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39283
|
+
callsSetter = true;
|
|
39284
|
+
return false;
|
|
39285
|
+
}
|
|
39286
|
+
});
|
|
39287
|
+
return callsSetter;
|
|
39288
|
+
};
|
|
39289
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39290
|
+
let matchedCall = null;
|
|
39291
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39292
|
+
if (matchedCall) return false;
|
|
39293
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39294
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39295
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39296
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39297
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39298
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39299
|
+
matchedCall = child;
|
|
39300
|
+
return false;
|
|
39301
|
+
}
|
|
39302
|
+
});
|
|
39303
|
+
return matchedCall;
|
|
39304
|
+
};
|
|
38436
39305
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38437
39306
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38438
39307
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38449,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38449
39318
|
severity: "warn",
|
|
38450
39319
|
recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
|
|
38451
39320
|
create: (context) => {
|
|
39321
|
+
let cachedStoreIndex = null;
|
|
39322
|
+
const storeIndexFor = (node) => {
|
|
39323
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39324
|
+
const programRoot = findProgramRoot(node);
|
|
39325
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39326
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39327
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39328
|
+
};
|
|
39329
|
+
return cachedStoreIndex;
|
|
39330
|
+
};
|
|
38452
39331
|
const checkComponent = (componentBody) => {
|
|
38453
39332
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38454
39333
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38486,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38486
39365
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38487
39366
|
if (!useStateInitializer) continue;
|
|
38488
39367
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39368
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38489
39369
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38490
39370
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38491
39371
|
context.report({
|
|
@@ -38493,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38493
39373
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38494
39374
|
});
|
|
38495
39375
|
}
|
|
39376
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39377
|
+
};
|
|
39378
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39379
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39380
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39381
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39382
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39383
|
+
binding,
|
|
39384
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39385
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39386
|
+
if (snapshotBindings.length === 0) return;
|
|
39387
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39388
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39389
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39390
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39391
|
+
const depsNode = effectCall.arguments[1];
|
|
39392
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39393
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39394
|
+
const callback = getEffectCallback(effectCall);
|
|
39395
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39396
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39397
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39398
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39399
|
+
reportedDeclarators.add(binding.declarator);
|
|
39400
|
+
context.report({
|
|
39401
|
+
node: binding.declarator,
|
|
39402
|
+
message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
|
|
39403
|
+
});
|
|
39404
|
+
}
|
|
39405
|
+
}
|
|
38496
39406
|
};
|
|
38497
39407
|
return {
|
|
38498
39408
|
FunctionDeclaration(node) {
|
|
38499
|
-
|
|
39409
|
+
const functionName = node.id?.name;
|
|
39410
|
+
if (!functionName) return;
|
|
39411
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38500
39412
|
checkComponent(node.body);
|
|
38501
39413
|
},
|
|
38502
39414
|
VariableDeclarator(node) {
|
|
38503
|
-
|
|
39415
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39416
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38504
39417
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38505
39418
|
checkComponent(node.init.body);
|
|
38506
39419
|
}
|
|
@@ -39110,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39110
40023
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39111
40024
|
const body = queryFnValue.body;
|
|
39112
40025
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39113
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40026
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39114
40027
|
node: queryFnProperty,
|
|
39115
40028
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39116
40029
|
});
|
|
@@ -39617,17 +40530,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39617
40530
|
}
|
|
39618
40531
|
});
|
|
39619
40532
|
//#endregion
|
|
39620
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39621
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39622
|
-
let ancestor = bindingIdentifier.parent;
|
|
39623
|
-
while (ancestor) {
|
|
39624
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39625
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39626
|
-
ancestor = ancestor.parent ?? null;
|
|
39627
|
-
}
|
|
39628
|
-
return null;
|
|
39629
|
-
};
|
|
39630
|
-
//#endregion
|
|
39631
40533
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39632
40534
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39633
40535
|
{
|
|
@@ -39636,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39636
40538
|
},
|
|
39637
40539
|
{
|
|
39638
40540
|
display: "Date.now()",
|
|
39639
|
-
matches: (node) =>
|
|
40541
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39640
40542
|
},
|
|
39641
40543
|
{
|
|
39642
40544
|
display: "Math.random()",
|
|
39643
|
-
matches: (node) =>
|
|
40545
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39644
40546
|
},
|
|
39645
40547
|
{
|
|
39646
40548
|
display: "performance.now()",
|
|
39647
|
-
matches: (node) =>
|
|
40549
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39648
40550
|
},
|
|
39649
40551
|
{
|
|
39650
40552
|
display: "crypto.randomUUID()",
|
|
39651
|
-
matches: (node) =>
|
|
40553
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39652
40554
|
}
|
|
39653
40555
|
];
|
|
39654
40556
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39660,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39660
40562
|
}
|
|
39661
40563
|
return null;
|
|
39662
40564
|
};
|
|
39663
|
-
const executesDuringRender = (functionNode) => {
|
|
39664
|
-
const parent = functionNode.parent;
|
|
39665
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39666
|
-
if (parent.callee === functionNode) return true;
|
|
39667
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39668
|
-
};
|
|
39669
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39670
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39671
|
-
const unwrapped = stripParenExpression(expression);
|
|
39672
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39673
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39674
|
-
const property = unwrapped.property;
|
|
39675
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39676
|
-
}
|
|
39677
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39678
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39679
|
-
return false;
|
|
39680
|
-
};
|
|
39681
|
-
const isFalsyLiteral = (node) => {
|
|
39682
|
-
if (!node) return true;
|
|
39683
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39684
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39685
|
-
};
|
|
39686
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39687
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39688
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39689
|
-
if (!binding) return false;
|
|
39690
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39691
|
-
if (!declarator?.init) return false;
|
|
39692
|
-
const init = stripParenExpression(declarator.init);
|
|
39693
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39694
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39695
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39696
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39697
|
-
};
|
|
39698
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39699
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39700
|
-
let cursor = node;
|
|
39701
|
-
let parent = node.parent;
|
|
39702
|
-
while (parent) {
|
|
39703
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39704
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39705
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39706
|
-
cursor = parent;
|
|
39707
|
-
parent = parent.parent ?? null;
|
|
39708
|
-
}
|
|
39709
|
-
return false;
|
|
39710
|
-
};
|
|
39711
40565
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39712
40566
|
const member = dateNode.parent;
|
|
39713
40567
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39731,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39731
40585
|
}
|
|
39732
40586
|
return false;
|
|
39733
40587
|
};
|
|
39734
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
39735
|
-
let cursor = node;
|
|
39736
|
-
let parent = node.parent;
|
|
39737
|
-
while (parent) {
|
|
39738
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39739
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39740
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39741
|
-
cursor = parent;
|
|
39742
|
-
parent = parent.parent ?? null;
|
|
39743
|
-
}
|
|
39744
|
-
return false;
|
|
39745
|
-
};
|
|
39746
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39747
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39748
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39749
|
-
return false;
|
|
39750
|
-
};
|
|
39751
40588
|
const renderingHydrationMismatchTime = defineRule({
|
|
39752
40589
|
id: "rendering-hydration-mismatch-time",
|
|
39753
40590
|
title: "Time or random value in JSX",
|
|
@@ -39795,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39795
40632
|
}
|
|
39796
40633
|
});
|
|
39797
40634
|
//#endregion
|
|
40635
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40636
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40637
|
+
"toLocaleString",
|
|
40638
|
+
"toLocaleDateString",
|
|
40639
|
+
"toLocaleTimeString",
|
|
40640
|
+
"getTimezoneOffset"
|
|
40641
|
+
]);
|
|
40642
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40643
|
+
let readsLocaleEnvironment = false;
|
|
40644
|
+
walkAst(expression, (child) => {
|
|
40645
|
+
if (readsLocaleEnvironment) return false;
|
|
40646
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40647
|
+
if (child.object.name === "Intl") {
|
|
40648
|
+
readsLocaleEnvironment = true;
|
|
40649
|
+
return false;
|
|
40650
|
+
}
|
|
40651
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40652
|
+
readsLocaleEnvironment = true;
|
|
40653
|
+
return false;
|
|
40654
|
+
}
|
|
40655
|
+
}
|
|
40656
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
|
|
40657
|
+
readsLocaleEnvironment = true;
|
|
40658
|
+
return false;
|
|
40659
|
+
}
|
|
40660
|
+
});
|
|
40661
|
+
return readsLocaleEnvironment;
|
|
40662
|
+
};
|
|
40663
|
+
//#endregion
|
|
39798
40664
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39799
40665
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39800
40666
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39860,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39860
40726
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39861
40727
|
const callback = getEffectCallback(node);
|
|
39862
40728
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39863
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39864
|
-
if (
|
|
40729
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
40730
|
+
if (bodyStatements.length !== 1) return;
|
|
39865
40731
|
const soleStatement = bodyStatements[0];
|
|
39866
40732
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39867
40733
|
const expression = soleStatement.expression;
|
|
39868
40734
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39869
40735
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39870
40736
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40737
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39871
40738
|
context.report({
|
|
39872
40739
|
node,
|
|
39873
40740
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40686,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40686
41553
|
} })
|
|
40687
41554
|
});
|
|
40688
41555
|
//#endregion
|
|
41556
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41557
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41558
|
+
//#endregion
|
|
40689
41559
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40690
41560
|
const rerenderLazyRefInit = defineRule({
|
|
40691
41561
|
id: "rerender-lazy-ref-init",
|
|
@@ -40696,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40696
41566
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40697
41567
|
create: (context) => ({ CallExpression(node) {
|
|
40698
41568
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40699
|
-
const initializer = node.arguments[0];
|
|
41569
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40700
41570
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40701
41571
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40702
41572
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40704,6 +41574,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
41574
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
41575
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
41576
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41577
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40707
41578
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
41579
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
41580
|
context.report({
|
|
@@ -40736,26 +41607,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
41607
|
"getUTCMilliseconds",
|
|
40737
41608
|
"valueOf"
|
|
40738
41609
|
]);
|
|
40739
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40740
|
-
"Date",
|
|
40741
|
-
"Map",
|
|
40742
|
-
"Set",
|
|
40743
|
-
"WeakMap",
|
|
40744
|
-
"WeakSet",
|
|
40745
|
-
"RegExp",
|
|
40746
|
-
"Error",
|
|
40747
|
-
"URL",
|
|
40748
|
-
"URLSearchParams",
|
|
40749
|
-
"AbortController"
|
|
40750
|
-
]);
|
|
40751
41610
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
41611
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
41612
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40754
|
-
|
|
40755
|
-
if (isNodeOfType(
|
|
40756
|
-
if (isNodeOfType(
|
|
40757
|
-
if (isNodeOfType(
|
|
40758
|
-
if (isNodeOfType(
|
|
41613
|
+
const innerExpression = stripParenExpression(expression);
|
|
41614
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41615
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41616
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41617
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40759
41618
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40760
41619
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40761
41620
|
if (spreadCall) return spreadCall;
|
|
@@ -40778,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40778
41637
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40779
41638
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40780
41639
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40781
|
-
if (
|
|
41640
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40782
41641
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40783
41642
|
if (isReactHookName(calleeName)) return;
|
|
40784
41643
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41445,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41445
42304
|
const body = reactionFn.body;
|
|
41446
42305
|
let singleAssignment = null;
|
|
41447
42306
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41448
|
-
const statements = body.body ?? [];
|
|
42307
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41449
42308
|
if (statements.length !== 1) return;
|
|
41450
42309
|
const onlyStatement = statements[0];
|
|
41451
42310
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41491,7 +42350,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
42350
|
});
|
|
41492
42351
|
//#endregion
|
|
41493
42352
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
42353
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
42354
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
42355
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
42356
|
"tap",
|
|
@@ -41519,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41519
42378
|
"catch",
|
|
41520
42379
|
"finally"
|
|
41521
42380
|
]);
|
|
41522
|
-
const findChainRoot = (
|
|
42381
|
+
const findChainRoot = (wrappedNode) => {
|
|
42382
|
+
const node = stripParenExpression(wrappedNode);
|
|
41523
42383
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41524
42384
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41525
42385
|
calleeName: node.callee.name,
|
|
@@ -41551,7 +42411,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
42411
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
41552
42412
|
create: (context) => {
|
|
41553
42413
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
42414
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
42415
|
return { ExpressionStatement(node) {
|
|
41556
42416
|
const expression = node.expression;
|
|
41557
42417
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42107,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42107
42967
|
};
|
|
42108
42968
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42109
42969
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42110
|
-
|
|
42111
|
-
|
|
42970
|
+
const receiver = stripParenExpression(callee.object);
|
|
42971
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
42972
|
+
const localName = receiver.name;
|
|
42112
42973
|
const binding = findVariableInitializer(node, localName);
|
|
42113
42974
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42114
42975
|
return localName === "Dimensions";
|
|
42115
42976
|
}
|
|
42116
|
-
if (isNodeOfType(
|
|
42117
|
-
if (getInitializerModuleSource(node,
|
|
42118
|
-
const rootName = getRootIdentifierName(
|
|
42977
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
42978
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
42979
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42119
42980
|
if (rootName === null) return false;
|
|
42120
42981
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42121
42982
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42122
42983
|
}
|
|
42123
|
-
if (getRequireCallSource(
|
|
42984
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42124
42985
|
return false;
|
|
42125
42986
|
};
|
|
42126
42987
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42526,7 +43387,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
43387
|
};
|
|
42527
43388
|
//#endregion
|
|
42528
43389
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
43390
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
43391
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
43392
|
const legacyShadowPropertyNames = [];
|
|
42532
43393
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +43410,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
43410
|
severity: "warn",
|
|
42550
43411
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
43412
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
43413
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
43414
|
return {
|
|
42554
43415
|
JSXAttribute(node) {
|
|
42555
43416
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -42564,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42564
43425
|
},
|
|
42565
43426
|
CallExpression(node) {
|
|
42566
43427
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42567
|
-
|
|
43428
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43429
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42568
43430
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42569
43431
|
const stylesArgument = node.arguments?.[0];
|
|
42570
43432
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43414,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43414
44276
|
const callee = node.callee;
|
|
43415
44277
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43416
44278
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43417
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44279
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43418
44280
|
context.report({
|
|
43419
44281
|
node,
|
|
43420
44282
|
message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
|
|
@@ -43470,7 +44332,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
44332
|
};
|
|
43471
44333
|
//#endregion
|
|
43472
44334
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
44335
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
44336
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
44337
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
44338
|
const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
|
|
@@ -43504,7 +44366,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
44366
|
severity: "warn",
|
|
43505
44367
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
44368
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
44369
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
44370
|
const flaggedImports = [];
|
|
43509
44371
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
44372
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43642,14 +44504,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43642
44504
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43643
44505
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43644
44506
|
const methodName = callee.property.name;
|
|
43645
|
-
|
|
44507
|
+
const receiver = stripParenExpression(callee.object);
|
|
44508
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43646
44509
|
factoryName: methodName,
|
|
43647
44510
|
chainMethodNames,
|
|
43648
44511
|
numberOfTapsArgument
|
|
43649
44512
|
};
|
|
43650
44513
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43651
44514
|
chainMethodNames.push(methodName);
|
|
43652
|
-
cursor =
|
|
44515
|
+
cursor = receiver;
|
|
43653
44516
|
}
|
|
43654
44517
|
return null;
|
|
43655
44518
|
};
|
|
@@ -43991,7 +44854,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
44854
|
});
|
|
43992
44855
|
//#endregion
|
|
43993
44856
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
44857
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
44858
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
44859
|
"shadowColor",
|
|
43997
44860
|
"shadowOffset",
|
|
@@ -44077,7 +44940,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
44940
|
severity: "warn",
|
|
44078
44941
|
recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
|
|
44079
44942
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44943
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
44944
|
return {
|
|
44082
44945
|
JSXAttribute(node) {
|
|
44083
44946
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +45037,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
45037
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
45038
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
45039
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
45040
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
45041
|
+
if (roleCandidates === null) return;
|
|
45042
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
45043
|
for (const role of roles) {
|
|
44181
45044
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
45045
|
if (!required) continue;
|
|
@@ -47293,7 +48156,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
48156
|
};
|
|
47294
48157
|
//#endregion
|
|
47295
48158
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
48159
|
+
const buildMessageDefault = (roles, propName) => {
|
|
48160
|
+
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
48161
|
+
};
|
|
47297
48162
|
const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
|
|
47298
48163
|
const roleSupportsAriaProps = defineRule({
|
|
47299
48164
|
id: "role-supports-aria-props",
|
|
@@ -47313,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47313
48178
|
const propName = propRawName.toLowerCase();
|
|
47314
48179
|
if (!propName.startsWith("aria-")) continue;
|
|
47315
48180
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48181
|
+
const attributeValue = attributeNode.value;
|
|
48182
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47316
48183
|
(ariaAttributes ??= []).push({
|
|
47317
48184
|
attribute,
|
|
47318
48185
|
propName
|
|
@@ -47321,17 +48188,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
48188
|
if (!ariaAttributes) return;
|
|
47322
48189
|
const elementType = getElementType(node, context.settings);
|
|
47323
48190
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
48191
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
48192
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
48193
|
+
const supportedSets = [];
|
|
48194
|
+
for (const role of roleCandidates) {
|
|
48195
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
48196
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
48197
|
+
if (!supported) return;
|
|
48198
|
+
supportedSets.push(supported);
|
|
48199
|
+
}
|
|
47327
48200
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
48201
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
48202
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
48203
|
context.report({
|
|
47333
48204
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
48205
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
48206
|
});
|
|
47336
48207
|
}
|
|
47337
48208
|
} })
|
|
@@ -47683,11 +48554,19 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47683
48554
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
48555
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
48556
|
};
|
|
47686
|
-
const
|
|
47687
|
-
const
|
|
48557
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
48558
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
48559
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
48560
|
+
};
|
|
48561
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
48562
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
48563
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
48564
|
+
return false;
|
|
48565
|
+
};
|
|
48566
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47688
48567
|
const initializer = symbol.initializer;
|
|
47689
48568
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
48569
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47691
48570
|
};
|
|
47692
48571
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
48572
|
let current = node.parent;
|
|
@@ -47766,7 +48645,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
48645
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
48646
|
if (!hookContext) return;
|
|
47768
48647
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48648
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
48649
|
context.report({
|
|
47771
48650
|
node: node.callee,
|
|
47772
48651
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47879,7 +48758,7 @@ const rulesOfHooks = defineRule({
|
|
|
47879
48758
|
Identifier(node) {
|
|
47880
48759
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
48760
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
48761
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47883
48762
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
48763
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
48764
|
context.report({
|
|
@@ -48027,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48027
48906
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48028
48907
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48029
48908
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48030
|
-
const
|
|
48909
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
48910
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48031
48911
|
if (!objectName) return;
|
|
48032
48912
|
const methodName = node.callee.property.name;
|
|
48033
48913
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48703,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48703
49583
|
};
|
|
48704
49584
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48705
49585
|
let chainTip = referenceIdentifier;
|
|
48706
|
-
while (chainTip.parent
|
|
49586
|
+
while (chainTip.parent) {
|
|
49587
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49588
|
+
chainTip = chainTip.parent;
|
|
49589
|
+
continue;
|
|
49590
|
+
}
|
|
49591
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49592
|
+
chainTip = chainTip.parent;
|
|
49593
|
+
continue;
|
|
49594
|
+
}
|
|
49595
|
+
break;
|
|
49596
|
+
}
|
|
48707
49597
|
return chainTip;
|
|
48708
49598
|
};
|
|
48709
49599
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48728,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48728
49618
|
const callee = callExpression.callee;
|
|
48729
49619
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48730
49620
|
const methodName = getMemberPropertyName$1(callee);
|
|
48731
|
-
|
|
49621
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49622
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48732
49623
|
}
|
|
48733
49624
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48734
49625
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49458,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49458
50349
|
};
|
|
49459
50350
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49460
50351
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49461
|
-
let currentNode = outerNode.callee.object;
|
|
50352
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49462
50353
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49463
50354
|
const calleeName = getCalleeName$2(currentNode);
|
|
49464
50355
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49469,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49469
50360
|
}
|
|
49470
50361
|
}
|
|
49471
50362
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49472
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50363
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49473
50364
|
else break;
|
|
49474
50365
|
}
|
|
49475
50366
|
return result;
|
|
@@ -50122,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50122
51013
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50123
51014
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50124
51015
|
if (methodName) methodNames.unshift(methodName);
|
|
50125
|
-
currentNode = currentNode.callee.object;
|
|
51016
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50126
51017
|
}
|
|
50127
51018
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50128
51019
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53204,6 +54095,18 @@ const reactDoctorRules = [
|
|
|
53204
54095
|
category: "Bugs"
|
|
53205
54096
|
}
|
|
53206
54097
|
},
|
|
54098
|
+
{
|
|
54099
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54100
|
+
id: "no-locale-format-in-render",
|
|
54101
|
+
source: "react-doctor",
|
|
54102
|
+
originallyExternal: false,
|
|
54103
|
+
rule: {
|
|
54104
|
+
...noLocaleFormatInRender,
|
|
54105
|
+
framework: "global",
|
|
54106
|
+
category: "Bugs",
|
|
54107
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54108
|
+
}
|
|
54109
|
+
},
|
|
53207
54110
|
{
|
|
53208
54111
|
key: "react-doctor/no-long-transition-duration",
|
|
53209
54112
|
id: "no-long-transition-duration",
|
|
@@ -55451,6 +56354,34 @@ const reactDoctorRules = [
|
|
|
55451
56354
|
];
|
|
55452
56355
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
56356
|
//#endregion
|
|
56357
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
56358
|
+
const isNextFileActive = (context) => {
|
|
56359
|
+
const rawFilename = context.filename;
|
|
56360
|
+
if (!rawFilename) return true;
|
|
56361
|
+
const filename = normalizeFilename(rawFilename);
|
|
56362
|
+
const manifest = readNearestPackageManifest(filename);
|
|
56363
|
+
if (!manifest) return true;
|
|
56364
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
56365
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
56366
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
56367
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
56368
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
56369
|
+
return true;
|
|
56370
|
+
};
|
|
56371
|
+
//#endregion
|
|
56372
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
56373
|
+
const EMPTY_VISITORS$1 = {};
|
|
56374
|
+
const wrapNextjsRule = (rule) => {
|
|
56375
|
+
const innerCreate = rule.create.bind(rule);
|
|
56376
|
+
return {
|
|
56377
|
+
...rule,
|
|
56378
|
+
create: (context) => {
|
|
56379
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
56380
|
+
return innerCreate(context);
|
|
56381
|
+
}
|
|
56382
|
+
};
|
|
56383
|
+
};
|
|
56384
|
+
//#endregion
|
|
55454
56385
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
56386
|
const EMPTY_VISITORS = {};
|
|
55456
56387
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +56851,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
56851
|
});
|
|
55921
56852
|
//#endregion
|
|
55922
56853
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56854
|
+
const applyFrameworkGate = (rule) => {
|
|
56855
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56856
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56857
|
+
return rule;
|
|
56858
|
+
};
|
|
55923
56859
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
56860
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56861
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
56862
|
return wrapped;
|
|
55927
56863
|
};
|
|
55928
56864
|
const plugin = {
|