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