oxlint-plugin-react-doctor 0.7.2-dev.fb8ffb0 → 0.7.3-dev.6b70b32
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 +138 -0
- package/dist/index.js +2394 -556
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
+
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
+
"WebSocket",
|
|
537
|
+
"EventSource",
|
|
538
|
+
"BroadcastChannel",
|
|
539
|
+
"RTCPeerConnection"
|
|
540
|
+
]);
|
|
535
541
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
536
542
|
"location",
|
|
537
543
|
"window",
|
|
@@ -687,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
687
693
|
"unwatch",
|
|
688
694
|
"unlisten",
|
|
689
695
|
"unsub",
|
|
690
|
-
"abort"
|
|
696
|
+
"abort",
|
|
697
|
+
"disconnect",
|
|
698
|
+
"unobserve",
|
|
699
|
+
"close"
|
|
691
700
|
]);
|
|
692
701
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
693
702
|
"remove",
|
|
@@ -782,7 +791,7 @@ const findProgramRoot = (node) => {
|
|
|
782
791
|
};
|
|
783
792
|
//#endregion
|
|
784
793
|
//#region src/plugin/utils/get-imported-name.ts
|
|
785
|
-
const getImportedName
|
|
794
|
+
const getImportedName = (importSpecifier) => {
|
|
786
795
|
if (!isNodeOfType(importSpecifier, "ImportSpecifier")) return void 0;
|
|
787
796
|
const imported = importSpecifier.imported;
|
|
788
797
|
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
@@ -904,7 +913,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
904
913
|
continue;
|
|
905
914
|
}
|
|
906
915
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
907
|
-
const importedName = getImportedName
|
|
916
|
+
const importedName = getImportedName(specifier);
|
|
908
917
|
if (!importedName || !ACTIVITY_IMPORTED_NAMES.has(importedName)) continue;
|
|
909
918
|
if (isNodeOfType(specifier.local, "Identifier")) localActivityNames.add(specifier.local.name);
|
|
910
919
|
}
|
|
@@ -2248,6 +2257,39 @@ const anchorHasContent = defineRule({
|
|
|
2248
2257
|
}
|
|
2249
2258
|
});
|
|
2250
2259
|
//#endregion
|
|
2260
|
+
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
+
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
+
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
+
const expression = stripParenExpression(rawExpression);
|
|
2264
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
+
return staticValue === null ? null : [staticValue];
|
|
2268
|
+
}
|
|
2269
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
+
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
+
if (consequentValues === null) return null;
|
|
2272
|
+
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
+
if (alternateValues === null) return null;
|
|
2274
|
+
return [...consequentValues, ...alternateValues];
|
|
2275
|
+
}
|
|
2276
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
+
if (remainingHops === 0) return null;
|
|
2278
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2281
|
+
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2282
|
+
}
|
|
2283
|
+
return null;
|
|
2284
|
+
};
|
|
2285
|
+
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2286
|
+
const value = attribute.value;
|
|
2287
|
+
if (!value) return null;
|
|
2288
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2289
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2290
|
+
return null;
|
|
2291
|
+
};
|
|
2292
|
+
//#endregion
|
|
2251
2293
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2252
2294
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2253
2295
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2275,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2275
2317
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2276
2318
|
const isInvalidHref = (value, validHrefs) => {
|
|
2277
2319
|
if (validHrefs.has(value)) return false;
|
|
2278
|
-
|
|
2320
|
+
const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
|
|
2321
|
+
return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
|
|
2279
2322
|
};
|
|
2280
|
-
const
|
|
2281
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2282
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2283
|
-
const expression = value.expression;
|
|
2284
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2285
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2286
|
-
}
|
|
2287
|
-
return null;
|
|
2288
|
-
};
|
|
2289
|
-
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2290
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2323
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2291
2324
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2292
2325
|
const expression = value.expression;
|
|
2293
2326
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2294
|
-
if (isNodeOfType(expression, "Literal"))
|
|
2295
|
-
if (expression.value === null) return true;
|
|
2296
|
-
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2297
|
-
}
|
|
2298
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2299
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2300
|
-
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2301
|
-
}
|
|
2327
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2302
2328
|
}
|
|
2303
2329
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2304
2330
|
return false;
|
|
@@ -2331,9 +2357,10 @@ const anchorIsValid = defineRule({
|
|
|
2331
2357
|
});
|
|
2332
2358
|
return;
|
|
2333
2359
|
}
|
|
2334
|
-
|
|
2360
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2361
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2335
2362
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2336
|
-
if (!hasOnClick &&
|
|
2363
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2337
2364
|
context.report({
|
|
2338
2365
|
node: node.name,
|
|
2339
2366
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3389,6 +3416,25 @@ const ariaRole = defineRule({
|
|
|
3389
3416
|
if (!roleAttribute) return;
|
|
3390
3417
|
const elementType = getElementType(node, context.settings);
|
|
3391
3418
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3419
|
+
const reportFirstInvalidCandidate = (candidates) => {
|
|
3420
|
+
for (const candidate of candidates) {
|
|
3421
|
+
if (candidate.trim().length === 0) {
|
|
3422
|
+
context.report({
|
|
3423
|
+
node: roleAttribute,
|
|
3424
|
+
message: buildBaseMessage("")
|
|
3425
|
+
});
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3429
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3430
|
+
context.report({
|
|
3431
|
+
node: roleAttribute,
|
|
3432
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3433
|
+
});
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3392
3438
|
const value = roleAttribute.value;
|
|
3393
3439
|
if (!value) {
|
|
3394
3440
|
context.report({
|
|
@@ -3405,22 +3451,7 @@ const ariaRole = defineRule({
|
|
|
3405
3451
|
});
|
|
3406
3452
|
return;
|
|
3407
3453
|
}
|
|
3408
|
-
|
|
3409
|
-
if (stringValue.trim().length === 0) {
|
|
3410
|
-
context.report({
|
|
3411
|
-
node: roleAttribute,
|
|
3412
|
-
message: buildBaseMessage("")
|
|
3413
|
-
});
|
|
3414
|
-
return;
|
|
3415
|
-
}
|
|
3416
|
-
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3417
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3418
|
-
context.report({
|
|
3419
|
-
node: roleAttribute,
|
|
3420
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3421
|
-
});
|
|
3422
|
-
return;
|
|
3423
|
-
}
|
|
3454
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3424
3455
|
return;
|
|
3425
3456
|
}
|
|
3426
3457
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3441,6 +3472,11 @@ const ariaRole = defineRule({
|
|
|
3441
3472
|
});
|
|
3442
3473
|
return;
|
|
3443
3474
|
}
|
|
3475
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3476
|
+
if (resolvedCandidates !== null) {
|
|
3477
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3444
3480
|
return;
|
|
3445
3481
|
}
|
|
3446
3482
|
context.report({
|
|
@@ -5157,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5157
5193
|
const callee = node.callee;
|
|
5158
5194
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5159
5195
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5160
|
-
if (!isWebStorageObject(callee.object)) return;
|
|
5196
|
+
if (!isWebStorageObject(stripParenExpression(callee.object))) return;
|
|
5161
5197
|
const keyArgument = node.arguments?.[0];
|
|
5162
5198
|
if (!keyArgument) return;
|
|
5163
5199
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6097,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6097
6133
|
})
|
|
6098
6134
|
});
|
|
6099
6135
|
//#endregion
|
|
6136
|
+
//#region src/plugin/utils/is-global-method-call.ts
|
|
6137
|
+
const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
6138
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6139
|
+
const callee = stripParenExpression(node.callee);
|
|
6140
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
6141
|
+
const receiver = stripParenExpression(callee.object);
|
|
6142
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6143
|
+
};
|
|
6144
|
+
//#endregion
|
|
6100
6145
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6101
6146
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6102
6147
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6103
6148
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6104
6149
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6105
|
-
const isJsonStringifyCall = (node) =>
|
|
6106
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6107
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6108
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6109
|
-
if (node.callee.object.name !== "JSON") return false;
|
|
6110
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6111
|
-
return node.callee.property.name === "stringify";
|
|
6112
|
-
};
|
|
6150
|
+
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6113
6151
|
const resolveStringKey = (keyArg, context) => {
|
|
6114
6152
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6115
6153
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6128,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6128
6166
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6129
6167
|
create: (context) => ({ CallExpression(node) {
|
|
6130
6168
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6131
|
-
|
|
6132
|
-
if (!
|
|
6169
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6170
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6171
|
+
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6133
6172
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6134
6173
|
if (node.callee.property.name !== "setItem") return;
|
|
6135
6174
|
const keyArg = node.arguments?.[0];
|
|
@@ -6142,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6142
6181
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6143
6182
|
context.report({
|
|
6144
6183
|
node: keyArg,
|
|
6145
|
-
message: `${
|
|
6184
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6146
6185
|
});
|
|
6147
6186
|
} })
|
|
6148
6187
|
});
|
|
@@ -7623,6 +7662,840 @@ const displayName = defineRule({
|
|
|
7623
7662
|
}
|
|
7624
7663
|
});
|
|
7625
7664
|
//#endregion
|
|
7665
|
+
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
7666
|
+
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
7667
|
+
if (!isNodeOfType(node, "Property")) return null;
|
|
7668
|
+
if (node.computed) {
|
|
7669
|
+
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
7670
|
+
return null;
|
|
7671
|
+
}
|
|
7672
|
+
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
7673
|
+
if (isNodeOfType(node.key, "Literal")) {
|
|
7674
|
+
if (typeof node.key.value === "string") return node.key.value;
|
|
7675
|
+
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
7676
|
+
}
|
|
7677
|
+
return null;
|
|
7678
|
+
};
|
|
7679
|
+
//#endregion
|
|
7680
|
+
//#region src/plugin/utils/read-static-boolean.ts
|
|
7681
|
+
const readStaticBoolean = (node) => {
|
|
7682
|
+
if (!node) return null;
|
|
7683
|
+
const unwrappedNode = stripParenExpression(node);
|
|
7684
|
+
if (!isNodeOfType(unwrappedNode, "Literal") || typeof unwrappedNode.value !== "boolean") return null;
|
|
7685
|
+
return unwrappedNode.value;
|
|
7686
|
+
};
|
|
7687
|
+
//#endregion
|
|
7688
|
+
//#region src/plugin/utils/is-react-api-call.ts
|
|
7689
|
+
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7690
|
+
const isImportedFromReact = (symbol) => {
|
|
7691
|
+
if (symbol.kind !== "import") return false;
|
|
7692
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
7693
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react");
|
|
7694
|
+
};
|
|
7695
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
7696
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7697
|
+
const symbol = scopes.symbolFor(identifier);
|
|
7698
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7699
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
7700
|
+
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
7701
|
+
};
|
|
7702
|
+
const isReactNamespaceImport = (identifier, scopes) => {
|
|
7703
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7704
|
+
const symbol = scopes.symbolFor(identifier);
|
|
7705
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7706
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
7707
|
+
};
|
|
7708
|
+
const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
7709
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7710
|
+
const callee = stripParenExpression(node.callee);
|
|
7711
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
7712
|
+
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7713
|
+
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7714
|
+
}
|
|
7715
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
7716
|
+
if (isReactNamespaceImport(callee.object, scopes)) return true;
|
|
7717
|
+
return Boolean(options.allowGlobalReactNamespace && callee.object.name === "React" && scopes.isGlobalReference(callee.object));
|
|
7718
|
+
};
|
|
7719
|
+
//#endregion
|
|
7720
|
+
//#region src/plugin/rules/state-and-effects/utils/build-listener-cleanup-mismatch-message.ts
|
|
7721
|
+
const buildListenerCleanupMismatchMessage = (eventName, registrationCapture, removalCapture, callbackComparison) => {
|
|
7722
|
+
const hasCallbackMismatch = callbackComparison === "different";
|
|
7723
|
+
if (hasCallbackMismatch && registrationCapture !== removalCapture) return `The cleanup removes \`${eventName}\` with a different callback binding and capture ${String(removalCapture)}, but it was registered with capture ${String(registrationCapture)}. Pass the same callback binding and capture flag to both EventTarget calls.`;
|
|
7724
|
+
if (hasCallbackMismatch) return `The cleanup removes \`${eventName}\` with a different callback binding than the one registered, so \`removeEventListener\` cannot detach that listener. Pass the same callback binding to both calls.`;
|
|
7725
|
+
return `The cleanup removes \`${eventName}\` with capture ${String(removalCapture)}, but it was registered with capture ${String(registrationCapture)}. \`removeEventListener\` must use the same capture flag as \`addEventListener\`.`;
|
|
7726
|
+
};
|
|
7727
|
+
//#endregion
|
|
7728
|
+
//#region src/plugin/rules/state-and-effects/utils/callback-may-register-event-listener.ts
|
|
7729
|
+
const callbackMayRegisterEventListener = (callbackNode) => {
|
|
7730
|
+
let foundCall = false;
|
|
7731
|
+
walkAst(callbackNode, (child) => {
|
|
7732
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
7733
|
+
foundCall = true;
|
|
7734
|
+
return false;
|
|
7735
|
+
}
|
|
7736
|
+
});
|
|
7737
|
+
return foundCall;
|
|
7738
|
+
};
|
|
7739
|
+
//#endregion
|
|
7740
|
+
//#region src/plugin/rules/state-and-effects/utils/compare-listener-callback-identities.ts
|
|
7741
|
+
const compareListenerCallbackIdentities = (registrationIdentity, removalIdentity) => {
|
|
7742
|
+
if (registrationIdentity.node === removalIdentity.node) return "same";
|
|
7743
|
+
if (registrationIdentity.isConcreteFunction && removalIdentity.isConcreteFunction) return "different";
|
|
7744
|
+
return "unknown";
|
|
7745
|
+
};
|
|
7746
|
+
//#endregion
|
|
7747
|
+
//#region src/plugin/rules/state-and-effects/utils/does-listener-analysis-abort-controller.ts
|
|
7748
|
+
const doesListenerAnalysisAbortController = (analysis, controllerSymbolId) => analysis.abortedControllerSymbolIds.has(controllerSymbolId) || analysis.exhaustiveBranches.some(({ alternate, consequent }) => doesListenerAnalysisAbortController(alternate, controllerSymbolId) && doesListenerAnalysisAbortController(consequent, controllerSymbolId));
|
|
7749
|
+
//#endregion
|
|
7750
|
+
//#region src/plugin/rules/state-and-effects/utils/compare-listener-target-keys.ts
|
|
7751
|
+
const compareListenerTargetKeys = (firstTargetKey, secondTargetKey) => {
|
|
7752
|
+
if (firstTargetKey === secondTargetKey) return "same";
|
|
7753
|
+
if (firstTargetKey.startsWith("global:") && secondTargetKey.startsWith("global:")) return "different";
|
|
7754
|
+
if (firstTargetKey.startsWith("fresh:") || secondTargetKey.startsWith("fresh:")) return "different";
|
|
7755
|
+
return "unknown";
|
|
7756
|
+
};
|
|
7757
|
+
//#endregion
|
|
7758
|
+
//#region src/plugin/rules/state-and-effects/utils/does-listener-analysis-cancel-registration.ts
|
|
7759
|
+
const cancellationResult = (analysis, registration) => {
|
|
7760
|
+
if (registration.abortControllerSymbolId !== null && doesListenerAnalysisAbortController(analysis, registration.abortControllerSymbolId)) return "cancelled";
|
|
7761
|
+
let result = analysis.hasUnknownRemovalCall || registration.abortControllerSymbolId !== null && analysis.hasUnknownAbortCall ? "unknown" : "not-cancelled";
|
|
7762
|
+
for (const removal of analysis.removals) {
|
|
7763
|
+
if (removal.eventName !== registration.eventName || removal.capture !== registration.capture || removal.callbackIdentity === null || compareListenerCallbackIdentities(registration.callbackIdentity, removal.callbackIdentity) !== "same") continue;
|
|
7764
|
+
const targetComparison = compareListenerTargetKeys(registration.targetKey, removal.targetKey);
|
|
7765
|
+
if (targetComparison === "same") return "cancelled";
|
|
7766
|
+
if (targetComparison === "unknown") result = "unknown";
|
|
7767
|
+
}
|
|
7768
|
+
for (const { alternate, consequent } of analysis.exhaustiveBranches) {
|
|
7769
|
+
const alternateResult = cancellationResult(alternate, registration);
|
|
7770
|
+
const consequentResult = cancellationResult(consequent, registration);
|
|
7771
|
+
if (alternateResult === "cancelled" && consequentResult === "cancelled") return "cancelled";
|
|
7772
|
+
if (alternateResult !== "not-cancelled" && consequentResult !== "not-cancelled") result = "unknown";
|
|
7773
|
+
}
|
|
7774
|
+
return result;
|
|
7775
|
+
};
|
|
7776
|
+
const doesListenerAnalysisCancelRegistration = (analysis, registration) => cancellationResult(analysis, registration) !== "not-cancelled";
|
|
7777
|
+
//#endregion
|
|
7778
|
+
//#region src/plugin/rules/state-and-effects/utils/static-member-property-name.ts
|
|
7779
|
+
const getStaticMemberPropertyName = (node) => {
|
|
7780
|
+
if (!node) return null;
|
|
7781
|
+
const unwrappedNode = stripParenExpression(node);
|
|
7782
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
7783
|
+
if (!unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier")) return unwrappedNode.property.name;
|
|
7784
|
+
if (unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Literal") && typeof unwrappedNode.property.value === "string") return unwrappedNode.property.value;
|
|
7785
|
+
return null;
|
|
7786
|
+
};
|
|
7787
|
+
//#endregion
|
|
7788
|
+
//#region src/plugin/rules/state-and-effects/utils/is-listener-path-ambiguous.ts
|
|
7789
|
+
const PATH_AMBIGUOUS_ANCESTOR_TYPES = new Set([
|
|
7790
|
+
"CatchClause",
|
|
7791
|
+
"ConditionalExpression",
|
|
7792
|
+
"DoWhileStatement",
|
|
7793
|
+
"ForInStatement",
|
|
7794
|
+
"ForOfStatement",
|
|
7795
|
+
"ForStatement",
|
|
7796
|
+
"IfStatement",
|
|
7797
|
+
"LogicalExpression",
|
|
7798
|
+
"SwitchCase",
|
|
7799
|
+
"SwitchStatement",
|
|
7800
|
+
"TryStatement",
|
|
7801
|
+
"WhileStatement"
|
|
7802
|
+
]);
|
|
7803
|
+
const isGuaranteedChild = (parentNode, childNode) => {
|
|
7804
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
7805
|
+
if (parentNode.test === childNode) return true;
|
|
7806
|
+
const staticTestValue = readStaticBoolean(parentNode.test);
|
|
7807
|
+
return parentNode.consequent === childNode && staticTestValue === true || parentNode.alternate === childNode && staticTestValue === false;
|
|
7808
|
+
}
|
|
7809
|
+
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
7810
|
+
if (parentNode.test === childNode) return true;
|
|
7811
|
+
const staticTestValue = readStaticBoolean(parentNode.test);
|
|
7812
|
+
return parentNode.consequent === childNode && staticTestValue === true || parentNode.alternate === childNode && staticTestValue === false;
|
|
7813
|
+
}
|
|
7814
|
+
if (isNodeOfType(parentNode, "LogicalExpression")) {
|
|
7815
|
+
if (parentNode.left === childNode) return true;
|
|
7816
|
+
if (parentNode.right !== childNode) return false;
|
|
7817
|
+
const staticLeftValue = readStaticBoolean(parentNode.left);
|
|
7818
|
+
return parentNode.operator === "&&" && staticLeftValue === true || parentNode.operator === "||" && staticLeftValue === false;
|
|
7819
|
+
}
|
|
7820
|
+
if (isNodeOfType(parentNode, "TryStatement")) return parentNode.finalizer === childNode;
|
|
7821
|
+
if (isNodeOfType(parentNode, "SwitchStatement")) return parentNode.discriminant === childNode;
|
|
7822
|
+
if (isNodeOfType(parentNode, "ForStatement")) return parentNode.init === childNode || parentNode.test === childNode;
|
|
7823
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.right === childNode;
|
|
7824
|
+
if (isNodeOfType(parentNode, "WhileStatement")) return parentNode.test === childNode;
|
|
7825
|
+
return false;
|
|
7826
|
+
};
|
|
7827
|
+
const isListenerPathAmbiguous = (node, bodyNode, allowAmbiguousChild) => {
|
|
7828
|
+
let currentNode = node;
|
|
7829
|
+
while (currentNode && currentNode !== bodyNode) {
|
|
7830
|
+
const parentNode = currentNode.parent;
|
|
7831
|
+
if (!parentNode) return true;
|
|
7832
|
+
if (PATH_AMBIGUOUS_ANCESTOR_TYPES.has(parentNode.type) && !isGuaranteedChild(parentNode, currentNode) && !allowAmbiguousChild?.(parentNode, currentNode)) return true;
|
|
7833
|
+
currentNode = parentNode;
|
|
7834
|
+
}
|
|
7835
|
+
return false;
|
|
7836
|
+
};
|
|
7837
|
+
//#endregion
|
|
7838
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-event-listener-capture.ts
|
|
7839
|
+
const resolveEventListenerCapture = (optionsNode) => {
|
|
7840
|
+
if (!optionsNode) return false;
|
|
7841
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
7842
|
+
if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? unwrappedOptions.value : null;
|
|
7843
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
|
|
7844
|
+
let capture = false;
|
|
7845
|
+
for (const property of unwrappedOptions.properties) {
|
|
7846
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
7847
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
7848
|
+
if (propertyName === null) return null;
|
|
7849
|
+
if (!property.computed && propertyName === "__proto__") return null;
|
|
7850
|
+
if (propertyName !== "capture") continue;
|
|
7851
|
+
const propertyValue = stripParenExpression(property.value);
|
|
7852
|
+
if (!isNodeOfType(propertyValue, "Literal") || typeof propertyValue.value !== "boolean") return null;
|
|
7853
|
+
capture = propertyValue.value;
|
|
7854
|
+
}
|
|
7855
|
+
return capture;
|
|
7856
|
+
};
|
|
7857
|
+
//#endregion
|
|
7858
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-static-once-option.ts
|
|
7859
|
+
const resolveStaticOnceOption = (optionsNode) => {
|
|
7860
|
+
if (!optionsNode) return false;
|
|
7861
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
7862
|
+
if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? false : null;
|
|
7863
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
|
|
7864
|
+
let once = false;
|
|
7865
|
+
for (const property of unwrappedOptions.properties) {
|
|
7866
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
7867
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
7868
|
+
if (propertyName === null || !property.computed && propertyName === "__proto__") return null;
|
|
7869
|
+
if (propertyName !== "once") continue;
|
|
7870
|
+
const propertyValue = stripParenExpression(property.value);
|
|
7871
|
+
if (!isNodeOfType(propertyValue, "Literal") || typeof propertyValue.value !== "boolean") return null;
|
|
7872
|
+
once = propertyValue.value;
|
|
7873
|
+
}
|
|
7874
|
+
return once;
|
|
7875
|
+
};
|
|
7876
|
+
//#endregion
|
|
7877
|
+
//#region src/plugin/rules/state-and-effects/effect-listener-cleanup-mismatch.ts
|
|
7878
|
+
const LISTENER_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
7879
|
+
const hasOnlyReadReferences = (symbol) => symbol.references.every((reference) => reference.flag === "read");
|
|
7880
|
+
const isStableSymbol = (symbol) => symbol.kind === "const" || symbol.kind === "import" || (symbol.kind === "function" || symbol.kind === "parameter") && hasOnlyReadReferences(symbol);
|
|
7881
|
+
const isPlainConstSymbol = (symbol) => symbol.kind === "const" && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") && symbol.declarationNode.id === symbol.bindingIdentifier;
|
|
7882
|
+
const resolveAliasedSymbol = (identifier, context, visitedSymbolIds) => {
|
|
7883
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
7884
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
7885
|
+
if (!symbol || !isStableSymbol(symbol) || visitedSymbolIds.has(symbol.id)) return null;
|
|
7886
|
+
if (symbol.kind === "const" && !isPlainConstSymbol(symbol)) return null;
|
|
7887
|
+
visitedSymbolIds.add(symbol.id);
|
|
7888
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
7889
|
+
if (symbol.kind === "const" && isNodeOfType(initializer, "Identifier")) {
|
|
7890
|
+
const resolvedAlias = resolveAliasedSymbol(initializer, context, visitedSymbolIds);
|
|
7891
|
+
if (resolvedAlias) return resolvedAlias;
|
|
7892
|
+
}
|
|
7893
|
+
return symbol;
|
|
7894
|
+
};
|
|
7895
|
+
const resolveCallbackIdentity = (callbackNode, context) => {
|
|
7896
|
+
if (!callbackNode) return null;
|
|
7897
|
+
const unwrappedCallback = stripParenExpression(callbackNode);
|
|
7898
|
+
if (isFunctionLike$1(unwrappedCallback)) return {
|
|
7899
|
+
node: unwrappedCallback,
|
|
7900
|
+
isConcreteFunction: true
|
|
7901
|
+
};
|
|
7902
|
+
if (!isNodeOfType(unwrappedCallback, "Identifier")) return null;
|
|
7903
|
+
const symbol = resolveAliasedSymbol(unwrappedCallback, context, /* @__PURE__ */ new Set());
|
|
7904
|
+
if (!symbol) return null;
|
|
7905
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
7906
|
+
if (initializer && isFunctionLike$1(initializer)) return {
|
|
7907
|
+
node: initializer,
|
|
7908
|
+
isConcreteFunction: true
|
|
7909
|
+
};
|
|
7910
|
+
return {
|
|
7911
|
+
node: symbol.bindingIdentifier,
|
|
7912
|
+
isConcreteFunction: false
|
|
7913
|
+
};
|
|
7914
|
+
};
|
|
7915
|
+
const resolveTargetKey = (targetNode, context) => {
|
|
7916
|
+
const unwrappedTarget = stripParenExpression(targetNode);
|
|
7917
|
+
if (isNodeOfType(unwrappedTarget, "Identifier")) {
|
|
7918
|
+
const symbol = resolveAliasedSymbol(unwrappedTarget, context, /* @__PURE__ */ new Set());
|
|
7919
|
+
if (symbol) {
|
|
7920
|
+
if (symbol.kind === "import" || symbol.kind === "parameter" || symbol.kind === "function") return null;
|
|
7921
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
7922
|
+
if (isNodeOfType(initializer, "NewExpression") && isNodeOfType(initializer.callee, "Identifier") && !context.scopes.isGlobalReference(initializer.callee)) return null;
|
|
7923
|
+
if (isNodeOfType(initializer, "NewExpression")) return `fresh:${symbol.id}`;
|
|
7924
|
+
return `symbol:${symbol.id}`;
|
|
7925
|
+
}
|
|
7926
|
+
if (context.scopes.isGlobalReference(unwrappedTarget)) return `global:${unwrappedTarget.name}`;
|
|
7927
|
+
return null;
|
|
7928
|
+
}
|
|
7929
|
+
if (isNodeOfType(unwrappedTarget, "MemberExpression") && !unwrappedTarget.computed && isNodeOfType(unwrappedTarget.property, "Identifier")) {
|
|
7930
|
+
const objectKey = resolveTargetKey(unwrappedTarget.object, context);
|
|
7931
|
+
if (unwrappedTarget.property.name === "document" && (objectKey === "global:window" || objectKey === "global:globalThis")) return "global:document";
|
|
7932
|
+
if (unwrappedTarget.property.name === "window" && (objectKey === "global:window" || objectKey === "global:globalThis")) return "global:window";
|
|
7933
|
+
return objectKey === null ? null : `${objectKey}.${unwrappedTarget.property.name}`;
|
|
7934
|
+
}
|
|
7935
|
+
return null;
|
|
7936
|
+
};
|
|
7937
|
+
const resolveStaticEventName = (eventNode, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
7938
|
+
if (!eventNode) return null;
|
|
7939
|
+
const unwrappedEvent = stripParenExpression(eventNode);
|
|
7940
|
+
if (isNodeOfType(unwrappedEvent, "Literal") && typeof unwrappedEvent.value === "string") return unwrappedEvent.value;
|
|
7941
|
+
if (isNodeOfType(unwrappedEvent, "TemplateLiteral") && unwrappedEvent.expressions.length === 0) return unwrappedEvent.quasis[0]?.value.cooked ?? "";
|
|
7942
|
+
if (!isNodeOfType(unwrappedEvent, "Identifier")) return null;
|
|
7943
|
+
const symbol = context.scopes.symbolFor(unwrappedEvent);
|
|
7944
|
+
if (!symbol || !isPlainConstSymbol(symbol) || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return null;
|
|
7945
|
+
visitedSymbolIds.add(symbol.id);
|
|
7946
|
+
return resolveStaticEventName(symbol.initializer, context, visitedSymbolIds);
|
|
7947
|
+
};
|
|
7948
|
+
const readStaticEventDispatch = (node, context) => {
|
|
7949
|
+
const targetNode = readDirectMemberReceiver(node.callee, "dispatchEvent", context);
|
|
7950
|
+
const eventArgument = node.arguments?.[0];
|
|
7951
|
+
if (!eventArgument) return null;
|
|
7952
|
+
const eventNode = stripParenExpression(eventArgument);
|
|
7953
|
+
if (!targetNode || !isNodeOfType(eventNode, "NewExpression") || !isNodeOfType(eventNode.callee, "Identifier") || eventNode.callee.name !== "Event" || !context.scopes.isGlobalReference(eventNode.callee)) return null;
|
|
7954
|
+
const target = stripParenExpression(targetNode);
|
|
7955
|
+
if (!isNodeOfType(target, "Identifier")) return null;
|
|
7956
|
+
const targetSymbol = resolveAliasedSymbol(target, context, /* @__PURE__ */ new Set());
|
|
7957
|
+
const targetInitializer = targetSymbol?.initializer ? stripParenExpression(targetSymbol.initializer) : null;
|
|
7958
|
+
if (!isNodeOfType(targetInitializer, "NewExpression") || !isNodeOfType(targetInitializer.callee, "Identifier") || targetInitializer.callee.name !== "EventTarget" || !context.scopes.isGlobalReference(targetInitializer.callee)) return null;
|
|
7959
|
+
const targetKey = resolveTargetKey(targetNode, context);
|
|
7960
|
+
const eventName = resolveStaticEventName(eventNode.arguments?.[0], context);
|
|
7961
|
+
return targetKey !== null && eventName !== null ? {
|
|
7962
|
+
eventName,
|
|
7963
|
+
targetKey
|
|
7964
|
+
} : null;
|
|
7965
|
+
};
|
|
7966
|
+
const resolveLocalAbortControllerSymbolId = (controllerNode, context) => {
|
|
7967
|
+
const unwrappedController = stripParenExpression(controllerNode);
|
|
7968
|
+
if (!isNodeOfType(unwrappedController, "Identifier")) return null;
|
|
7969
|
+
const controllerSymbol = resolveAliasedSymbol(unwrappedController, context, /* @__PURE__ */ new Set());
|
|
7970
|
+
if (!controllerSymbol || controllerSymbol.kind !== "const" || !controllerSymbol.initializer) return null;
|
|
7971
|
+
const initializer = stripParenExpression(controllerSymbol.initializer);
|
|
7972
|
+
if (!isNodeOfType(initializer, "NewExpression") || !isNodeOfType(initializer.callee, "Identifier") || initializer.callee.name !== "AbortController" || !context.scopes.isGlobalReference(initializer.callee)) return null;
|
|
7973
|
+
return controllerSymbol.id;
|
|
7974
|
+
};
|
|
7975
|
+
const readDirectMemberReceiver = (memberNode, memberName, context) => {
|
|
7976
|
+
if (!memberNode) return null;
|
|
7977
|
+
const unwrappedMember = stripParenExpression(memberNode);
|
|
7978
|
+
if (!isNodeOfType(unwrappedMember, "MemberExpression")) return null;
|
|
7979
|
+
if ((getStaticMemberPropertyName(unwrappedMember) ?? (context && unwrappedMember.computed ? resolveStaticEventName(unwrappedMember.property, context) : null)) !== memberName) return null;
|
|
7980
|
+
return unwrappedMember.object;
|
|
7981
|
+
};
|
|
7982
|
+
const isSignalObjectPatternBinding = (patternNode, bindingIdentifier) => {
|
|
7983
|
+
if (!isNodeOfType(patternNode, "ObjectPattern")) return false;
|
|
7984
|
+
return patternNode.properties.some((property) => {
|
|
7985
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
7986
|
+
if (getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") return false;
|
|
7987
|
+
return (isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value) === bindingIdentifier;
|
|
7988
|
+
});
|
|
7989
|
+
};
|
|
7990
|
+
const resolveSignalAbortControllerSymbolId = (signalNode, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
7991
|
+
const unwrappedSignal = stripParenExpression(signalNode);
|
|
7992
|
+
const directControllerNode = readDirectMemberReceiver(unwrappedSignal, "signal");
|
|
7993
|
+
if (directControllerNode) return resolveLocalAbortControllerSymbolId(directControllerNode, context);
|
|
7994
|
+
if (!isNodeOfType(unwrappedSignal, "Identifier")) return null;
|
|
7995
|
+
const signalSymbol = context.scopes.symbolFor(unwrappedSignal);
|
|
7996
|
+
if (!signalSymbol || signalSymbol.kind !== "const" || !signalSymbol.initializer || visitedSymbolIds.has(signalSymbol.id) || !hasOnlyReadReferences(signalSymbol)) return null;
|
|
7997
|
+
visitedSymbolIds.add(signalSymbol.id);
|
|
7998
|
+
const declarationNode = signalSymbol.declarationNode;
|
|
7999
|
+
if (!isNodeOfType(declarationNode, "VariableDeclarator")) return null;
|
|
8000
|
+
if (isNodeOfType(declarationNode.id, "Identifier") && declarationNode.id === signalSymbol.bindingIdentifier) {
|
|
8001
|
+
const initializer = stripParenExpression(signalSymbol.initializer);
|
|
8002
|
+
if (isNodeOfType(initializer, "Identifier")) return resolveSignalAbortControllerSymbolId(initializer, context, visitedSymbolIds);
|
|
8003
|
+
const controllerNode = readDirectMemberReceiver(initializer, "signal");
|
|
8004
|
+
return controllerNode ? resolveLocalAbortControllerSymbolId(controllerNode, context) : null;
|
|
8005
|
+
}
|
|
8006
|
+
if (!isSignalObjectPatternBinding(declarationNode.id, signalSymbol.bindingIdentifier)) return null;
|
|
8007
|
+
return resolveLocalAbortControllerSymbolId(signalSymbol.initializer, context);
|
|
8008
|
+
};
|
|
8009
|
+
const readControllerAbortedCondition = (conditionNode, controllerSymbolId, context) => {
|
|
8010
|
+
const unwrappedCondition = stripParenExpression(conditionNode);
|
|
8011
|
+
if (isNodeOfType(unwrappedCondition, "UnaryExpression") && unwrappedCondition.operator === "!") {
|
|
8012
|
+
const operandCondition = readControllerAbortedCondition(unwrappedCondition.argument, controllerSymbolId, context);
|
|
8013
|
+
if (operandCondition === "aborted") return "not-aborted";
|
|
8014
|
+
if (operandCondition === "not-aborted") return "aborted";
|
|
8015
|
+
return null;
|
|
8016
|
+
}
|
|
8017
|
+
if (isNodeOfType(unwrappedCondition, "BinaryExpression")) {
|
|
8018
|
+
const leftBoolean = readStaticBoolean(unwrappedCondition.left);
|
|
8019
|
+
const rightBoolean = readStaticBoolean(unwrappedCondition.right);
|
|
8020
|
+
const memberNode = leftBoolean === null ? unwrappedCondition.left : rightBoolean === null ? unwrappedCondition.right : null;
|
|
8021
|
+
const comparedBoolean = leftBoolean ?? rightBoolean;
|
|
8022
|
+
if (memberNode && comparedBoolean !== null) {
|
|
8023
|
+
const memberCondition = readControllerAbortedCondition(memberNode, controllerSymbolId, context);
|
|
8024
|
+
if (memberCondition) {
|
|
8025
|
+
const isEquality = unwrappedCondition.operator === "==" || unwrappedCondition.operator === "===";
|
|
8026
|
+
const isInequality = unwrappedCondition.operator === "!=" || unwrappedCondition.operator === "!==";
|
|
8027
|
+
const shouldInvert = isEquality && !comparedBoolean || isInequality && comparedBoolean;
|
|
8028
|
+
if (isEquality || isInequality) {
|
|
8029
|
+
if (!shouldInvert) return memberCondition;
|
|
8030
|
+
return memberCondition === "aborted" ? "not-aborted" : "aborted";
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
return null;
|
|
8035
|
+
}
|
|
8036
|
+
const signalNode = readDirectMemberReceiver(unwrappedCondition, "aborted");
|
|
8037
|
+
if (!signalNode) return null;
|
|
8038
|
+
return resolveSignalAbortControllerSymbolId(signalNode, context) === controllerSymbolId ? "aborted" : null;
|
|
8039
|
+
};
|
|
8040
|
+
const isAbortGuardForChild = (parentNode, childNode, controllerSymbolId, context) => {
|
|
8041
|
+
if (isNodeOfType(parentNode, "IfStatement") || isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
8042
|
+
const condition = readControllerAbortedCondition(parentNode.test, controllerSymbolId, context);
|
|
8043
|
+
return parentNode.consequent === childNode && condition === "not-aborted" || parentNode.alternate === childNode && condition === "aborted";
|
|
8044
|
+
}
|
|
8045
|
+
if (!isNodeOfType(parentNode, "LogicalExpression") || parentNode.right !== childNode) return false;
|
|
8046
|
+
const condition = readControllerAbortedCondition(parentNode.left, controllerSymbolId, context);
|
|
8047
|
+
return parentNode.operator === "&&" && condition === "not-aborted" || parentNode.operator === "||" && condition === "aborted";
|
|
8048
|
+
};
|
|
8049
|
+
const isAbortGuaranteedByPath = (node, bodyNode, controllerSymbolId, context) => !isListenerPathAmbiguous(node, bodyNode, (parentNode, childNode) => isAbortGuardForChild(parentNode, childNode, controllerSymbolId, context));
|
|
8050
|
+
const resolveBoundAbortControllerSymbolId = (callNode, context) => {
|
|
8051
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8052
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
8053
|
+
const boundAbortSymbol = resolveAliasedSymbol(callee, context, /* @__PURE__ */ new Set());
|
|
8054
|
+
if (!boundAbortSymbol?.initializer) return null;
|
|
8055
|
+
const initializer = stripParenExpression(boundAbortSymbol.initializer);
|
|
8056
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
8057
|
+
const abortMethodNode = readDirectMemberReceiver(initializer.callee, "bind");
|
|
8058
|
+
if (!abortMethodNode) return null;
|
|
8059
|
+
const controllerNode = readDirectMemberReceiver(abortMethodNode, "abort");
|
|
8060
|
+
const boundThisNode = initializer.arguments?.[0];
|
|
8061
|
+
if (!controllerNode || !boundThisNode) return null;
|
|
8062
|
+
const controllerSymbolId = resolveLocalAbortControllerSymbolId(controllerNode, context);
|
|
8063
|
+
const boundThisSymbolId = resolveLocalAbortControllerSymbolId(boundThisNode, context);
|
|
8064
|
+
return controllerSymbolId !== null && controllerSymbolId === boundThisSymbolId ? controllerSymbolId : null;
|
|
8065
|
+
};
|
|
8066
|
+
const resolveRegistrationCancellation = (optionsNode, context) => {
|
|
8067
|
+
const noCancellation = {
|
|
8068
|
+
abortControllerSymbolId: null,
|
|
8069
|
+
hasUnknownCancellation: false
|
|
8070
|
+
};
|
|
8071
|
+
if (!optionsNode) return noCancellation;
|
|
8072
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
8073
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return noCancellation;
|
|
8074
|
+
let resolvedCancellation = null;
|
|
8075
|
+
for (const property of unwrappedOptions.properties) {
|
|
8076
|
+
if (!isNodeOfType(property, "Property")) return noCancellation;
|
|
8077
|
+
if (getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") continue;
|
|
8078
|
+
if (resolvedCancellation) return {
|
|
8079
|
+
abortControllerSymbolId: null,
|
|
8080
|
+
hasUnknownCancellation: true
|
|
8081
|
+
};
|
|
8082
|
+
const abortControllerSymbolId = resolveSignalAbortControllerSymbolId(property.value, context);
|
|
8083
|
+
resolvedCancellation = {
|
|
8084
|
+
abortControllerSymbolId,
|
|
8085
|
+
hasUnknownCancellation: abortControllerSymbolId === null
|
|
8086
|
+
};
|
|
8087
|
+
}
|
|
8088
|
+
return resolvedCancellation ?? noCancellation;
|
|
8089
|
+
};
|
|
8090
|
+
const readListenerCandidate = (node, methodName, context) => {
|
|
8091
|
+
const targetNode = readDirectMemberReceiver(node.callee, methodName, context);
|
|
8092
|
+
if (!targetNode) return null;
|
|
8093
|
+
const targetKey = resolveTargetKey(targetNode, context);
|
|
8094
|
+
const eventName = resolveStaticEventName(node.arguments?.[0], context);
|
|
8095
|
+
const callbackIdentity = resolveCallbackIdentity(node.arguments?.[1], context);
|
|
8096
|
+
const capture = resolveEventListenerCapture(node.arguments?.[2]);
|
|
8097
|
+
if (targetKey === null || eventName === null) return null;
|
|
8098
|
+
return {
|
|
8099
|
+
node,
|
|
8100
|
+
targetKey,
|
|
8101
|
+
eventName,
|
|
8102
|
+
callbackIdentity,
|
|
8103
|
+
capture
|
|
8104
|
+
};
|
|
8105
|
+
};
|
|
8106
|
+
const readDestructuredRemovalCandidate = (node, context) => {
|
|
8107
|
+
const methodNode = readDirectMemberReceiver(node.callee, "call");
|
|
8108
|
+
if (!methodNode || !isNodeOfType(methodNode, "Identifier")) return null;
|
|
8109
|
+
const methodSymbol = context.scopes.symbolFor(methodNode);
|
|
8110
|
+
if (!methodSymbol || methodSymbol.kind !== "const" || !hasOnlyReadReferences(methodSymbol) || !isNodeOfType(methodSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
8111
|
+
const declaration = methodSymbol.declarationNode;
|
|
8112
|
+
if (!isNodeOfType(declaration.id, "ObjectPattern") || !declaration.init) return null;
|
|
8113
|
+
if (!declaration.id.properties.some((property) => {
|
|
8114
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8115
|
+
const bindingNode = isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value;
|
|
8116
|
+
return getStaticPropertyKeyName(property, { allowComputedString: true }) === "removeEventListener" && bindingNode === methodSymbol.bindingIdentifier;
|
|
8117
|
+
})) return null;
|
|
8118
|
+
const targetNode = node.arguments?.[0];
|
|
8119
|
+
if (!targetNode) return null;
|
|
8120
|
+
const declarationTargetKey = resolveTargetKey(declaration.init, context);
|
|
8121
|
+
const targetKey = resolveTargetKey(targetNode, context);
|
|
8122
|
+
if (declarationTargetKey === null || targetKey === null || declarationTargetKey !== targetKey) return null;
|
|
8123
|
+
const eventName = resolveStaticEventName(node.arguments?.[1], context);
|
|
8124
|
+
if (eventName === null) return null;
|
|
8125
|
+
return {
|
|
8126
|
+
node,
|
|
8127
|
+
targetKey,
|
|
8128
|
+
eventName,
|
|
8129
|
+
callbackIdentity: resolveCallbackIdentity(node.arguments?.[2], context),
|
|
8130
|
+
capture: resolveEventListenerCapture(node.arguments?.[3])
|
|
8131
|
+
};
|
|
8132
|
+
};
|
|
8133
|
+
const resolveReturnedCleanupBody = (returnedValue, context) => {
|
|
8134
|
+
if (!returnedValue) return null;
|
|
8135
|
+
const unwrappedValue = stripParenExpression(returnedValue);
|
|
8136
|
+
if (isFunctionLike$1(unwrappedValue)) return unwrappedValue.body;
|
|
8137
|
+
if (!isNodeOfType(unwrappedValue, "Identifier")) return null;
|
|
8138
|
+
const symbol = resolveAliasedSymbol(unwrappedValue, context, /* @__PURE__ */ new Set());
|
|
8139
|
+
if (!symbol || !symbol.initializer) return null;
|
|
8140
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
8141
|
+
return isFunctionLike$1(initializer) ? initializer.body : null;
|
|
8142
|
+
};
|
|
8143
|
+
const collectEffectListenerInputs = (effectBody, context) => {
|
|
8144
|
+
const registrations = [];
|
|
8145
|
+
const cleanupBodies = [];
|
|
8146
|
+
const listenerRegistrationCountsByTarget = /* @__PURE__ */ new Map();
|
|
8147
|
+
let returnStatementCount = 0;
|
|
8148
|
+
if (!isNodeOfType(effectBody, "BlockStatement")) return {
|
|
8149
|
+
registrations,
|
|
8150
|
+
cleanupBodies,
|
|
8151
|
+
hasCanonicalCleanupReturn: false,
|
|
8152
|
+
returnStatementCount
|
|
8153
|
+
};
|
|
8154
|
+
const finalEffectStatement = effectBody.body[effectBody.body.length - 1];
|
|
8155
|
+
const hasCanonicalCleanupReturn = isNodeOfType(finalEffectStatement, "ReturnStatement");
|
|
8156
|
+
walkAst(effectBody, (child) => {
|
|
8157
|
+
if (child !== effectBody && isFunctionLike$1(child)) return false;
|
|
8158
|
+
if (isNodeOfType(child, "ClassDeclaration") || isNodeOfType(child, "ClassExpression")) return false;
|
|
8159
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
8160
|
+
if (isListenerPathAmbiguous(child, effectBody)) return;
|
|
8161
|
+
const registrationTarget = readDirectMemberReceiver(child.callee, "addEventListener", context);
|
|
8162
|
+
const registrationTargetKey = registrationTarget ? resolveTargetKey(registrationTarget, context) : null;
|
|
8163
|
+
if (registrationTargetKey !== null) listenerRegistrationCountsByTarget.set(registrationTargetKey, (listenerRegistrationCountsByTarget.get(registrationTargetKey) ?? 0) + 1);
|
|
8164
|
+
const candidate = readListenerCandidate(child, "addEventListener", context);
|
|
8165
|
+
if (candidate?.callbackIdentity && candidate.capture !== null) {
|
|
8166
|
+
const registrationCancellation = resolveRegistrationCancellation(candidate.node.arguments?.[2], context);
|
|
8167
|
+
registrations.push({
|
|
8168
|
+
node: candidate.node,
|
|
8169
|
+
targetKey: candidate.targetKey,
|
|
8170
|
+
eventName: candidate.eventName,
|
|
8171
|
+
callbackIdentity: candidate.callbackIdentity,
|
|
8172
|
+
capture: candidate.capture,
|
|
8173
|
+
once: resolveStaticOnceOption(candidate.node.arguments?.[2]) === true,
|
|
8174
|
+
abortControllerSymbolId: registrationCancellation.abortControllerSymbolId,
|
|
8175
|
+
hasUnknownCancellation: registrationCancellation.hasUnknownCancellation
|
|
8176
|
+
});
|
|
8177
|
+
}
|
|
8178
|
+
const eventDispatch = readStaticEventDispatch(child, context);
|
|
8179
|
+
if (eventDispatch) {
|
|
8180
|
+
const dispatchedRegistrations = registrations.filter((registration) => registration.targetKey === eventDispatch.targetKey && registration.eventName === eventDispatch.eventName);
|
|
8181
|
+
const dispatchedRegistration = dispatchedRegistrations[0];
|
|
8182
|
+
if (dispatchedRegistrations.length === 1 && dispatchedRegistration?.once && listenerRegistrationCountsByTarget.get(eventDispatch.targetKey) === 1 && dispatchedRegistration.callbackIdentity.isConcreteFunction && !callbackMayRegisterEventListener(dispatchedRegistration.callbackIdentity.node)) registrations.splice(registrations.indexOf(dispatchedRegistration), 1);
|
|
8183
|
+
}
|
|
8184
|
+
return;
|
|
8185
|
+
}
|
|
8186
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
8187
|
+
returnStatementCount += 1;
|
|
8188
|
+
const cleanupBody = resolveReturnedCleanupBody(child.argument, context);
|
|
8189
|
+
if (cleanupBody) cleanupBodies.push(cleanupBody);
|
|
8190
|
+
}
|
|
8191
|
+
});
|
|
8192
|
+
return {
|
|
8193
|
+
registrations,
|
|
8194
|
+
cleanupBodies,
|
|
8195
|
+
hasCanonicalCleanupReturn,
|
|
8196
|
+
returnStatementCount
|
|
8197
|
+
};
|
|
8198
|
+
};
|
|
8199
|
+
const resolveCalledCleanup = (callNode, context) => {
|
|
8200
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8201
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
8202
|
+
const cleanupSymbol = resolveAliasedSymbol(callee, context, /* @__PURE__ */ new Set());
|
|
8203
|
+
if (!cleanupSymbol?.initializer || cleanupSymbol.kind !== "const" && cleanupSymbol.kind !== "function") return null;
|
|
8204
|
+
const initializer = stripParenExpression(cleanupSymbol.initializer);
|
|
8205
|
+
if (!isFunctionLike$1(initializer) || initializer.async || initializer.generator) return null;
|
|
8206
|
+
return {
|
|
8207
|
+
body: initializer.body,
|
|
8208
|
+
symbolId: cleanupSymbol.id
|
|
8209
|
+
};
|
|
8210
|
+
};
|
|
8211
|
+
const analyzeCleanupBody = (cleanupBody, context, visitedCleanupSymbolIds = /* @__PURE__ */ new Set(), isLoopBody = false) => {
|
|
8212
|
+
const removals = [];
|
|
8213
|
+
const abortedControllerSymbolIds = /* @__PURE__ */ new Set();
|
|
8214
|
+
const exhaustiveBranches = [];
|
|
8215
|
+
let hasAmbiguousReachability = false;
|
|
8216
|
+
let hasUnknownAbortCall = false;
|
|
8217
|
+
let hasUnknownRemovalCall = false;
|
|
8218
|
+
const finalCleanupStatement = isNodeOfType(cleanupBody, "BlockStatement") ? cleanupBody.body[cleanupBody.body.length - 1] : null;
|
|
8219
|
+
const addCleanupAnalysis = (analysis) => {
|
|
8220
|
+
removals.push(...analysis.removals);
|
|
8221
|
+
hasUnknownAbortCall ||= analysis.hasUnknownAbortCall;
|
|
8222
|
+
hasUnknownRemovalCall ||= analysis.hasUnknownRemovalCall;
|
|
8223
|
+
for (const controllerSymbolId of analysis.abortedControllerSymbolIds) abortedControllerSymbolIds.add(controllerSymbolId);
|
|
8224
|
+
exhaustiveBranches.push(...analysis.exhaustiveBranches);
|
|
8225
|
+
};
|
|
8226
|
+
const addGuaranteedLoopPrefix = (loopBody) => {
|
|
8227
|
+
const loopStatements = isNodeOfType(loopBody, "BlockStatement") ? loopBody.body : [loopBody];
|
|
8228
|
+
for (const loopStatement of loopStatements) {
|
|
8229
|
+
if (isNodeOfType(loopStatement, "BreakStatement") || isNodeOfType(loopStatement, "ContinueStatement")) return true;
|
|
8230
|
+
if (isNodeOfType(loopStatement, "BlockStatement")) {
|
|
8231
|
+
if (addGuaranteedLoopPrefix(loopStatement)) return true;
|
|
8232
|
+
continue;
|
|
8233
|
+
}
|
|
8234
|
+
if (isNodeOfType(loopStatement, "IfStatement")) {
|
|
8235
|
+
const staticTestValue = readStaticBoolean(loopStatement.test);
|
|
8236
|
+
if (staticTestValue !== null) {
|
|
8237
|
+
const testAnalysis = analyzeCleanupBody(loopStatement.test, context, new Set(visitedCleanupSymbolIds), true);
|
|
8238
|
+
if (!testAnalysis) return true;
|
|
8239
|
+
addCleanupAnalysis(testAnalysis);
|
|
8240
|
+
const guaranteedBranch = staticTestValue ? loopStatement.consequent : loopStatement.alternate;
|
|
8241
|
+
if (guaranteedBranch && addGuaranteedLoopPrefix(guaranteedBranch)) return true;
|
|
8242
|
+
continue;
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
const loopStatementAnalysis = analyzeCleanupBody(loopStatement, context, new Set(visitedCleanupSymbolIds), true);
|
|
8246
|
+
if (!loopStatementAnalysis) return true;
|
|
8247
|
+
addCleanupAnalysis(loopStatementAnalysis);
|
|
8248
|
+
}
|
|
8249
|
+
return false;
|
|
8250
|
+
};
|
|
8251
|
+
walkAst(cleanupBody, (child) => {
|
|
8252
|
+
if (child !== cleanupBody && isFunctionLike$1(child)) return false;
|
|
8253
|
+
if (isNodeOfType(child, "ClassDeclaration") || isNodeOfType(child, "ClassExpression")) return false;
|
|
8254
|
+
if (isNodeOfType(child, "ReturnStatement") && child === finalCleanupStatement) return;
|
|
8255
|
+
if (isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement") || isLoopBody && (isNodeOfType(child, "BreakStatement") || isNodeOfType(child, "ContinueStatement"))) {
|
|
8256
|
+
hasAmbiguousReachability = true;
|
|
8257
|
+
return false;
|
|
8258
|
+
}
|
|
8259
|
+
if (isNodeOfType(child, "ForOfStatement")) {
|
|
8260
|
+
const iterable = stripParenExpression(child.right);
|
|
8261
|
+
if (isNodeOfType(iterable, "ArrayExpression") && iterable.elements.some((element) => element && !isNodeOfType(element, "SpreadElement")) && !isListenerPathAmbiguous(child, cleanupBody)) addGuaranteedLoopPrefix(child.body);
|
|
8262
|
+
return false;
|
|
8263
|
+
}
|
|
8264
|
+
if ((isNodeOfType(child, "IfStatement") || isNodeOfType(child, "ConditionalExpression")) && child.alternate && readStaticBoolean(child.test) === null && !isListenerPathAmbiguous(child, cleanupBody)) {
|
|
8265
|
+
const consequentAnalysis = analyzeCleanupBody(child.consequent, context, new Set(visitedCleanupSymbolIds));
|
|
8266
|
+
const alternateAnalysis = analyzeCleanupBody(child.alternate, context, new Set(visitedCleanupSymbolIds));
|
|
8267
|
+
if (consequentAnalysis && alternateAnalysis) {
|
|
8268
|
+
exhaustiveBranches.push({
|
|
8269
|
+
alternate: alternateAnalysis,
|
|
8270
|
+
consequent: consequentAnalysis
|
|
8271
|
+
});
|
|
8272
|
+
hasUnknownAbortCall ||= consequentAnalysis.hasUnknownAbortCall && alternateAnalysis.hasUnknownAbortCall;
|
|
8273
|
+
hasUnknownRemovalCall ||= consequentAnalysis.hasUnknownRemovalCall && alternateAnalysis.hasUnknownRemovalCall;
|
|
8274
|
+
}
|
|
8275
|
+
const testAnalysis = analyzeCleanupBody(child.test, context, new Set(visitedCleanupSymbolIds));
|
|
8276
|
+
if (testAnalysis) addCleanupAnalysis(testAnalysis);
|
|
8277
|
+
return false;
|
|
8278
|
+
}
|
|
8279
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8280
|
+
const hasAmbiguousPath = isListenerPathAmbiguous(child, cleanupBody);
|
|
8281
|
+
const removalTarget = readDirectMemberReceiver(child.callee, "removeEventListener", context);
|
|
8282
|
+
if (removalTarget && !hasAmbiguousPath) {
|
|
8283
|
+
const removal = readListenerCandidate(child, "removeEventListener", context);
|
|
8284
|
+
if (removal) {
|
|
8285
|
+
removals.push(removal);
|
|
8286
|
+
hasUnknownRemovalCall ||= removal.callbackIdentity === null || removal.capture === null;
|
|
8287
|
+
} else hasUnknownRemovalCall = true;
|
|
8288
|
+
}
|
|
8289
|
+
if (!removalTarget && !hasAmbiguousPath) {
|
|
8290
|
+
const destructuredRemoval = readDestructuredRemovalCandidate(child, context);
|
|
8291
|
+
if (destructuredRemoval) removals.push(destructuredRemoval);
|
|
8292
|
+
}
|
|
8293
|
+
const controllerNode = readDirectMemberReceiver(child.callee, "abort");
|
|
8294
|
+
if (controllerNode) {
|
|
8295
|
+
const controllerSymbolId = resolveLocalAbortControllerSymbolId(controllerNode, context);
|
|
8296
|
+
if (controllerSymbolId === null) {
|
|
8297
|
+
if (!hasAmbiguousPath) hasUnknownAbortCall = true;
|
|
8298
|
+
} else if (!hasAmbiguousPath || isAbortGuaranteedByPath(child, cleanupBody, controllerSymbolId, context)) abortedControllerSymbolIds.add(controllerSymbolId);
|
|
8299
|
+
return;
|
|
8300
|
+
}
|
|
8301
|
+
if (hasAmbiguousPath) return;
|
|
8302
|
+
const boundAbortControllerSymbolId = resolveBoundAbortControllerSymbolId(child, context);
|
|
8303
|
+
if (boundAbortControllerSymbolId !== null) {
|
|
8304
|
+
abortedControllerSymbolIds.add(boundAbortControllerSymbolId);
|
|
8305
|
+
return;
|
|
8306
|
+
}
|
|
8307
|
+
const calledCleanup = resolveCalledCleanup(child, context);
|
|
8308
|
+
if (!calledCleanup || visitedCleanupSymbolIds.has(calledCleanup.symbolId)) return;
|
|
8309
|
+
visitedCleanupSymbolIds.add(calledCleanup.symbolId);
|
|
8310
|
+
const calledCleanupAnalysis = analyzeCleanupBody(calledCleanup.body, context, visitedCleanupSymbolIds);
|
|
8311
|
+
if (!calledCleanupAnalysis) return;
|
|
8312
|
+
addCleanupAnalysis(calledCleanupAnalysis);
|
|
8313
|
+
});
|
|
8314
|
+
if (hasAmbiguousReachability) return null;
|
|
8315
|
+
return {
|
|
8316
|
+
removals,
|
|
8317
|
+
abortedControllerSymbolIds,
|
|
8318
|
+
exhaustiveBranches,
|
|
8319
|
+
hasUnknownAbortCall,
|
|
8320
|
+
hasUnknownRemovalCall
|
|
8321
|
+
};
|
|
8322
|
+
};
|
|
8323
|
+
const analyzeEffectListeners = (effectBody, context) => {
|
|
8324
|
+
const effectInputs = collectEffectListenerInputs(effectBody, context);
|
|
8325
|
+
if (!effectInputs.hasCanonicalCleanupReturn || effectInputs.returnStatementCount !== 1 || effectInputs.cleanupBodies.length !== 1) return null;
|
|
8326
|
+
const removals = [];
|
|
8327
|
+
const abortedControllerSymbolIds = /* @__PURE__ */ new Set();
|
|
8328
|
+
const exhaustiveBranches = [];
|
|
8329
|
+
let hasUnknownAbortCall = false;
|
|
8330
|
+
let hasUnknownRemovalCall = false;
|
|
8331
|
+
const addAnalysis = (analysis) => {
|
|
8332
|
+
removals.push(...analysis.removals);
|
|
8333
|
+
exhaustiveBranches.push(...analysis.exhaustiveBranches);
|
|
8334
|
+
hasUnknownAbortCall ||= analysis.hasUnknownAbortCall;
|
|
8335
|
+
hasUnknownRemovalCall ||= analysis.hasUnknownRemovalCall;
|
|
8336
|
+
for (const controllerSymbolId of analysis.abortedControllerSymbolIds) abortedControllerSymbolIds.add(controllerSymbolId);
|
|
8337
|
+
};
|
|
8338
|
+
const setupAbortAnalysis = analyzeCleanupBody(effectBody, context);
|
|
8339
|
+
for (const cleanupBody of effectInputs.cleanupBodies) {
|
|
8340
|
+
const cleanupAnalysis = analyzeCleanupBody(cleanupBody, context);
|
|
8341
|
+
if (!cleanupAnalysis) return null;
|
|
8342
|
+
addAnalysis(cleanupAnalysis);
|
|
8343
|
+
}
|
|
8344
|
+
return {
|
|
8345
|
+
registrations: effectInputs.registrations,
|
|
8346
|
+
removals,
|
|
8347
|
+
abortedControllerSymbolIds,
|
|
8348
|
+
exhaustiveBranches,
|
|
8349
|
+
hasUnknownAbortCall,
|
|
8350
|
+
hasUnknownRemovalCall,
|
|
8351
|
+
setupAbortAnalysis
|
|
8352
|
+
};
|
|
8353
|
+
};
|
|
8354
|
+
const effectListenerCleanupMismatch = defineRule({
|
|
8355
|
+
id: "effect-listener-cleanup-mismatch",
|
|
8356
|
+
title: "Effect cleanup does not match its event listener",
|
|
8357
|
+
severity: "error",
|
|
8358
|
+
recommendation: "Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",
|
|
8359
|
+
create: (context) => ({ CallExpression(node) {
|
|
8360
|
+
if (!isReactApiCall(node, LISTENER_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
8361
|
+
const effectCallback = getEffectCallback(node);
|
|
8362
|
+
if (!isFunctionLike$1(effectCallback)) return;
|
|
8363
|
+
const listenerAnalysis = analyzeEffectListeners(effectCallback.body, context);
|
|
8364
|
+
if (!listenerAnalysis) return;
|
|
8365
|
+
for (const registration of listenerAnalysis.registrations) {
|
|
8366
|
+
if (registration.hasUnknownCancellation || listenerAnalysis.hasUnknownRemovalCall) continue;
|
|
8367
|
+
if (listenerAnalysis.registrations.filter((candidateRegistration) => candidateRegistration.targetKey === registration.targetKey && candidateRegistration.eventName === registration.eventName).length > 1) continue;
|
|
8368
|
+
if (registration.abortControllerSymbolId !== null && listenerAnalysis.hasUnknownAbortCall) continue;
|
|
8369
|
+
if (registration.abortControllerSymbolId !== null && listenerAnalysis.setupAbortAnalysis && doesListenerAnalysisAbortController(listenerAnalysis.setupAbortAnalysis, registration.abortControllerSymbolId)) continue;
|
|
8370
|
+
if (doesListenerAnalysisCancelRegistration(listenerAnalysis, registration)) continue;
|
|
8371
|
+
const candidateRemovals = listenerAnalysis.removals.filter((removal) => removal.targetKey === registration.targetKey && removal.eventName === registration.eventName);
|
|
8372
|
+
let firstProvableMismatch;
|
|
8373
|
+
let didFindNonMismatchCandidate = false;
|
|
8374
|
+
for (const removal of candidateRemovals) {
|
|
8375
|
+
if (!removal.callbackIdentity || removal.capture === null) {
|
|
8376
|
+
didFindNonMismatchCandidate = true;
|
|
8377
|
+
break;
|
|
8378
|
+
}
|
|
8379
|
+
const callbackComparison = compareListenerCallbackIdentities(registration.callbackIdentity, removal.callbackIdentity);
|
|
8380
|
+
if (callbackComparison === "unknown") {
|
|
8381
|
+
didFindNonMismatchCandidate = true;
|
|
8382
|
+
break;
|
|
8383
|
+
}
|
|
8384
|
+
if (callbackComparison === "same" && registration.capture === removal.capture) {
|
|
8385
|
+
didFindNonMismatchCandidate = true;
|
|
8386
|
+
break;
|
|
8387
|
+
}
|
|
8388
|
+
firstProvableMismatch ??= {
|
|
8389
|
+
removalNode: removal.node,
|
|
8390
|
+
removalCapture: removal.capture,
|
|
8391
|
+
callbackComparison
|
|
8392
|
+
};
|
|
8393
|
+
}
|
|
8394
|
+
if (candidateRemovals.length === 0 || didFindNonMismatchCandidate || !firstProvableMismatch) continue;
|
|
8395
|
+
context.report({
|
|
8396
|
+
node: firstProvableMismatch.removalNode,
|
|
8397
|
+
message: buildListenerCleanupMismatchMessage(registration.eventName, registration.capture, firstProvableMismatch.removalCapture, firstProvableMismatch.callbackComparison)
|
|
8398
|
+
});
|
|
8399
|
+
}
|
|
8400
|
+
} })
|
|
8401
|
+
});
|
|
8402
|
+
//#endregion
|
|
8403
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8404
|
+
const isReactHookName = (name) => {
|
|
8405
|
+
if (!name.startsWith("use")) return false;
|
|
8406
|
+
if (name.length === 3) return true;
|
|
8407
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
8408
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8409
|
+
};
|
|
8410
|
+
//#endregion
|
|
8411
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8412
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8413
|
+
//#endregion
|
|
8414
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
8415
|
+
const hocWrapperCalleeName = (callee) => {
|
|
8416
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8417
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8418
|
+
return null;
|
|
8419
|
+
};
|
|
8420
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
8421
|
+
let current = functionNode;
|
|
8422
|
+
for (;;) {
|
|
8423
|
+
const parent = current.parent;
|
|
8424
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
8425
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
8426
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
8427
|
+
current = parent;
|
|
8428
|
+
continue;
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8431
|
+
break;
|
|
8432
|
+
}
|
|
8433
|
+
const binding = current.parent;
|
|
8434
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
8435
|
+
return null;
|
|
8436
|
+
};
|
|
8437
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
8438
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
8439
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
8440
|
+
};
|
|
8441
|
+
//#endregion
|
|
8442
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
8443
|
+
const findEnclosingFunction = (node) => {
|
|
8444
|
+
let cursor = node.parent;
|
|
8445
|
+
while (cursor) {
|
|
8446
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
8447
|
+
cursor = cursor.parent ?? null;
|
|
8448
|
+
}
|
|
8449
|
+
return null;
|
|
8450
|
+
};
|
|
8451
|
+
//#endregion
|
|
8452
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
8453
|
+
const enclosingComponentOrHookName = (node) => {
|
|
8454
|
+
const functionNode = findEnclosingFunction(node);
|
|
8455
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8456
|
+
};
|
|
8457
|
+
//#endregion
|
|
8458
|
+
//#region src/plugin/utils/get-range-start.ts
|
|
8459
|
+
const getRangeStart = (node) => {
|
|
8460
|
+
const rangeStart = node.range?.[0];
|
|
8461
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8462
|
+
};
|
|
8463
|
+
//#endregion
|
|
8464
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
8465
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
8466
|
+
let node = callExpression;
|
|
8467
|
+
let parent = node.parent;
|
|
8468
|
+
while (parent) {
|
|
8469
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8470
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8471
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8472
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8473
|
+
node = parent;
|
|
8474
|
+
parent = node.parent;
|
|
8475
|
+
continue;
|
|
8476
|
+
}
|
|
8477
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8478
|
+
node = parent;
|
|
8479
|
+
parent = node.parent;
|
|
8480
|
+
continue;
|
|
8481
|
+
}
|
|
8482
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8483
|
+
node = parent;
|
|
8484
|
+
parent = node.parent;
|
|
8485
|
+
continue;
|
|
8486
|
+
}
|
|
8487
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8488
|
+
const expressions = parent.expressions ?? [];
|
|
8489
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
8490
|
+
node = parent;
|
|
8491
|
+
parent = node.parent;
|
|
8492
|
+
continue;
|
|
8493
|
+
}
|
|
8494
|
+
return false;
|
|
8495
|
+
}
|
|
8496
|
+
return false;
|
|
8497
|
+
};
|
|
8498
|
+
//#endregion
|
|
7626
8499
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7627
8500
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7628
8501
|
if (!node || typeof node !== "object") return;
|
|
@@ -7774,6 +8647,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7774
8647
|
};
|
|
7775
8648
|
//#endregion
|
|
7776
8649
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8650
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8651
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
8652
|
+
subscribe: "subscription",
|
|
8653
|
+
timer: "timer",
|
|
8654
|
+
socket: "connection"
|
|
8655
|
+
};
|
|
8656
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
8657
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
8658
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8659
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8660
|
+
};
|
|
7777
8661
|
const findSubscribeLikeUsages = (callback) => {
|
|
7778
8662
|
const usages = [];
|
|
7779
8663
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7785,6 +8669,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7785
8669
|
}
|
|
7786
8670
|
walkAst(callback, (child) => {
|
|
7787
8671
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
8672
|
+
if (isSocketConstruction(child)) {
|
|
8673
|
+
usages.push({
|
|
8674
|
+
kind: "socket",
|
|
8675
|
+
node: child,
|
|
8676
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8677
|
+
});
|
|
8678
|
+
return;
|
|
8679
|
+
}
|
|
7788
8680
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7789
8681
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7790
8682
|
usages.push({
|
|
@@ -7794,7 +8686,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7794
8686
|
});
|
|
7795
8687
|
return;
|
|
7796
8688
|
}
|
|
7797
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
8689
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
7798
8690
|
kind: "subscribe",
|
|
7799
8691
|
node: child,
|
|
7800
8692
|
resourceName: child.callee.property.name
|
|
@@ -7810,6 +8702,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7810
8702
|
};
|
|
7811
8703
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7812
8704
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8705
|
+
walkAst(effectCallback.body, (child) => {
|
|
8706
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
8707
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8708
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8709
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8710
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8711
|
+
});
|
|
7813
8712
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7814
8713
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7815
8714
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7817,7 +8716,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7817
8716
|
const bindingName = declarator.id.name;
|
|
7818
8717
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7819
8718
|
const init = declarator.init;
|
|
7820
|
-
if (!init
|
|
8719
|
+
if (!init) continue;
|
|
8720
|
+
if (isSocketConstruction(init)) {
|
|
8721
|
+
bindings.subscriptionNames.add(bindingName);
|
|
8722
|
+
continue;
|
|
8723
|
+
}
|
|
8724
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7821
8725
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7822
8726
|
bindings.subscriptionNames.add(bindingName);
|
|
7823
8727
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7843,9 +8747,21 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7843
8747
|
});
|
|
7844
8748
|
return bindings;
|
|
7845
8749
|
};
|
|
7846
|
-
const
|
|
7847
|
-
|
|
7848
|
-
|
|
8750
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8751
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8752
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8753
|
+
const releaseStarts = [];
|
|
8754
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8755
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8756
|
+
const releaseStart = getRangeStart(child);
|
|
8757
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8758
|
+
});
|
|
8759
|
+
if (releaseStarts.length === 0) return usages;
|
|
8760
|
+
return usages.filter((usage) => {
|
|
8761
|
+
const usageStart = getRangeStart(usage.node);
|
|
8762
|
+
if (usageStart === null) return true;
|
|
8763
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8764
|
+
});
|
|
7849
8765
|
};
|
|
7850
8766
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7851
8767
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
@@ -7869,26 +8785,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7869
8785
|
});
|
|
7870
8786
|
return didFindCleanupReturn;
|
|
7871
8787
|
};
|
|
8788
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8789
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8790
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8791
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8792
|
+
if (keyName === "signal") return true;
|
|
8793
|
+
if (keyName !== "once") return false;
|
|
8794
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8795
|
+
};
|
|
8796
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8797
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8798
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8799
|
+
["addListener", new Set([
|
|
8800
|
+
"removeListener",
|
|
8801
|
+
"off",
|
|
8802
|
+
"abort"
|
|
8803
|
+
])],
|
|
8804
|
+
["on", new Set([
|
|
8805
|
+
"off",
|
|
8806
|
+
"removeListener",
|
|
8807
|
+
"on"
|
|
8808
|
+
])],
|
|
8809
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8810
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8811
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8812
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8813
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8814
|
+
]);
|
|
8815
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8816
|
+
"cleanup",
|
|
8817
|
+
"dispose",
|
|
8818
|
+
"destroy",
|
|
8819
|
+
"teardown"
|
|
8820
|
+
]);
|
|
8821
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8822
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8823
|
+
const getReleaseVerbName = (node) => {
|
|
8824
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8825
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8826
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8827
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8828
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8829
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8830
|
+
return null;
|
|
8831
|
+
};
|
|
8832
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8833
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8834
|
+
let didFindPairedRelease = false;
|
|
8835
|
+
walkAst(body, (child) => {
|
|
8836
|
+
if (didFindPairedRelease) return false;
|
|
8837
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8838
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8839
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8840
|
+
didFindPairedRelease = true;
|
|
8841
|
+
return false;
|
|
8842
|
+
}
|
|
8843
|
+
});
|
|
8844
|
+
return didFindPairedRelease;
|
|
8845
|
+
};
|
|
8846
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8847
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8848
|
+
let programNode = anyNode;
|
|
8849
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8850
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8851
|
+
if (cached) return cached;
|
|
8852
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8853
|
+
walkAst(programNode, (child) => {
|
|
8854
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8855
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8856
|
+
});
|
|
8857
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8858
|
+
return releaseVerbNames;
|
|
8859
|
+
};
|
|
8860
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8861
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8862
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8863
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8864
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8865
|
+
return false;
|
|
8866
|
+
};
|
|
8867
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8868
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8869
|
+
const body = retainedFunction.body;
|
|
8870
|
+
if (!body) return null;
|
|
8871
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8872
|
+
let leak = null;
|
|
8873
|
+
walkAst(body, (child) => {
|
|
8874
|
+
if (leak !== null) return false;
|
|
8875
|
+
if (isFunctionLike$1(child)) return false;
|
|
8876
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8877
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8878
|
+
leak = {
|
|
8879
|
+
kind: "socket",
|
|
8880
|
+
node: child,
|
|
8881
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8882
|
+
};
|
|
8883
|
+
return false;
|
|
8884
|
+
}
|
|
8885
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8886
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8887
|
+
leak = {
|
|
8888
|
+
kind: "timer",
|
|
8889
|
+
node: child,
|
|
8890
|
+
resourceName: "setInterval"
|
|
8891
|
+
};
|
|
8892
|
+
return false;
|
|
8893
|
+
}
|
|
8894
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8895
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8896
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8897
|
+
kind: "subscribe",
|
|
8898
|
+
node: child,
|
|
8899
|
+
resourceName: registrationVerbName
|
|
8900
|
+
};
|
|
8901
|
+
return false;
|
|
8902
|
+
}
|
|
8903
|
+
});
|
|
8904
|
+
return leak;
|
|
8905
|
+
};
|
|
8906
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8907
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8908
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8909
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8910
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8911
|
+
};
|
|
7872
8912
|
const effectNeedsCleanup = defineRule({
|
|
7873
8913
|
id: "effect-needs-cleanup",
|
|
7874
8914
|
title: "Effect subscription or timer never cleaned up",
|
|
7875
8915
|
severity: "error",
|
|
7876
8916
|
tags: ["test-noise"],
|
|
7877
|
-
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.",
|
|
7878
|
-
create: (context) =>
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
8917
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8918
|
+
create: (context) => {
|
|
8919
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8920
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8921
|
+
if (!leak) return;
|
|
8922
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8923
|
+
context.report({
|
|
8924
|
+
node: leak.node,
|
|
8925
|
+
message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
|
|
8926
|
+
});
|
|
8927
|
+
};
|
|
8928
|
+
return {
|
|
8929
|
+
CallExpression(node) {
|
|
8930
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8931
|
+
const retainedCallback = getEffectCallback(node);
|
|
8932
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8933
|
+
return;
|
|
8934
|
+
}
|
|
8935
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8936
|
+
const callback = getEffectCallback(node);
|
|
8937
|
+
if (!callback) return;
|
|
8938
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8939
|
+
if (usages.length === 0) return;
|
|
8940
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8941
|
+
const firstUsage = usages[0];
|
|
8942
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8943
|
+
context.report({
|
|
8944
|
+
node,
|
|
8945
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8946
|
+
});
|
|
8947
|
+
},
|
|
8948
|
+
FunctionDeclaration(node) {
|
|
8949
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8950
|
+
},
|
|
8951
|
+
ArrowFunctionExpression(node) {
|
|
8952
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8953
|
+
},
|
|
8954
|
+
FunctionExpression(node) {
|
|
8955
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8956
|
+
}
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
7892
8959
|
});
|
|
7893
8960
|
//#endregion
|
|
7894
8961
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8447,17 +9514,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8447
9514
|
return computedCaptures;
|
|
8448
9515
|
};
|
|
8449
9516
|
//#endregion
|
|
8450
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8451
|
-
const isReactHookName = (name) => {
|
|
8452
|
-
if (!name.startsWith("use")) return false;
|
|
8453
|
-
if (name.length === 3) return true;
|
|
8454
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
8455
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8456
|
-
};
|
|
8457
|
-
//#endregion
|
|
8458
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8459
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8460
|
-
//#endregion
|
|
8461
9517
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8462
9518
|
const reactHocCalleeName = (callee) => {
|
|
8463
9519
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8671,8 +9727,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8671
9727
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8672
9728
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8673
9729
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8674
|
-
* `symbolHasStableHookOrigin` / `
|
|
8675
|
-
*
|
|
9730
|
+
* `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
|
|
9731
|
+
* multiple sites — extracting
|
|
8676
9732
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8677
9733
|
* declared logic.
|
|
8678
9734
|
*
|
|
@@ -8728,11 +9784,6 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
8728
9784
|
}
|
|
8729
9785
|
return false;
|
|
8730
9786
|
};
|
|
8731
|
-
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
8732
|
-
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8733
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8734
|
-
return getHookName(initializer.callee) === "useEffectEvent";
|
|
8735
|
-
};
|
|
8736
9787
|
const getFunctionValueNode = (symbol) => {
|
|
8737
9788
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
8738
9789
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -8818,6 +9869,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
8818
9869
|
};
|
|
8819
9870
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
8820
9871
|
//#endregion
|
|
9872
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9873
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9874
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9875
|
+
if (importSource === null) return false;
|
|
9876
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9877
|
+
};
|
|
9878
|
+
//#endregion
|
|
9879
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9880
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9881
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9882
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
9883
|
+
};
|
|
9884
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9885
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9886
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9887
|
+
return false;
|
|
9888
|
+
};
|
|
9889
|
+
//#endregion
|
|
9890
|
+
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9891
|
+
const getUseEffectEventCalleeName = (callee) => {
|
|
9892
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9893
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9894
|
+
return null;
|
|
9895
|
+
};
|
|
9896
|
+
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9897
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9898
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9899
|
+
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9900
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9901
|
+
};
|
|
9902
|
+
//#endregion
|
|
8821
9903
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
8822
9904
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
8823
9905
|
"useEffect",
|
|
@@ -9468,7 +10550,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9468
10550
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9469
10551
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9470
10552
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9471
|
-
if (depSymbol &&
|
|
10553
|
+
if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9472
10554
|
context.report({
|
|
9473
10555
|
node: elementNode,
|
|
9474
10556
|
message: buildEffectEventDepMessage()
|
|
@@ -9894,7 +10976,10 @@ const PRAGMA = "React";
|
|
|
9894
10976
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9895
10977
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9896
10978
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9897
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10979
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10980
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10981
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10982
|
+
}
|
|
9898
10983
|
return true;
|
|
9899
10984
|
};
|
|
9900
10985
|
//#endregion
|
|
@@ -11059,8 +12144,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11059
12144
|
if (node.attributes.length === 0) return;
|
|
11060
12145
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11061
12146
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11062
|
-
const
|
|
11063
|
-
if (
|
|
12147
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
12148
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11064
12149
|
let hasInteractiveHandler = false;
|
|
11065
12150
|
for (const attribute of node.attributes) {
|
|
11066
12151
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11074,11 +12159,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11074
12159
|
const elementType = getElementType(node, context.settings);
|
|
11075
12160
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11076
12161
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11077
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11078
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11079
12162
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11080
|
-
|
|
11081
|
-
const
|
|
12163
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
12164
|
+
for (const role of roleCandidates) {
|
|
12165
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
12166
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
12167
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
12168
|
+
}
|
|
12169
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
12170
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
12171
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11082
12172
|
context.report({
|
|
11083
12173
|
node,
|
|
11084
12174
|
message
|
|
@@ -11267,16 +12357,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11267
12357
|
return names;
|
|
11268
12358
|
};
|
|
11269
12359
|
//#endregion
|
|
11270
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11271
|
-
const findEnclosingFunction = (node) => {
|
|
11272
|
-
let cursor = node.parent;
|
|
11273
|
-
while (cursor) {
|
|
11274
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
11275
|
-
cursor = cursor.parent ?? null;
|
|
11276
|
-
}
|
|
11277
|
-
return null;
|
|
11278
|
-
};
|
|
11279
|
-
//#endregion
|
|
11280
12360
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11281
12361
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11282
12362
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -11480,7 +12560,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
|
|
|
11480
12560
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
11481
12561
|
if (!isNodeOfType(specifier.local, "Identifier")) continue;
|
|
11482
12562
|
if (source === "jotai-tanstack-query") {
|
|
11483
|
-
const importedName = getImportedName
|
|
12563
|
+
const importedName = getImportedName(specifier);
|
|
11484
12564
|
if (importedName && QUERY_ATOM_FACTORY_IMPORTED_NAMES.has(importedName)) queryAtomFactoryLocalNames.add(specifier.local.name);
|
|
11485
12565
|
continue;
|
|
11486
12566
|
}
|
|
@@ -11976,14 +13056,15 @@ const jsCacheStorage = defineRule({
|
|
|
11976
13056
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
11977
13057
|
CallExpression(node) {
|
|
11978
13058
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
11979
|
-
|
|
13059
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13060
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
11980
13061
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
11981
13062
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
11982
13063
|
const storageKey = String(node.arguments[0].value);
|
|
11983
13064
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
11984
13065
|
storageReadCounts.set(storageKey, readCount);
|
|
11985
13066
|
if (readCount === 2) {
|
|
11986
|
-
const storageName =
|
|
13067
|
+
const storageName = receiver.name;
|
|
11987
13068
|
context.report({
|
|
11988
13069
|
node,
|
|
11989
13070
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -11997,13 +13078,16 @@ const jsCacheStorage = defineRule({
|
|
|
11997
13078
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
11998
13079
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
11999
13080
|
const callee = callExpression.callee;
|
|
12000
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
13081
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
13082
|
+
const receiver = stripParenExpression(callee.object);
|
|
13083
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
13084
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
13085
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
13086
|
+
return true;
|
|
13087
|
+
}
|
|
13088
|
+
return false;
|
|
12006
13089
|
}
|
|
13090
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12007
13091
|
return false;
|
|
12008
13092
|
};
|
|
12009
13093
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12015,10 +13099,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12015
13099
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12016
13100
|
let cursor = receiverNode;
|
|
12017
13101
|
while (cursor) {
|
|
12018
|
-
|
|
12019
|
-
cursor = cursor.expression;
|
|
12020
|
-
continue;
|
|
12021
|
-
}
|
|
13102
|
+
cursor = stripParenExpression(cursor);
|
|
12022
13103
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12023
13104
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12024
13105
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12094,10 +13175,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12094
13175
|
let hops = 0;
|
|
12095
13176
|
while (cursor && hops < 12) {
|
|
12096
13177
|
hops += 1;
|
|
12097
|
-
|
|
12098
|
-
cursor = cursor.expression;
|
|
12099
|
-
continue;
|
|
12100
|
-
}
|
|
13178
|
+
cursor = stripParenExpression(cursor);
|
|
12101
13179
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12102
13180
|
const callee = cursor.callee;
|
|
12103
13181
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12121,10 +13199,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12121
13199
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12122
13200
|
let cursor = receiverNode;
|
|
12123
13201
|
while (cursor) {
|
|
12124
|
-
|
|
12125
|
-
cursor = cursor.expression;
|
|
12126
|
-
continue;
|
|
12127
|
-
}
|
|
13202
|
+
cursor = stripParenExpression(cursor);
|
|
12128
13203
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12129
13204
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12130
13205
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12189,7 +13264,7 @@ const jsCombineIterations = defineRule({
|
|
|
12189
13264
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12190
13265
|
const outerMethod = node.callee.property.name;
|
|
12191
13266
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12192
|
-
const innerCall = node.callee.object;
|
|
13267
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12193
13268
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12194
13269
|
const innerMethod = innerCall.callee.property.name;
|
|
12195
13270
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12262,10 +13337,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12262
13337
|
if (!filterArgument) return;
|
|
12263
13338
|
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;
|
|
12264
13339
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12265
|
-
const innerCall = node.callee.object;
|
|
13340
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12266
13341
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12267
13342
|
if (innerCall.callee.property.name !== "map") return;
|
|
12268
|
-
const receiver = innerCall.callee.object;
|
|
13343
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12269
13344
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12270
13345
|
const elements = receiver.elements ?? [];
|
|
12271
13346
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13525,7 +14600,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13525
14600
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13526
14601
|
create: (context) => ({ CallExpression(node) {
|
|
13527
14602
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13528
|
-
const receiver = node.callee.object;
|
|
14603
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13529
14604
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13530
14605
|
const spreadArgument = receiver.elements[0].argument;
|
|
13531
14606
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18562,7 +19637,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18562
19637
|
};
|
|
18563
19638
|
const describeClientSideNavigation = (node) => {
|
|
18564
19639
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18565
|
-
const
|
|
19640
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19641
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18566
19642
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18567
19643
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18568
19644
|
}
|
|
@@ -19182,7 +20258,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19182
20258
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19183
20259
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19184
20260
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19185
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
20261
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19186
20262
|
return node.callee.property.name;
|
|
19187
20263
|
};
|
|
19188
20264
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19196,7 +20272,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19196
20272
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19197
20273
|
const { property, object } = node.callee;
|
|
19198
20274
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19199
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
20275
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19200
20276
|
return true;
|
|
19201
20277
|
};
|
|
19202
20278
|
const getDbCallDescription = (node) => {
|
|
@@ -19204,7 +20280,8 @@ const getDbCallDescription = (node) => {
|
|
|
19204
20280
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19205
20281
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19206
20282
|
const methodName = node.callee.property.name;
|
|
19207
|
-
const
|
|
20283
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
20284
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19208
20285
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19209
20286
|
};
|
|
19210
20287
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20089,7 +21166,7 @@ const astMentionsSuspense = (programNode) => {
|
|
|
20089
21166
|
return false;
|
|
20090
21167
|
}
|
|
20091
21168
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
20092
|
-
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName
|
|
21169
|
+
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense")) {
|
|
20093
21170
|
didDetect = true;
|
|
20094
21171
|
return false;
|
|
20095
21172
|
}
|
|
@@ -20133,7 +21210,7 @@ const collectSuspenseLocalNames = (programNode) => {
|
|
|
20133
21210
|
for (const statement of programNode.body ?? []) {
|
|
20134
21211
|
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
20135
21212
|
if (statement.source?.value !== "react") continue;
|
|
20136
|
-
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName
|
|
21213
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
|
|
20137
21214
|
}
|
|
20138
21215
|
return names;
|
|
20139
21216
|
};
|
|
@@ -20604,13 +21681,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20604
21681
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20605
21682
|
let node = ref.identifier;
|
|
20606
21683
|
let parent = node.parent;
|
|
20607
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
21684
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20608
21685
|
node = parent;
|
|
20609
21686
|
parent = node.parent;
|
|
20610
21687
|
}
|
|
20611
21688
|
if (current.callee === node) return current;
|
|
20612
21689
|
}
|
|
20613
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
21690
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20614
21691
|
return null;
|
|
20615
21692
|
};
|
|
20616
21693
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20745,7 +21822,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20745
21822
|
const importDeclaration = declarationNode.parent;
|
|
20746
21823
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20747
21824
|
}));
|
|
20748
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21825
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20749
21826
|
if (!node) return false;
|
|
20750
21827
|
if (isNodeOfType(node, "Identifier")) {
|
|
20751
21828
|
if (node.name === hookName) return true;
|
|
@@ -20754,15 +21831,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20754
21831
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20755
21832
|
return false;
|
|
20756
21833
|
}
|
|
20757
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21834
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21835
|
+
const receiver = stripParenExpression(node.object);
|
|
21836
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21837
|
+
}
|
|
20758
21838
|
return false;
|
|
20759
21839
|
};
|
|
20760
21840
|
const isUseEffect = (node) => {
|
|
20761
21841
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20762
21842
|
const callee = node.callee;
|
|
20763
21843
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20764
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20765
|
-
|
|
21844
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21845
|
+
const receiver = stripParenExpression(callee.object);
|
|
21846
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20766
21847
|
};
|
|
20767
21848
|
const getEffectFn = (analysis, node) => {
|
|
20768
21849
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20790,7 +21871,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20790
21871
|
const node = def.node;
|
|
20791
21872
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20792
21873
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20793
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21874
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20794
21875
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20795
21876
|
const elements = node.id.elements ?? [];
|
|
20796
21877
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20801,7 +21882,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20801
21882
|
const node = def.node;
|
|
20802
21883
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20803
21884
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20804
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21885
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20805
21886
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20806
21887
|
const elements = node.id.elements ?? [];
|
|
20807
21888
|
if (elements.length !== 2) return false;
|
|
@@ -20848,7 +21929,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20848
21929
|
const node = def.node;
|
|
20849
21930
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20850
21931
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20851
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21932
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20852
21933
|
}));
|
|
20853
21934
|
const isRefCurrent = (ref) => {
|
|
20854
21935
|
const parent = ref.identifier.parent;
|
|
@@ -20861,11 +21942,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20861
21942
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20862
21943
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20863
21944
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20864
|
-
|
|
20865
|
-
|
|
21945
|
+
let effectiveNode = ref.identifier;
|
|
21946
|
+
let parent = effectiveNode.parent;
|
|
21947
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21948
|
+
effectiveNode = parent;
|
|
21949
|
+
parent = effectiveNode.parent;
|
|
21950
|
+
}
|
|
20866
21951
|
if (!parent) return false;
|
|
20867
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20868
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21952
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21953
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20869
21954
|
const memberParent = parent.parent;
|
|
20870
21955
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20871
21956
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20876,7 +21961,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20876
21961
|
};
|
|
20877
21962
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20878
21963
|
const getUseStateDecl = (analysis, ref) => {
|
|
20879
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21964
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20880
21965
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20881
21966
|
return node ?? null;
|
|
20882
21967
|
};
|
|
@@ -21081,7 +22166,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21081
22166
|
const noAdjustStateOnPropChange = defineRule({
|
|
21082
22167
|
id: "no-adjust-state-on-prop-change",
|
|
21083
22168
|
title: "State synced to a prop inside an effect",
|
|
21084
|
-
severity: "
|
|
22169
|
+
severity: "warn",
|
|
21085
22170
|
tags: ["test-noise"],
|
|
21086
22171
|
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",
|
|
21087
22172
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21122,7 +22207,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21122
22207
|
"summary",
|
|
21123
22208
|
"textarea"
|
|
21124
22209
|
]);
|
|
22210
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
22211
|
+
const value = attribute.value;
|
|
22212
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
22213
|
+
const expression = value.expression;
|
|
22214
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
22215
|
+
};
|
|
22216
|
+
const DISABLEABLE_TAGS = new Set([
|
|
22217
|
+
"button",
|
|
22218
|
+
"input",
|
|
22219
|
+
"select",
|
|
22220
|
+
"textarea"
|
|
22221
|
+
]);
|
|
21125
22222
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
22223
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
22224
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
22225
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
22226
|
+
}
|
|
21126
22227
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21127
22228
|
switch (tagName) {
|
|
21128
22229
|
case "input": {
|
|
@@ -21136,7 +22237,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21136
22237
|
case "a":
|
|
21137
22238
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21138
22239
|
case "audio":
|
|
21139
|
-
case "video":
|
|
22240
|
+
case "video": {
|
|
22241
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
22242
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
22243
|
+
}
|
|
21140
22244
|
default: return false;
|
|
21141
22245
|
}
|
|
21142
22246
|
};
|
|
@@ -22100,7 +23204,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22100
23204
|
"some"
|
|
22101
23205
|
]);
|
|
22102
23206
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22103
|
-
const isPositionallyStableIterationReceiver = (
|
|
23207
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
23208
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22104
23209
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22105
23210
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22106
23211
|
const only = receiver.elements[0];
|
|
@@ -22111,17 +23216,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22111
23216
|
}
|
|
22112
23217
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22113
23218
|
const callee = receiver.callee;
|
|
22114
|
-
if (
|
|
23219
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22115
23220
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22116
23221
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22117
23222
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22118
23223
|
return false;
|
|
22119
23224
|
};
|
|
22120
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
22121
|
-
if (parentCall.arguments[1] !== callback) return false;
|
|
22122
|
-
const callee = parentCall.callee;
|
|
22123
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
|
|
22124
|
-
};
|
|
23225
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22125
23226
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22126
23227
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22127
23228
|
for (const property of source.properties ?? []) {
|
|
@@ -22180,18 +23281,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22180
23281
|
return false;
|
|
22181
23282
|
}
|
|
22182
23283
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22183
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
23284
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22184
23285
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22185
23286
|
}
|
|
22186
23287
|
return false;
|
|
22187
23288
|
};
|
|
22188
|
-
const isReactCloneElement = (callExpression) =>
|
|
22189
|
-
const callee = callExpression.callee;
|
|
22190
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22191
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22192
|
-
if (callee.property.name !== "cloneElement") return false;
|
|
22193
|
-
return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
|
|
22194
|
-
};
|
|
23289
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22195
23290
|
const noArrayIndexKey = defineRule({
|
|
22196
23291
|
id: "no-array-index-key",
|
|
22197
23292
|
title: "Array index used as a key",
|
|
@@ -22700,6 +23795,10 @@ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
|
22700
23795
|
"ClassDeclaration",
|
|
22701
23796
|
"ClassExpression"
|
|
22702
23797
|
]);
|
|
23798
|
+
const REACT_CREATE_ELEMENT_OPTIONS = {
|
|
23799
|
+
allowGlobalReactNamespace: false,
|
|
23800
|
+
allowUnboundBareCalls: false
|
|
23801
|
+
};
|
|
22703
23802
|
const isCallArgumentFunctionExpression = (node) => {
|
|
22704
23803
|
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return false;
|
|
22705
23804
|
const parent = node.parent;
|
|
@@ -22707,44 +23806,10 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
22707
23806
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
22708
23807
|
};
|
|
22709
23808
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
22710
|
-
const isReactImport$1 = (symbol) => {
|
|
22711
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
22712
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
22713
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
22714
|
-
return importDeclaration.source.value === "react";
|
|
22715
|
-
};
|
|
22716
|
-
const getImportedName = (symbol) => {
|
|
22717
|
-
if (symbol.kind !== "import") return null;
|
|
22718
|
-
if (!isReactImport$1(symbol)) return null;
|
|
22719
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
22720
|
-
};
|
|
22721
|
-
const isReactNamespaceImport = (symbol) => {
|
|
22722
|
-
if (symbol.kind !== "import") return false;
|
|
22723
|
-
if (!isReactImport$1(symbol)) return false;
|
|
22724
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
22725
|
-
};
|
|
22726
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
22727
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
22728
|
-
const symbol = scopes.symbolFor(callee);
|
|
22729
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
22730
|
-
};
|
|
22731
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
22732
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22733
|
-
if (callee.computed) return false;
|
|
22734
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
22735
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22736
|
-
if (callee.property.name !== "createElement") return false;
|
|
22737
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
22738
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
22739
|
-
};
|
|
22740
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
22741
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
22742
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
22743
|
-
};
|
|
22744
23809
|
const containsRenderOutput$1 = (node, rootNode, scopes) => {
|
|
22745
23810
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
22746
23811
|
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
22747
|
-
if (
|
|
23812
|
+
if (isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) return true;
|
|
22748
23813
|
const nodeRecord = node;
|
|
22749
23814
|
for (const key of Object.keys(nodeRecord)) {
|
|
22750
23815
|
if (key === "parent") continue;
|
|
@@ -22915,7 +23980,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22915
23980
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22916
23981
|
return false;
|
|
22917
23982
|
};
|
|
22918
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23983
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22919
23984
|
"forEach",
|
|
22920
23985
|
"map",
|
|
22921
23986
|
"filter",
|
|
@@ -22936,30 +24001,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22936
24001
|
if (parent.callee === functionNode) return true;
|
|
22937
24002
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22938
24003
|
const callee = parent.callee;
|
|
22939
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24004
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22940
24005
|
};
|
|
22941
24006
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
22942
|
-
const
|
|
22943
|
-
|
|
22944
|
-
if (statement.alternate) return null;
|
|
22945
|
-
const consequent = statement.consequent;
|
|
22946
|
-
if (isTerminatingStatement(consequent)) return consequent;
|
|
22947
|
-
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
22948
|
-
return null;
|
|
22949
|
-
};
|
|
22950
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
24007
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
24008
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22951
24009
|
let fallThroughCount = 0;
|
|
22952
|
-
let
|
|
24010
|
+
let maxTerminatedCount = 0;
|
|
22953
24011
|
for (const statement of statements) {
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
24012
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24013
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
24014
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
24015
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
24016
|
+
fallThroughCount: 0,
|
|
24017
|
+
maxTerminatedCount: 0,
|
|
24018
|
+
doAllPathsTerminate: false
|
|
24019
|
+
};
|
|
24020
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
24021
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
24022
|
+
fallThroughCount: 0,
|
|
24023
|
+
maxTerminatedCount,
|
|
24024
|
+
doAllPathsTerminate: true
|
|
24025
|
+
};
|
|
24026
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
24027
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22957
24028
|
continue;
|
|
22958
24029
|
}
|
|
22959
|
-
if (isTerminatingStatement(statement))
|
|
24030
|
+
if (isTerminatingStatement(statement)) {
|
|
24031
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
24032
|
+
return {
|
|
24033
|
+
fallThroughCount: 0,
|
|
24034
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
24035
|
+
doAllPathsTerminate: true
|
|
24036
|
+
};
|
|
24037
|
+
}
|
|
22960
24038
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22961
24039
|
}
|
|
22962
|
-
return
|
|
24040
|
+
return {
|
|
24041
|
+
fallThroughCount,
|
|
24042
|
+
maxTerminatedCount,
|
|
24043
|
+
doAllPathsTerminate: false
|
|
24044
|
+
};
|
|
24045
|
+
};
|
|
24046
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
24047
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
24048
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22963
24049
|
};
|
|
22964
24050
|
const collectLocalHelperFunctions = (root) => {
|
|
22965
24051
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23090,7 +24176,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23090
24176
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23091
24177
|
const firstStatement = (body.body ?? [])[0];
|
|
23092
24178
|
if (!firstStatement) return false;
|
|
23093
|
-
if (!
|
|
24179
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
24180
|
+
const consequent = firstStatement.consequent;
|
|
24181
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23094
24182
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23095
24183
|
};
|
|
23096
24184
|
const noCascadingSetState = defineRule({
|
|
@@ -23449,40 +24537,6 @@ const noCloneElement = defineRule({
|
|
|
23449
24537
|
} })
|
|
23450
24538
|
});
|
|
23451
24539
|
//#endregion
|
|
23452
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23453
|
-
const hocWrapperCalleeName = (callee) => {
|
|
23454
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23455
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23456
|
-
return null;
|
|
23457
|
-
};
|
|
23458
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23459
|
-
let current = functionNode;
|
|
23460
|
-
for (;;) {
|
|
23461
|
-
const parent = current.parent;
|
|
23462
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23463
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23464
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23465
|
-
current = parent;
|
|
23466
|
-
continue;
|
|
23467
|
-
}
|
|
23468
|
-
}
|
|
23469
|
-
break;
|
|
23470
|
-
}
|
|
23471
|
-
const binding = current.parent;
|
|
23472
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23473
|
-
return null;
|
|
23474
|
-
};
|
|
23475
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23476
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23477
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
23478
|
-
};
|
|
23479
|
-
//#endregion
|
|
23480
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23481
|
-
const enclosingComponentOrHookName = (node) => {
|
|
23482
|
-
const functionNode = findEnclosingFunction(node);
|
|
23483
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23484
|
-
};
|
|
23485
|
-
//#endregion
|
|
23486
24540
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23487
24541
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23488
24542
|
const CONTEXT_MODULES = [
|
|
@@ -24071,16 +25125,6 @@ const isInitialOnlyPropName = (propName) => {
|
|
|
24071
25125
|
return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
|
|
24072
25126
|
};
|
|
24073
25127
|
//#endregion
|
|
24074
|
-
//#region src/plugin/rules/state-and-effects/utils/static-member-property-name.ts
|
|
24075
|
-
const getStaticMemberPropertyName = (node) => {
|
|
24076
|
-
if (!node) return null;
|
|
24077
|
-
const unwrappedNode = stripParenExpression(node);
|
|
24078
|
-
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
24079
|
-
if (!unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier")) return unwrappedNode.property.name;
|
|
24080
|
-
if (unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Literal") && typeof unwrappedNode.property.value === "string") return unwrappedNode.property.value;
|
|
24081
|
-
return null;
|
|
24082
|
-
};
|
|
24083
|
-
//#endregion
|
|
24084
25128
|
//#region src/plugin/rules/state-and-effects/utils/event-handler-reference.ts
|
|
24085
25129
|
const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
|
|
24086
25130
|
const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
|
|
@@ -24089,13 +25133,6 @@ const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
|
|
|
24089
25133
|
const propertyName = getStaticMemberPropertyName(node);
|
|
24090
25134
|
return propertyName ? `${resolveName(node.object.name)}.${propertyName}` : null;
|
|
24091
25135
|
};
|
|
24092
|
-
const getStaticPropertyKeyName = (node) => {
|
|
24093
|
-
if (!isNodeOfType(node, "Property")) return null;
|
|
24094
|
-
if (node.computed) return null;
|
|
24095
|
-
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
24096
|
-
if (isNodeOfType(node.key, "Literal")) return String(node.key.value);
|
|
24097
|
-
return null;
|
|
24098
|
-
};
|
|
24099
25136
|
const isEventHandlerValue = (node, eventHandlerReferenceNames, resolveName = (name) => name) => {
|
|
24100
25137
|
if (isInlineFunctionExpression(node)) return true;
|
|
24101
25138
|
if (isNodeOfType(node, "Identifier")) return eventHandlerReferenceNames.has(resolveName(node.name));
|
|
@@ -24453,11 +25490,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24453
25490
|
return isInitialOnlyPropName(arg.name);
|
|
24454
25491
|
};
|
|
24455
25492
|
//#endregion
|
|
25493
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
25494
|
+
const isNoOpStatement = (statement) => {
|
|
25495
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
25496
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
25497
|
+
const expression = stripParenExpression(statement.expression);
|
|
25498
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
25499
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
25500
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
25501
|
+
return false;
|
|
25502
|
+
};
|
|
25503
|
+
//#endregion
|
|
24456
25504
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24457
25505
|
const getCallbackStatements = (callback) => {
|
|
24458
25506
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24459
|
-
|
|
24460
|
-
return callback.body ? [callback.body] : [];
|
|
25507
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24461
25508
|
};
|
|
24462
25509
|
//#endregion
|
|
24463
25510
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24496,6 +25543,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24496
25543
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24497
25544
|
}
|
|
24498
25545
|
};
|
|
25546
|
+
const flattenGuardedStatements = (statements) => {
|
|
25547
|
+
const flattened = [];
|
|
25548
|
+
for (const statement of statements) {
|
|
25549
|
+
if (isNoOpStatement(statement)) continue;
|
|
25550
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
25551
|
+
flattened.push(statement);
|
|
25552
|
+
continue;
|
|
25553
|
+
}
|
|
25554
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
25555
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
25556
|
+
if (!branch) continue;
|
|
25557
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
25558
|
+
if (flattenedBranch === null) return null;
|
|
25559
|
+
flattened.push(...flattenedBranch);
|
|
25560
|
+
}
|
|
25561
|
+
continue;
|
|
25562
|
+
}
|
|
25563
|
+
return null;
|
|
25564
|
+
}
|
|
25565
|
+
return flattened;
|
|
25566
|
+
};
|
|
24499
25567
|
const noDerivedStateEffect = defineRule({
|
|
24500
25568
|
id: "no-derived-state-effect",
|
|
24501
25569
|
title: "Derived state stored in an effect",
|
|
@@ -24527,8 +25595,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24527
25595
|
}
|
|
24528
25596
|
}
|
|
24529
25597
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24530
|
-
const statements = getCallbackStatements(callback);
|
|
24531
|
-
if (statements.length === 0) return;
|
|
25598
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
25599
|
+
if (statements === null || statements.length === 0) return;
|
|
24532
25600
|
if (!statements.every((statement) => {
|
|
24533
25601
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24534
25602
|
const expression = statement.expression;
|
|
@@ -25163,7 +26231,7 @@ const noDidMountSetState = defineRule({
|
|
|
25163
26231
|
const { mode } = resolveSettings$20(context.settings);
|
|
25164
26232
|
return { CallExpression(node) {
|
|
25165
26233
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25166
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
26234
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25167
26235
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25168
26236
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25169
26237
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25288,7 +26356,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25288
26356
|
const { mode } = resolveSettings$19(context.settings);
|
|
25289
26357
|
return { CallExpression(node) {
|
|
25290
26358
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25291
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
26359
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25292
26360
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25293
26361
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25294
26362
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25385,18 +26453,45 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25385
26453
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25386
26454
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25387
26455
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
26456
|
+
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
26457
|
+
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
26458
|
+
"assign",
|
|
26459
|
+
"entries",
|
|
26460
|
+
"fromEntries",
|
|
26461
|
+
"keys",
|
|
26462
|
+
"values"
|
|
26463
|
+
]);
|
|
26464
|
+
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
26465
|
+
"map",
|
|
26466
|
+
"filter",
|
|
26467
|
+
"slice",
|
|
26468
|
+
"concat",
|
|
26469
|
+
"flat",
|
|
26470
|
+
"flatMap",
|
|
26471
|
+
"toSorted",
|
|
26472
|
+
"toReversed",
|
|
26473
|
+
"toSpliced",
|
|
26474
|
+
"with"
|
|
26475
|
+
]);
|
|
25388
26476
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25389
26477
|
const isPlainDataProducerCall = (expression) => {
|
|
25390
26478
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25391
26479
|
const callee = expression.callee;
|
|
25392
26480
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25393
26481
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25394
|
-
if (isNodeOfType(callee.object, "Identifier")
|
|
26482
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
26483
|
+
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
26484
|
+
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
26485
|
+
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
26486
|
+
}
|
|
25395
26487
|
return producesPlainStateValue(callee.object);
|
|
25396
26488
|
};
|
|
26489
|
+
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
26490
|
+
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25397
26491
|
const producesPlainStateValue = (expression) => {
|
|
25398
26492
|
const unwrapped = stripParenExpression(expression);
|
|
25399
26493
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
26494
|
+
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25400
26495
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25401
26496
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25402
26497
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25411,6 +26506,38 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25411
26506
|
}
|
|
25412
26507
|
return producesPlainStateValue(unwrapped);
|
|
25413
26508
|
};
|
|
26509
|
+
const producesOpaqueInstanceValue = (expression) => {
|
|
26510
|
+
if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
|
|
26511
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
26512
|
+
const callee = expression.callee;
|
|
26513
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
26514
|
+
if (isPlainDataProducerCall(expression)) return false;
|
|
26515
|
+
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
26516
|
+
return true;
|
|
26517
|
+
};
|
|
26518
|
+
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
26519
|
+
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
26520
|
+
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
26521
|
+
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
26522
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
26523
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
26524
|
+
const setterName = node.callee.name;
|
|
26525
|
+
if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
|
|
26526
|
+
const argument = node.arguments?.[0];
|
|
26527
|
+
if (!argument) return;
|
|
26528
|
+
const unwrapped = stripParenExpression(argument);
|
|
26529
|
+
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
26530
|
+
if (producesPlainStateValue(unwrapped)) {
|
|
26531
|
+
plainFedSetterNames.add(setterName);
|
|
26532
|
+
return;
|
|
26533
|
+
}
|
|
26534
|
+
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
26535
|
+
}, true);
|
|
26536
|
+
return {
|
|
26537
|
+
plainFedSetterNames,
|
|
26538
|
+
opaqueFedSetterNames
|
|
26539
|
+
};
|
|
26540
|
+
};
|
|
25414
26541
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25415
26542
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25416
26543
|
walkAst(componentBody, (node) => {
|
|
@@ -25480,11 +26607,15 @@ const noDirectStateMutation = defineRule({
|
|
|
25480
26607
|
if (bindings.length === 0) return;
|
|
25481
26608
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25482
26609
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
26610
|
+
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25483
26611
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25484
26612
|
for (const binding of bindings) {
|
|
25485
26613
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25486
26614
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25487
|
-
|
|
26615
|
+
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
26616
|
+
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
26617
|
+
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
26618
|
+
plainObjectStateValueNames.add(binding.valueName);
|
|
25488
26619
|
}
|
|
25489
26620
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25490
26621
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -25609,9 +26740,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25609
26740
|
create: (context) => ({ CallExpression(node) {
|
|
25610
26741
|
const callee = node.callee;
|
|
25611
26742
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25612
|
-
|
|
26743
|
+
const receiver = stripParenExpression(callee.object);
|
|
26744
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25613
26745
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25614
|
-
if (context.scopes.symbolFor(
|
|
26746
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25615
26747
|
if (!importsReactViewTransition(node)) return;
|
|
25616
26748
|
context.report({
|
|
25617
26749
|
node,
|
|
@@ -25631,7 +26763,8 @@ const noDocumentWrite = defineRule({
|
|
|
25631
26763
|
create: (context) => ({ CallExpression(node) {
|
|
25632
26764
|
const callee = node.callee;
|
|
25633
26765
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25634
|
-
|
|
26766
|
+
const receiver = stripParenExpression(callee.object);
|
|
26767
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25635
26768
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25636
26769
|
context.report({
|
|
25637
26770
|
node,
|
|
@@ -26263,13 +27396,6 @@ const noEffectEventHandler = defineRule({
|
|
|
26263
27396
|
}
|
|
26264
27397
|
});
|
|
26265
27398
|
//#endregion
|
|
26266
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
26267
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
26268
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
26269
|
-
if (importSource === null) return false;
|
|
26270
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
26271
|
-
};
|
|
26272
|
-
//#endregion
|
|
26273
27399
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26274
27400
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26275
27401
|
const componentBindingStack = [];
|
|
@@ -26323,11 +27449,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26323
27449
|
const initializer = declaratorNode.init;
|
|
26324
27450
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26325
27451
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26326
|
-
if (
|
|
26327
|
-
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26328
|
-
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26329
|
-
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26330
|
-
}
|
|
27452
|
+
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
26331
27453
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26332
27454
|
} });
|
|
26333
27455
|
return {
|
|
@@ -27044,7 +28166,7 @@ const collectVariableDeclarationReferenceNames = (node, scope, eventHandlerRefer
|
|
|
27044
28166
|
};
|
|
27045
28167
|
const collectPropertyReferenceNames = (node, scope, eventHandlerReferenceNames) => {
|
|
27046
28168
|
if (!isNodeOfType(node, "Property")) return /* @__PURE__ */ new Set();
|
|
27047
|
-
const propertyName = getStaticPropertyKeyName(node);
|
|
28169
|
+
const propertyName = getStaticPropertyKeyName(node, { stringifyNonStringLiterals: true });
|
|
27048
28170
|
if (propertyName && isEventHandlerName(propertyName) && isEventHandlerValue(node.value, eventHandlerReferenceNames, (name) => resolveBindingName(scope, name))) return /* @__PURE__ */ new Set();
|
|
27049
28171
|
const names = /* @__PURE__ */ new Set();
|
|
27050
28172
|
if (node.computed) addNames$1(names, collectScopedReferenceNames(node.key, scope, eventHandlerReferenceNames));
|
|
@@ -27422,7 +28544,7 @@ const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLoc
|
|
|
27422
28544
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
27423
28545
|
for (const property of node.properties ?? []) {
|
|
27424
28546
|
if (!isNodeOfType(property, "Property")) continue;
|
|
27425
|
-
const propertyName = getStaticPropertyKeyName(property);
|
|
28547
|
+
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
27426
28548
|
if (!propertyName) continue;
|
|
27427
28549
|
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
27428
28550
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
@@ -27933,7 +29055,7 @@ const noFlushSync = defineRule({
|
|
|
27933
29055
|
if (node.source?.value !== "react-dom") return;
|
|
27934
29056
|
for (const specifier of node.specifiers ?? []) {
|
|
27935
29057
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
27936
|
-
if (getImportedName
|
|
29058
|
+
if (getImportedName(specifier) !== "flushSync") continue;
|
|
27937
29059
|
const program = findProgram(node);
|
|
27938
29060
|
if (program) {
|
|
27939
29061
|
if (importsImperativeDomLibrary(program)) return;
|
|
@@ -28907,7 +30029,7 @@ const noIsMounted = defineRule({
|
|
|
28907
30029
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28908
30030
|
create: (context) => ({ CallExpression(node) {
|
|
28909
30031
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28910
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
30032
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28911
30033
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28912
30034
|
if (!getParentComponent(node)) return;
|
|
28913
30035
|
context.report({
|
|
@@ -28922,7 +30044,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28922
30044
|
const isJsonMethodCall = (node, method) => {
|
|
28923
30045
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28924
30046
|
const callee = node.callee;
|
|
28925
|
-
|
|
30047
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
30048
|
+
const receiver = stripParenExpression(callee.object);
|
|
30049
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28926
30050
|
};
|
|
28927
30051
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28928
30052
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29007,7 +30131,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29007
30131
|
const noJsxElementType = defineRule({
|
|
29008
30132
|
id: "no-jsx-element-type",
|
|
29009
30133
|
title: "No JSX.Element",
|
|
29010
|
-
severity: "
|
|
30134
|
+
severity: "warn",
|
|
29011
30135
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29012
30136
|
create: (context) => {
|
|
29013
30137
|
let isJsxImported = false;
|
|
@@ -29333,6 +30457,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29333
30457
|
}
|
|
29334
30458
|
});
|
|
29335
30459
|
//#endregion
|
|
30460
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
30461
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
30462
|
+
"map",
|
|
30463
|
+
"filter",
|
|
30464
|
+
"forEach",
|
|
30465
|
+
"flatMap",
|
|
30466
|
+
"reduce",
|
|
30467
|
+
"reduceRight",
|
|
30468
|
+
"some",
|
|
30469
|
+
"every",
|
|
30470
|
+
"find",
|
|
30471
|
+
"findIndex",
|
|
30472
|
+
"findLast",
|
|
30473
|
+
"findLastIndex",
|
|
30474
|
+
"sort",
|
|
30475
|
+
"toSorted"
|
|
30476
|
+
]);
|
|
30477
|
+
const executesDuringRender = (functionNode) => {
|
|
30478
|
+
const parent = functionNode.parent;
|
|
30479
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
30480
|
+
if (parent.callee === functionNode) return true;
|
|
30481
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
30482
|
+
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;
|
|
30483
|
+
};
|
|
30484
|
+
//#endregion
|
|
30485
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30486
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30487
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30488
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30489
|
+
return false;
|
|
30490
|
+
};
|
|
30491
|
+
//#endregion
|
|
30492
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
30493
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
30494
|
+
let ancestor = bindingIdentifier.parent;
|
|
30495
|
+
while (ancestor) {
|
|
30496
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
30497
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
30498
|
+
ancestor = ancestor.parent ?? null;
|
|
30499
|
+
}
|
|
30500
|
+
return null;
|
|
30501
|
+
};
|
|
30502
|
+
//#endregion
|
|
30503
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
30504
|
+
const isFalsyLiteral = (node) => {
|
|
30505
|
+
if (!node) return true;
|
|
30506
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
30507
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
30508
|
+
};
|
|
30509
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
30510
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
30511
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30512
|
+
if (!binding) return false;
|
|
30513
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30514
|
+
if (!declarator?.init) return false;
|
|
30515
|
+
const init = stripParenExpression(declarator.init);
|
|
30516
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
30517
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
30518
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
30519
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
30520
|
+
};
|
|
30521
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
30522
|
+
//#endregion
|
|
30523
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30524
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
30525
|
+
let cursor = node;
|
|
30526
|
+
let parent = node.parent;
|
|
30527
|
+
while (parent) {
|
|
30528
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
30529
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30530
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30531
|
+
cursor = parent;
|
|
30532
|
+
parent = parent.parent ?? null;
|
|
30533
|
+
}
|
|
30534
|
+
return false;
|
|
30535
|
+
};
|
|
30536
|
+
//#endregion
|
|
30537
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
30538
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
30539
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
30540
|
+
const unwrapped = stripParenExpression(expression);
|
|
30541
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
30542
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
30543
|
+
const property = unwrapped.property;
|
|
30544
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
30545
|
+
}
|
|
30546
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
30547
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
30548
|
+
return false;
|
|
30549
|
+
};
|
|
30550
|
+
//#endregion
|
|
30551
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
30552
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
30553
|
+
let cursor = node;
|
|
30554
|
+
let parent = node.parent;
|
|
30555
|
+
while (parent) {
|
|
30556
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
30557
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
30558
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
30559
|
+
cursor = parent;
|
|
30560
|
+
parent = parent.parent ?? null;
|
|
30561
|
+
}
|
|
30562
|
+
return false;
|
|
30563
|
+
};
|
|
30564
|
+
//#endregion
|
|
30565
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
30566
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
30567
|
+
"toLocaleString",
|
|
30568
|
+
"toLocaleDateString",
|
|
30569
|
+
"toLocaleTimeString"
|
|
30570
|
+
]);
|
|
30571
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
30572
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
30573
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
30574
|
+
"format",
|
|
30575
|
+
"formatToParts",
|
|
30576
|
+
"formatRange"
|
|
30577
|
+
]);
|
|
30578
|
+
const isProvableDateExpression = (expression) => {
|
|
30579
|
+
if (!expression) return false;
|
|
30580
|
+
const unwrapped = stripParenExpression(expression);
|
|
30581
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
30582
|
+
};
|
|
30583
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
30584
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
30585
|
+
if (!expression) return false;
|
|
30586
|
+
const unwrapped = stripParenExpression(expression);
|
|
30587
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
30588
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
30589
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
30590
|
+
return false;
|
|
30591
|
+
};
|
|
30592
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
30593
|
+
if (!objectExpression) return false;
|
|
30594
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
30595
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
30596
|
+
for (const property of unwrapped.properties ?? []) {
|
|
30597
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
30598
|
+
if (property.computed) continue;
|
|
30599
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
30600
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
30601
|
+
}
|
|
30602
|
+
return false;
|
|
30603
|
+
};
|
|
30604
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
30605
|
+
if (!argument) return false;
|
|
30606
|
+
const unwrapped = stripParenExpression(argument);
|
|
30607
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
30608
|
+
return true;
|
|
30609
|
+
};
|
|
30610
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
30611
|
+
const localeArgument = call.arguments?.[0];
|
|
30612
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
30613
|
+
const optionsArgument = call.arguments?.[1];
|
|
30614
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
30615
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
30616
|
+
};
|
|
30617
|
+
const matchLocaleMethodCall = (call) => {
|
|
30618
|
+
const callee = call.callee;
|
|
30619
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
30620
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
30621
|
+
const methodName = callee.property.name;
|
|
30622
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
30623
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
30624
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
30625
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
30626
|
+
return {
|
|
30627
|
+
node: call,
|
|
30628
|
+
display: `${methodName}()`
|
|
30629
|
+
};
|
|
30630
|
+
};
|
|
30631
|
+
const getIntlFormatterName = (expression) => {
|
|
30632
|
+
if (!expression) return null;
|
|
30633
|
+
const unwrapped = stripParenExpression(expression);
|
|
30634
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
30635
|
+
const callee = unwrapped.callee;
|
|
30636
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
30637
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
30638
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
30639
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
30640
|
+
};
|
|
30641
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
30642
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
30643
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
30644
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
30645
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
30646
|
+
};
|
|
30647
|
+
const matchIntlFormatCall = (call) => {
|
|
30648
|
+
const callee = call.callee;
|
|
30649
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
30650
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
30651
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
30652
|
+
let construction = stripParenExpression(callee.object);
|
|
30653
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
30654
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
30655
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
30656
|
+
}
|
|
30657
|
+
if (!construction) return null;
|
|
30658
|
+
const formatterName = getIntlFormatterName(construction);
|
|
30659
|
+
if (!formatterName) return null;
|
|
30660
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
30661
|
+
return {
|
|
30662
|
+
node: call,
|
|
30663
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
30664
|
+
};
|
|
30665
|
+
};
|
|
30666
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
30667
|
+
if (!expression) return false;
|
|
30668
|
+
const unwrapped = stripParenExpression(expression);
|
|
30669
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
30670
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
30671
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
30672
|
+
};
|
|
30673
|
+
const matchDateDefaultStringification = (node) => {
|
|
30674
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
30675
|
+
const callee = node.callee;
|
|
30676
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
30677
|
+
node,
|
|
30678
|
+
display: "Date.prototype.toString()"
|
|
30679
|
+
};
|
|
30680
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
30681
|
+
node,
|
|
30682
|
+
display: "String(new Date(…))"
|
|
30683
|
+
};
|
|
30684
|
+
return null;
|
|
30685
|
+
}
|
|
30686
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
30687
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
30688
|
+
node: expression,
|
|
30689
|
+
display: "`${new Date(…)}`"
|
|
30690
|
+
};
|
|
30691
|
+
}
|
|
30692
|
+
return null;
|
|
30693
|
+
};
|
|
30694
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
30695
|
+
let functionNode = findEnclosingFunction(node);
|
|
30696
|
+
while (functionNode) {
|
|
30697
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30698
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
30699
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
30700
|
+
}
|
|
30701
|
+
return null;
|
|
30702
|
+
};
|
|
30703
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30704
|
+
if (fileHasUseClientDirective) return true;
|
|
30705
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30706
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30707
|
+
let callsHook = false;
|
|
30708
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30709
|
+
if (callsHook) return false;
|
|
30710
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30711
|
+
callsHook = true;
|
|
30712
|
+
return false;
|
|
30713
|
+
}
|
|
30714
|
+
});
|
|
30715
|
+
return callsHook;
|
|
30716
|
+
};
|
|
30717
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30718
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30719
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30720
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30721
|
+
let cursor = node;
|
|
30722
|
+
while (cursor) {
|
|
30723
|
+
ancestors.add(cursor);
|
|
30724
|
+
cursor = cursor.parent ?? null;
|
|
30725
|
+
}
|
|
30726
|
+
for (const statement of body.body ?? []) {
|
|
30727
|
+
if (ancestors.has(statement)) return false;
|
|
30728
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30729
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30730
|
+
let returnsEarly = false;
|
|
30731
|
+
walkAst(statement.consequent, (child) => {
|
|
30732
|
+
if (isFunctionLike$1(child)) return false;
|
|
30733
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30734
|
+
returnsEarly = true;
|
|
30735
|
+
return false;
|
|
30736
|
+
}
|
|
30737
|
+
});
|
|
30738
|
+
if (returnsEarly) return true;
|
|
30739
|
+
}
|
|
30740
|
+
return false;
|
|
30741
|
+
};
|
|
30742
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30743
|
+
let cursor = node.parent;
|
|
30744
|
+
while (cursor) {
|
|
30745
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30746
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30747
|
+
cursor = cursor.parent ?? null;
|
|
30748
|
+
}
|
|
30749
|
+
return null;
|
|
30750
|
+
};
|
|
30751
|
+
const noLocaleFormatInRender = defineRule({
|
|
30752
|
+
id: "no-locale-format-in-render",
|
|
30753
|
+
title: "Locale/timezone formatting during render",
|
|
30754
|
+
severity: "warn",
|
|
30755
|
+
category: "Correctness",
|
|
30756
|
+
disabledWhen: [
|
|
30757
|
+
"vite",
|
|
30758
|
+
"cra",
|
|
30759
|
+
"expo",
|
|
30760
|
+
"react-native",
|
|
30761
|
+
"unknown"
|
|
30762
|
+
],
|
|
30763
|
+
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.",
|
|
30764
|
+
create: (context) => {
|
|
30765
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30766
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30767
|
+
let fileHasUseClientDirective = false;
|
|
30768
|
+
let fileIsEmailTemplate = false;
|
|
30769
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30770
|
+
const reportIfRenderPhase = (match) => {
|
|
30771
|
+
if (reportedNodes.has(match.node)) return;
|
|
30772
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30773
|
+
if (!componentOrHookNode) return;
|
|
30774
|
+
if (fileIsEmailTemplate) return;
|
|
30775
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30776
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30777
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30778
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30779
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30780
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30781
|
+
reportedNodes.add(match.node);
|
|
30782
|
+
context.report({
|
|
30783
|
+
node: match.node,
|
|
30784
|
+
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.`
|
|
30785
|
+
});
|
|
30786
|
+
};
|
|
30787
|
+
return {
|
|
30788
|
+
Program(node) {
|
|
30789
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30790
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30791
|
+
},
|
|
30792
|
+
CallExpression(node) {
|
|
30793
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30794
|
+
if (match) reportIfRenderPhase(match);
|
|
30795
|
+
},
|
|
30796
|
+
TemplateLiteral(node) {
|
|
30797
|
+
const match = matchDateDefaultStringification(node);
|
|
30798
|
+
if (match) reportIfRenderPhase(match);
|
|
30799
|
+
},
|
|
30800
|
+
JSXExpressionContainer(node) {
|
|
30801
|
+
const expression = stripParenExpression(node.expression);
|
|
30802
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30803
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30804
|
+
const helperName = expression.callee.name;
|
|
30805
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30806
|
+
if (!componentOrHookNode) return;
|
|
30807
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30808
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30809
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30810
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30811
|
+
if (isFunctionLike$1(child)) return false;
|
|
30812
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30813
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30814
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30815
|
+
if (fileIsEmailTemplate) return;
|
|
30816
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30817
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30818
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30819
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30820
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30821
|
+
reportedNodes.add(match.node);
|
|
30822
|
+
context.report({
|
|
30823
|
+
node: match.node,
|
|
30824
|
+
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.`
|
|
30825
|
+
});
|
|
30826
|
+
});
|
|
30827
|
+
}
|
|
30828
|
+
};
|
|
30829
|
+
}
|
|
30830
|
+
});
|
|
30831
|
+
//#endregion
|
|
29336
30832
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29337
30833
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29338
30834
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30146,7 +31642,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30146
31642
|
"reverse",
|
|
30147
31643
|
"sort"
|
|
30148
31644
|
]);
|
|
30149
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30150
31645
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30151
31646
|
"assign",
|
|
30152
31647
|
"defineProperties",
|
|
@@ -30220,7 +31715,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30220
31715
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30221
31716
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30222
31717
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30223
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30224
31718
|
}
|
|
30225
31719
|
}
|
|
30226
31720
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30259,6 +31753,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30259
31753
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30260
31754
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30261
31755
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
31756
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30262
31757
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30263
31758
|
});
|
|
30264
31759
|
return mutations;
|
|
@@ -30491,7 +31986,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30491
31986
|
id: "no-nested-component-definition",
|
|
30492
31987
|
title: "Component defined inside another component",
|
|
30493
31988
|
tags: ["test-noise", "react-jsx-only"],
|
|
30494
|
-
severity: "
|
|
31989
|
+
severity: "warn",
|
|
30495
31990
|
category: "Correctness",
|
|
30496
31991
|
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.",
|
|
30497
31992
|
create: (context) => {
|
|
@@ -31446,12 +32941,12 @@ const noPassDataToParent = defineRule({
|
|
|
31446
32941
|
if (calleeNode === identifier) {
|
|
31447
32942
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31448
32943
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31449
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32944
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31450
32945
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31451
32946
|
if (isCustomHookParameter(ref)) continue;
|
|
31452
32947
|
} else continue;
|
|
31453
32948
|
const methodName = getCallMethodName(calleeNode);
|
|
31454
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32949
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31455
32950
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31456
32951
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31457
32952
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31642,7 +33137,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31642
33137
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31643
33138
|
const calleeNode = callExpr.callee;
|
|
31644
33139
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31645
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
33140
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31646
33141
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31647
33142
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31648
33143
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -31970,40 +33465,6 @@ const noPreventDefault = defineRule({
|
|
|
31970
33465
|
}
|
|
31971
33466
|
});
|
|
31972
33467
|
//#endregion
|
|
31973
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31974
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
31975
|
-
let node = callExpression;
|
|
31976
|
-
let parent = node.parent;
|
|
31977
|
-
while (parent) {
|
|
31978
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31979
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31980
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31981
|
-
node = parent;
|
|
31982
|
-
parent = node.parent;
|
|
31983
|
-
continue;
|
|
31984
|
-
}
|
|
31985
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31986
|
-
node = parent;
|
|
31987
|
-
parent = node.parent;
|
|
31988
|
-
continue;
|
|
31989
|
-
}
|
|
31990
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31991
|
-
node = parent;
|
|
31992
|
-
parent = node.parent;
|
|
31993
|
-
continue;
|
|
31994
|
-
}
|
|
31995
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31996
|
-
const expressions = parent.expressions ?? [];
|
|
31997
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
31998
|
-
node = parent;
|
|
31999
|
-
parent = node.parent;
|
|
32000
|
-
continue;
|
|
32001
|
-
}
|
|
32002
|
-
return false;
|
|
32003
|
-
}
|
|
32004
|
-
return false;
|
|
32005
|
-
};
|
|
32006
|
-
//#endregion
|
|
32007
33468
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32008
33469
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32009
33470
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32210,7 +33671,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32210
33671
|
return `${callee.name}()`;
|
|
32211
33672
|
}
|
|
32212
33673
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32213
|
-
const receiver = callee.object;
|
|
33674
|
+
const receiver = stripParenExpression(callee.object);
|
|
32214
33675
|
const property = callee.property;
|
|
32215
33676
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32216
33677
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32331,9 +33792,11 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32331
33792
|
if (typeof sourceValue !== "string") return;
|
|
32332
33793
|
if (handleExtraSource?.(node, context)) return;
|
|
32333
33794
|
if (sourceValue !== source) return;
|
|
33795
|
+
if (isTypeOnlyImport(node)) return;
|
|
32334
33796
|
for (const specifier of node.specifiers ?? []) {
|
|
32335
33797
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
32336
|
-
|
|
33798
|
+
if (specifier.importKind === "type") continue;
|
|
33799
|
+
const importedName = getImportedName(specifier);
|
|
32337
33800
|
if (!importedName) continue;
|
|
32338
33801
|
const message = messages.get(importedName);
|
|
32339
33802
|
if (message) context.report({
|
|
@@ -32351,8 +33814,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32351
33814
|
MemberExpression(node) {
|
|
32352
33815
|
if (namespaceBindings.size === 0) return;
|
|
32353
33816
|
if (node.computed) return;
|
|
32354
|
-
|
|
32355
|
-
if (!
|
|
33817
|
+
const receiver = stripParenExpression(node.object);
|
|
33818
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33819
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32356
33820
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32357
33821
|
const message = messages.get(node.property.name);
|
|
32358
33822
|
if (message) context.report({
|
|
@@ -32385,7 +33849,7 @@ const buildTestUtilsMessage = (importedName) => {
|
|
|
32385
33849
|
const reportTestUtilsImports = (node, context) => {
|
|
32386
33850
|
for (const specifier of node.specifiers ?? []) {
|
|
32387
33851
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
32388
|
-
const importedName = getImportedName
|
|
33852
|
+
const importedName = getImportedName(specifier) ?? "default";
|
|
32389
33853
|
context.report({
|
|
32390
33854
|
node: specifier,
|
|
32391
33855
|
message: buildTestUtilsMessage(importedName)
|
|
@@ -32417,7 +33881,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32417
33881
|
});
|
|
32418
33882
|
//#endregion
|
|
32419
33883
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32420
|
-
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."]
|
|
33884
|
+
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."]]);
|
|
32421
33885
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32422
33886
|
if (!rawFilename) return false;
|
|
32423
33887
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32455,7 +33919,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32455
33919
|
requires: ["react:19"],
|
|
32456
33920
|
tags: ["test-noise", "migration-hint"],
|
|
32457
33921
|
severity: "warn",
|
|
32458
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33922
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32459
33923
|
create: (context) => {
|
|
32460
33924
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32461
33925
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32779,34 +34243,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32779
34243
|
});
|
|
32780
34244
|
//#endregion
|
|
32781
34245
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32782
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32783
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32784
|
-
visitedSymbols.add(symbol);
|
|
32785
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
32786
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32787
|
-
const source = symbol.initializer;
|
|
32788
|
-
if (!source) return false;
|
|
32789
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32790
|
-
};
|
|
32791
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32792
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32793
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32794
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
32795
|
-
};
|
|
32796
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32797
|
-
let current = node;
|
|
32798
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
32799
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32800
|
-
current = current.object;
|
|
32801
|
-
}
|
|
32802
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32803
|
-
return false;
|
|
32804
|
-
};
|
|
32805
34246
|
const isInsideComponentContext = (node) => {
|
|
32806
34247
|
let cursor = node.parent;
|
|
32807
34248
|
while (cursor) {
|
|
32808
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32809
34249
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
34250
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32810
34251
|
cursor = cursor.parent ?? null;
|
|
32811
34252
|
}
|
|
32812
34253
|
return false;
|
|
@@ -32816,24 +34257,28 @@ const functionBodyOf = (node) => {
|
|
|
32816
34257
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32817
34258
|
return null;
|
|
32818
34259
|
};
|
|
34260
|
+
const isHookCallee = (callee) => {
|
|
34261
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
34262
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
34263
|
+
return false;
|
|
34264
|
+
};
|
|
32819
34265
|
const containsHookCall = (body) => {
|
|
32820
34266
|
let found = false;
|
|
32821
34267
|
walkAst(body, (child) => {
|
|
32822
|
-
if (found) return;
|
|
34268
|
+
if (found) return false;
|
|
34269
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32823
34270
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32824
|
-
|
|
32825
|
-
if (name && isReactHookName(name)) found = true;
|
|
34271
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32826
34272
|
});
|
|
32827
34273
|
return found;
|
|
32828
34274
|
};
|
|
32829
|
-
const
|
|
34275
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32830
34276
|
if (!symbol) return false;
|
|
32831
34277
|
const declaration = symbol.declarationNode;
|
|
32832
34278
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32833
34279
|
const body = functionBodyOf(declaration);
|
|
32834
34280
|
if (!body) return false;
|
|
32835
|
-
|
|
32836
|
-
return !containsHookCall(body);
|
|
34281
|
+
return containsHookCall(body);
|
|
32837
34282
|
};
|
|
32838
34283
|
const noRenderInRender = defineRule({
|
|
32839
34284
|
id: "no-render-in-render",
|
|
@@ -32842,20 +34287,13 @@ const noRenderInRender = defineRule({
|
|
|
32842
34287
|
tags: ["test-noise"],
|
|
32843
34288
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32844
34289
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32845
|
-
const expression = node.expression;
|
|
34290
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32846
34291
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
34292
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
34293
|
+
const calleeName = expression.callee.name;
|
|
34294
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32851
34295
|
if (!isInsideComponentContext(node)) return;
|
|
32852
|
-
if (
|
|
32853
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32854
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32855
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32856
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32857
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32858
|
-
}
|
|
34296
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32859
34297
|
context.report({
|
|
32860
34298
|
node: expression,
|
|
32861
34299
|
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.`
|
|
@@ -32916,13 +34354,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32916
34354
|
//#endregion
|
|
32917
34355
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32918
34356
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
32919
|
-
const isReactDomRenderCall = (node) =>
|
|
32920
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32921
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32922
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32923
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32924
|
-
return node.callee.property.name === "render";
|
|
32925
|
-
};
|
|
34357
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32926
34358
|
const isUsedAsReturnValue = (parent) => {
|
|
32927
34359
|
if (!parent) return false;
|
|
32928
34360
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33758,7 +35190,7 @@ const noSetState = defineRule({
|
|
|
33758
35190
|
category: "Architecture",
|
|
33759
35191
|
create: (context) => ({ CallExpression(node) {
|
|
33760
35192
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33761
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
35193
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33762
35194
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33763
35195
|
if (!getParentComponent(node)) return;
|
|
33764
35196
|
context.report({
|
|
@@ -33924,6 +35356,204 @@ const noSideTabBorder = defineRule({
|
|
|
33924
35356
|
})
|
|
33925
35357
|
});
|
|
33926
35358
|
//#endregion
|
|
35359
|
+
//#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
|
|
35360
|
+
const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
|
|
35361
|
+
"window",
|
|
35362
|
+
"globalThis",
|
|
35363
|
+
"self"
|
|
35364
|
+
]);
|
|
35365
|
+
const NULLISH_COMPARISON_OPERATORS = new Set([
|
|
35366
|
+
"==",
|
|
35367
|
+
"===",
|
|
35368
|
+
"!=",
|
|
35369
|
+
"!=="
|
|
35370
|
+
]);
|
|
35371
|
+
const getGlobalTimerCalleeName = (node) => {
|
|
35372
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
35373
|
+
const callee = stripParenExpression(node.callee);
|
|
35374
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
35375
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
35376
|
+
const receiver = stripParenExpression(callee.object);
|
|
35377
|
+
if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
|
|
35378
|
+
}
|
|
35379
|
+
return null;
|
|
35380
|
+
};
|
|
35381
|
+
const getRefCurrentReceiverName = (node) => {
|
|
35382
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
|
|
35383
|
+
if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
35384
|
+
const receiver = stripParenExpression(node.object);
|
|
35385
|
+
return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
35386
|
+
};
|
|
35387
|
+
const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
|
|
35388
|
+
const parseClearCallOnTimerRef = (node) => {
|
|
35389
|
+
const clearCalleeName = getGlobalTimerCalleeName(node);
|
|
35390
|
+
if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
|
|
35391
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
35392
|
+
const clearedArgument = node.arguments?.[0];
|
|
35393
|
+
if (!clearedArgument) return null;
|
|
35394
|
+
const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
|
|
35395
|
+
return refName === null ? null : {
|
|
35396
|
+
clearCalleeName,
|
|
35397
|
+
refName
|
|
35398
|
+
};
|
|
35399
|
+
};
|
|
35400
|
+
const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
|
|
35401
|
+
const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
|
|
35402
|
+
const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
|
|
35403
|
+
const isClearGuardIfIdiom = (ifStatement, refName) => {
|
|
35404
|
+
if (ifStatement.alternate) return false;
|
|
35405
|
+
const consequent = ifStatement.consequent;
|
|
35406
|
+
return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
|
|
35407
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
35408
|
+
const expression = stripParenExpression(statement.expression);
|
|
35409
|
+
if (isClearCallOnRef(expression, refName)) return true;
|
|
35410
|
+
return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
|
|
35411
|
+
});
|
|
35412
|
+
};
|
|
35413
|
+
const climbBooleanProjection = (refCurrentMember) => {
|
|
35414
|
+
let cursor = refCurrentMember;
|
|
35415
|
+
const logicalAncestors = [];
|
|
35416
|
+
let didPassBooleanProjection = false;
|
|
35417
|
+
while (cursor.parent) {
|
|
35418
|
+
const parent = cursor.parent;
|
|
35419
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
|
|
35420
|
+
cursor = parent;
|
|
35421
|
+
continue;
|
|
35422
|
+
}
|
|
35423
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
|
|
35424
|
+
didPassBooleanProjection = true;
|
|
35425
|
+
cursor = parent;
|
|
35426
|
+
continue;
|
|
35427
|
+
}
|
|
35428
|
+
if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
|
|
35429
|
+
didPassBooleanProjection = true;
|
|
35430
|
+
cursor = parent;
|
|
35431
|
+
continue;
|
|
35432
|
+
}
|
|
35433
|
+
if (isNodeOfType(parent, "LogicalExpression")) {
|
|
35434
|
+
logicalAncestors.push(parent);
|
|
35435
|
+
cursor = parent;
|
|
35436
|
+
continue;
|
|
35437
|
+
}
|
|
35438
|
+
break;
|
|
35439
|
+
}
|
|
35440
|
+
return {
|
|
35441
|
+
conditionRoot: cursor,
|
|
35442
|
+
logicalAncestors,
|
|
35443
|
+
didPassBooleanProjection
|
|
35444
|
+
};
|
|
35445
|
+
};
|
|
35446
|
+
const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
|
|
35447
|
+
const isPendingSignalRead = (refCurrentMember, refName) => {
|
|
35448
|
+
const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
|
|
35449
|
+
const conditionParent = conditionRoot.parent;
|
|
35450
|
+
if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
|
|
35451
|
+
if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
|
|
35452
|
+
if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
|
|
35453
|
+
return didPassBooleanProjection;
|
|
35454
|
+
};
|
|
35455
|
+
const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
35456
|
+
const facts = {
|
|
35457
|
+
holdsScheduledTimerId: false,
|
|
35458
|
+
hasPendingSignalRead: false
|
|
35459
|
+
};
|
|
35460
|
+
walkAst(ownerScope, (child) => {
|
|
35461
|
+
if (isAssignmentToRefCurrent(child, refName)) {
|
|
35462
|
+
const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
|
|
35463
|
+
if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
|
|
35464
|
+
return;
|
|
35465
|
+
}
|
|
35466
|
+
if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
|
|
35467
|
+
});
|
|
35468
|
+
return facts;
|
|
35469
|
+
};
|
|
35470
|
+
const isEffectCallbackFunction = (functionNode) => {
|
|
35471
|
+
const parent = functionNode.parent;
|
|
35472
|
+
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
35473
|
+
return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
|
|
35474
|
+
};
|
|
35475
|
+
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
35476
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
35477
|
+
let isReturnedByName = false;
|
|
35478
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
35479
|
+
if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
35480
|
+
const returned = stripParenExpression(child.argument);
|
|
35481
|
+
if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
|
|
35482
|
+
});
|
|
35483
|
+
return isReturnedByName;
|
|
35484
|
+
};
|
|
35485
|
+
const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
35486
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
35487
|
+
if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
|
|
35488
|
+
if (effectCallback.body === functionNode) return true;
|
|
35489
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
35490
|
+
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
35491
|
+
};
|
|
35492
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
35493
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
35494
|
+
if (cleanupBindingName === null) return false;
|
|
35495
|
+
let isReturnedFromEffect = false;
|
|
35496
|
+
walkAst(ownerScope, (child) => {
|
|
35497
|
+
if (isReturnedFromEffect) return false;
|
|
35498
|
+
if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
35499
|
+
const effectCallback = getEffectCallback(child);
|
|
35500
|
+
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
35501
|
+
});
|
|
35502
|
+
return isReturnedFromEffect;
|
|
35503
|
+
};
|
|
35504
|
+
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
35505
|
+
let functionNode = findEnclosingFunction(node);
|
|
35506
|
+
while (functionNode) {
|
|
35507
|
+
const outerFunction = findEnclosingFunction(functionNode);
|
|
35508
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
35509
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
35510
|
+
functionNode = outerFunction;
|
|
35511
|
+
}
|
|
35512
|
+
return false;
|
|
35513
|
+
};
|
|
35514
|
+
const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
|
|
35515
|
+
const enclosingFunction = findEnclosingFunction(clearCall);
|
|
35516
|
+
if (!isFunctionLike$1(enclosingFunction)) return false;
|
|
35517
|
+
if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
35518
|
+
const clearStart = getRangeStart(clearCall);
|
|
35519
|
+
if (clearStart === null) return false;
|
|
35520
|
+
let didFindLaterReassignment = false;
|
|
35521
|
+
walkInsideStatementBlocks(enclosingFunction.body, (child) => {
|
|
35522
|
+
if (didFindLaterReassignment) return;
|
|
35523
|
+
if (!isAssignmentToRefCurrent(child, refName)) return;
|
|
35524
|
+
const assignmentStart = getRangeStart(child);
|
|
35525
|
+
if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
|
|
35526
|
+
});
|
|
35527
|
+
return didFindLaterReassignment;
|
|
35528
|
+
};
|
|
35529
|
+
const isShadowedTimerGlobal = (clearCall) => {
|
|
35530
|
+
const callee = stripParenExpression(clearCall.callee);
|
|
35531
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
35532
|
+
return findVariableInitializer(callee, callee.name) !== null;
|
|
35533
|
+
};
|
|
35534
|
+
const noStaleTimerRef = defineRule({
|
|
35535
|
+
id: "no-stale-timer-ref",
|
|
35536
|
+
title: "Cleared timer ref keeps the stale id",
|
|
35537
|
+
severity: "warn",
|
|
35538
|
+
recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
|
|
35539
|
+
create: (context) => ({ CallExpression(node) {
|
|
35540
|
+
const clearCall = parseClearCallOnTimerRef(node);
|
|
35541
|
+
if (!clearCall) return;
|
|
35542
|
+
if (isShadowedTimerGlobal(node)) return;
|
|
35543
|
+
const { clearCalleeName, refName } = clearCall;
|
|
35544
|
+
const refBinding = findVariableInitializer(node, refName);
|
|
35545
|
+
if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
|
|
35546
|
+
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
35547
|
+
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
35548
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
35549
|
+
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
35550
|
+
context.report({
|
|
35551
|
+
node,
|
|
35552
|
+
message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
|
|
35553
|
+
});
|
|
35554
|
+
} })
|
|
35555
|
+
});
|
|
35556
|
+
//#endregion
|
|
33927
35557
|
//#region src/plugin/utils/is-abstract-role.ts
|
|
33928
35558
|
const isAbstractRole = (openingElement, settings) => {
|
|
33929
35559
|
const elementType = getElementType(openingElement, settings);
|
|
@@ -36204,6 +37834,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36204
37834
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36205
37835
|
return true;
|
|
36206
37836
|
};
|
|
37837
|
+
const isTrivialContainerLiteral = (node) => {
|
|
37838
|
+
if (!node) return false;
|
|
37839
|
+
const innerExpression = stripParenExpression(node);
|
|
37840
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
37841
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
37842
|
+
return false;
|
|
37843
|
+
};
|
|
37844
|
+
const isNonEscapingRead = (identifier) => {
|
|
37845
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
37846
|
+
const memberNode = readRoot.parent;
|
|
37847
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
37848
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
37849
|
+
const memberUseParent = memberUse.parent;
|
|
37850
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
37851
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
37852
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
37853
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
37854
|
+
};
|
|
37855
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
37856
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
37857
|
+
const parentNode = memoUsageRoot.parent;
|
|
37858
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
37859
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
37860
|
+
const bindingTarget = parentNode.id;
|
|
37861
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
37862
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
37863
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
37864
|
+
if (!symbol) return false;
|
|
37865
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
37866
|
+
};
|
|
36207
37867
|
const noUsememoSimpleExpression = defineRule({
|
|
36208
37868
|
id: "no-usememo-simple-expression",
|
|
36209
37869
|
title: "useMemo on a cheap value",
|
|
@@ -36225,9 +37885,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36225
37885
|
let returnExpression = null;
|
|
36226
37886
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36227
37887
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36228
|
-
if (returnExpression
|
|
37888
|
+
if (!returnExpression) return;
|
|
37889
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
37890
|
+
context.report({
|
|
37891
|
+
node,
|
|
37892
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37893
|
+
});
|
|
37894
|
+
return;
|
|
37895
|
+
}
|
|
37896
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36229
37897
|
node,
|
|
36230
|
-
message: "This
|
|
37898
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36231
37899
|
});
|
|
36232
37900
|
} })
|
|
36233
37901
|
});
|
|
@@ -36333,7 +38001,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36333
38001
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36334
38002
|
return { CallExpression(node) {
|
|
36335
38003
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36336
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
38004
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36337
38005
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36338
38006
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36339
38007
|
context.report({
|
|
@@ -36650,6 +38318,10 @@ const isRouteFactoryCall = (expression) => {
|
|
|
36650
38318
|
}
|
|
36651
38319
|
return false;
|
|
36652
38320
|
};
|
|
38321
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
|
|
38322
|
+
const expression = skipTsExpression(argument);
|
|
38323
|
+
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
38324
|
+
});
|
|
36653
38325
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
36654
38326
|
const isHocCallee = (callee, state) => {
|
|
36655
38327
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -36689,7 +38361,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
36689
38361
|
continue;
|
|
36690
38362
|
}
|
|
36691
38363
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36692
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
38364
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
36693
38365
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36694
38366
|
}
|
|
36695
38367
|
return false;
|
|
@@ -36797,18 +38469,22 @@ const onlyExportComponents = defineRule({
|
|
|
36797
38469
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36798
38470
|
allowExportNames: new Set(settings.allowExportNames),
|
|
36799
38471
|
allowConstantExport: settings.allowConstantExport,
|
|
36800
|
-
localComponentNames
|
|
38472
|
+
localComponentNames,
|
|
38473
|
+
scopes: context.scopes
|
|
36801
38474
|
};
|
|
36802
38475
|
for (const child of componentCandidates) {
|
|
36803
38476
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36804
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
38477
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
36805
38478
|
}
|
|
36806
38479
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36807
38480
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36808
38481
|
}
|
|
36809
38482
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36810
38483
|
const initializer = child.init;
|
|
36811
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
38484
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
38485
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
38486
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
38487
|
+
}
|
|
36812
38488
|
}
|
|
36813
38489
|
}
|
|
36814
38490
|
const exports = [];
|
|
@@ -36878,6 +38554,10 @@ const onlyExportComponents = defineRule({
|
|
|
36878
38554
|
return false;
|
|
36879
38555
|
})();
|
|
36880
38556
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
38557
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
38558
|
+
kind: "non-component",
|
|
38559
|
+
reportNode: stripped
|
|
38560
|
+
});
|
|
36881
38561
|
else context.report({
|
|
36882
38562
|
node: stripped,
|
|
36883
38563
|
message: ANONYMOUS_MESSAGE
|
|
@@ -37768,8 +39448,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37768
39448
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37769
39449
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37770
39450
|
if (roleAttribute) {
|
|
37771
|
-
const
|
|
37772
|
-
if (
|
|
39451
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
39452
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37773
39453
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37774
39454
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37775
39455
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -38473,11 +40153,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38473
40153
|
}
|
|
38474
40154
|
return null;
|
|
38475
40155
|
};
|
|
40156
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
40157
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
40158
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
40159
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
40160
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
40161
|
+
return false;
|
|
40162
|
+
};
|
|
38476
40163
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38477
40164
|
const handlerStatements = getCallbackStatements(handler);
|
|
38478
40165
|
if (handlerStatements.length !== 1) return null;
|
|
38479
40166
|
const onlyStatement = handlerStatements[0];
|
|
38480
|
-
|
|
40167
|
+
let expression = onlyStatement;
|
|
40168
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
40169
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
40170
|
+
if (!expression) return null;
|
|
40171
|
+
expression = stripParenExpression(expression);
|
|
38481
40172
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38482
40173
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38483
40174
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38487,6 +40178,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38487
40178
|
setterArgument: expression.arguments[0]
|
|
38488
40179
|
};
|
|
38489
40180
|
};
|
|
40181
|
+
const isListenerCollectionInitializer = (init) => {
|
|
40182
|
+
if (!init) return false;
|
|
40183
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
40184
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
40185
|
+
};
|
|
40186
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
40187
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
40188
|
+
const firstParam = functionNode.params?.[0];
|
|
40189
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
40190
|
+
const listenerParamName = firstParam.name;
|
|
40191
|
+
let registersListener = false;
|
|
40192
|
+
walkAst(functionNode.body, (child) => {
|
|
40193
|
+
if (registersListener) return false;
|
|
40194
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40195
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
40196
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
40197
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
40198
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
40199
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
40200
|
+
const registeredArgument = child.arguments?.[0];
|
|
40201
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
40202
|
+
});
|
|
40203
|
+
return registersListener;
|
|
40204
|
+
};
|
|
40205
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
40206
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
40207
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
40208
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
40209
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
40210
|
+
mutableBindingNames,
|
|
40211
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
40212
|
+
};
|
|
40213
|
+
for (const statement of programRoot.body ?? []) {
|
|
40214
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
40215
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
40216
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
40217
|
+
continue;
|
|
40218
|
+
}
|
|
40219
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
40220
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
40221
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
40222
|
+
const init = declarator.init ?? null;
|
|
40223
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
40224
|
+
mutableBindingNames.add(declarator.id.name);
|
|
40225
|
+
continue;
|
|
40226
|
+
}
|
|
40227
|
+
if (isListenerCollectionInitializer(init)) {
|
|
40228
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
40229
|
+
continue;
|
|
40230
|
+
}
|
|
40231
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
40232
|
+
}
|
|
40233
|
+
}
|
|
40234
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
40235
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
40236
|
+
return {
|
|
40237
|
+
mutableBindingNames,
|
|
40238
|
+
subscribeFunctionNames
|
|
40239
|
+
};
|
|
40240
|
+
};
|
|
40241
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
40242
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
40243
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
40244
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
40245
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
40246
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
40247
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
40248
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
40249
|
+
return initialArgument.name;
|
|
40250
|
+
};
|
|
40251
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
40252
|
+
if (!argument) return false;
|
|
40253
|
+
const unwrapped = stripParenExpression(argument);
|
|
40254
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
40255
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
40256
|
+
let callsSetter = false;
|
|
40257
|
+
walkAst(unwrapped.body, (child) => {
|
|
40258
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
40259
|
+
callsSetter = true;
|
|
40260
|
+
return false;
|
|
40261
|
+
}
|
|
40262
|
+
});
|
|
40263
|
+
return callsSetter;
|
|
40264
|
+
};
|
|
40265
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
40266
|
+
let matchedCall = null;
|
|
40267
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
40268
|
+
if (matchedCall) return false;
|
|
40269
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40270
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
40271
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
40272
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
40273
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
40274
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
40275
|
+
matchedCall = child;
|
|
40276
|
+
return false;
|
|
40277
|
+
}
|
|
40278
|
+
});
|
|
40279
|
+
return matchedCall;
|
|
40280
|
+
};
|
|
38490
40281
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38491
40282
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38492
40283
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38503,6 +40294,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38503
40294
|
severity: "warn",
|
|
38504
40295
|
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.",
|
|
38505
40296
|
create: (context) => {
|
|
40297
|
+
let cachedStoreIndex = null;
|
|
40298
|
+
const storeIndexFor = (node) => {
|
|
40299
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
40300
|
+
const programRoot = findProgramRoot(node);
|
|
40301
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
40302
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
40303
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
40304
|
+
};
|
|
40305
|
+
return cachedStoreIndex;
|
|
40306
|
+
};
|
|
38506
40307
|
const checkComponent = (componentBody) => {
|
|
38507
40308
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38508
40309
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38540,6 +40341,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38540
40341
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38541
40342
|
if (!useStateInitializer) continue;
|
|
38542
40343
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
40344
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38543
40345
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38544
40346
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38545
40347
|
context.report({
|
|
@@ -38547,14 +40349,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38547
40349
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38548
40350
|
});
|
|
38549
40351
|
}
|
|
40352
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
40353
|
+
};
|
|
40354
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
40355
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
40356
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
40357
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
40358
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
40359
|
+
binding,
|
|
40360
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
40361
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
40362
|
+
if (snapshotBindings.length === 0) return;
|
|
40363
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
40364
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
40365
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
40366
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
40367
|
+
const depsNode = effectCall.arguments[1];
|
|
40368
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
40369
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
40370
|
+
const callback = getEffectCallback(effectCall);
|
|
40371
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
40372
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
40373
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
40374
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
40375
|
+
reportedDeclarators.add(binding.declarator);
|
|
40376
|
+
context.report({
|
|
40377
|
+
node: binding.declarator,
|
|
40378
|
+
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.`
|
|
40379
|
+
});
|
|
40380
|
+
}
|
|
40381
|
+
}
|
|
38550
40382
|
};
|
|
38551
40383
|
return {
|
|
38552
40384
|
FunctionDeclaration(node) {
|
|
38553
|
-
|
|
40385
|
+
const functionName = node.id?.name;
|
|
40386
|
+
if (!functionName) return;
|
|
40387
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38554
40388
|
checkComponent(node.body);
|
|
38555
40389
|
},
|
|
38556
40390
|
VariableDeclarator(node) {
|
|
38557
|
-
|
|
40391
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
40392
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38558
40393
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38559
40394
|
checkComponent(node.init.body);
|
|
38560
40395
|
}
|
|
@@ -39164,7 +40999,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39164
40999
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39165
41000
|
const body = queryFnValue.body;
|
|
39166
41001
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39167
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
41002
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39168
41003
|
node: queryFnProperty,
|
|
39169
41004
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39170
41005
|
});
|
|
@@ -39671,17 +41506,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39671
41506
|
}
|
|
39672
41507
|
});
|
|
39673
41508
|
//#endregion
|
|
39674
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39675
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39676
|
-
let ancestor = bindingIdentifier.parent;
|
|
39677
|
-
while (ancestor) {
|
|
39678
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39679
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39680
|
-
ancestor = ancestor.parent ?? null;
|
|
39681
|
-
}
|
|
39682
|
-
return null;
|
|
39683
|
-
};
|
|
39684
|
-
//#endregion
|
|
39685
41509
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39686
41510
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39687
41511
|
{
|
|
@@ -39690,19 +41514,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39690
41514
|
},
|
|
39691
41515
|
{
|
|
39692
41516
|
display: "Date.now()",
|
|
39693
|
-
matches: (node) =>
|
|
41517
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39694
41518
|
},
|
|
39695
41519
|
{
|
|
39696
41520
|
display: "Math.random()",
|
|
39697
|
-
matches: (node) =>
|
|
41521
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39698
41522
|
},
|
|
39699
41523
|
{
|
|
39700
41524
|
display: "performance.now()",
|
|
39701
|
-
matches: (node) =>
|
|
41525
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39702
41526
|
},
|
|
39703
41527
|
{
|
|
39704
41528
|
display: "crypto.randomUUID()",
|
|
39705
|
-
matches: (node) =>
|
|
41529
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39706
41530
|
}
|
|
39707
41531
|
];
|
|
39708
41532
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39714,54 +41538,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39714
41538
|
}
|
|
39715
41539
|
return null;
|
|
39716
41540
|
};
|
|
39717
|
-
const executesDuringRender = (functionNode) => {
|
|
39718
|
-
const parent = functionNode.parent;
|
|
39719
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39720
|
-
if (parent.callee === functionNode) return true;
|
|
39721
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39722
|
-
};
|
|
39723
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39724
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39725
|
-
const unwrapped = stripParenExpression(expression);
|
|
39726
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39727
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39728
|
-
const property = unwrapped.property;
|
|
39729
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39730
|
-
}
|
|
39731
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39732
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39733
|
-
return false;
|
|
39734
|
-
};
|
|
39735
|
-
const isFalsyLiteral = (node) => {
|
|
39736
|
-
if (!node) return true;
|
|
39737
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39738
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39739
|
-
};
|
|
39740
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39741
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39742
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39743
|
-
if (!binding) return false;
|
|
39744
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39745
|
-
if (!declarator?.init) return false;
|
|
39746
|
-
const init = stripParenExpression(declarator.init);
|
|
39747
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39748
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39749
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39750
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39751
|
-
};
|
|
39752
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39753
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39754
|
-
let cursor = node;
|
|
39755
|
-
let parent = node.parent;
|
|
39756
|
-
while (parent) {
|
|
39757
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39758
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39759
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39760
|
-
cursor = parent;
|
|
39761
|
-
parent = parent.parent ?? null;
|
|
39762
|
-
}
|
|
39763
|
-
return false;
|
|
39764
|
-
};
|
|
39765
41541
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39766
41542
|
const member = dateNode.parent;
|
|
39767
41543
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39785,23 +41561,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39785
41561
|
}
|
|
39786
41562
|
return false;
|
|
39787
41563
|
};
|
|
39788
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
39789
|
-
let cursor = node;
|
|
39790
|
-
let parent = node.parent;
|
|
39791
|
-
while (parent) {
|
|
39792
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39793
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39794
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39795
|
-
cursor = parent;
|
|
39796
|
-
parent = parent.parent ?? null;
|
|
39797
|
-
}
|
|
39798
|
-
return false;
|
|
39799
|
-
};
|
|
39800
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39801
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39802
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39803
|
-
return false;
|
|
39804
|
-
};
|
|
39805
41564
|
const renderingHydrationMismatchTime = defineRule({
|
|
39806
41565
|
id: "rendering-hydration-mismatch-time",
|
|
39807
41566
|
title: "Time or random value in JSX",
|
|
@@ -39849,6 +41608,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39849
41608
|
}
|
|
39850
41609
|
});
|
|
39851
41610
|
//#endregion
|
|
41611
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
41612
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
41613
|
+
"toLocaleString",
|
|
41614
|
+
"toLocaleDateString",
|
|
41615
|
+
"toLocaleTimeString",
|
|
41616
|
+
"getTimezoneOffset"
|
|
41617
|
+
]);
|
|
41618
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
41619
|
+
let readsLocaleEnvironment = false;
|
|
41620
|
+
walkAst(expression, (child) => {
|
|
41621
|
+
if (readsLocaleEnvironment) return false;
|
|
41622
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
41623
|
+
if (child.object.name === "Intl") {
|
|
41624
|
+
readsLocaleEnvironment = true;
|
|
41625
|
+
return false;
|
|
41626
|
+
}
|
|
41627
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
41628
|
+
readsLocaleEnvironment = true;
|
|
41629
|
+
return false;
|
|
41630
|
+
}
|
|
41631
|
+
}
|
|
41632
|
+
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)) {
|
|
41633
|
+
readsLocaleEnvironment = true;
|
|
41634
|
+
return false;
|
|
41635
|
+
}
|
|
41636
|
+
});
|
|
41637
|
+
return readsLocaleEnvironment;
|
|
41638
|
+
};
|
|
41639
|
+
//#endregion
|
|
39852
41640
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39853
41641
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39854
41642
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39914,14 +41702,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39914
41702
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39915
41703
|
const callback = getEffectCallback(node);
|
|
39916
41704
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39917
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39918
|
-
if (
|
|
41705
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
41706
|
+
if (bodyStatements.length !== 1) return;
|
|
39919
41707
|
const soleStatement = bodyStatements[0];
|
|
39920
41708
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39921
41709
|
const expression = soleStatement.expression;
|
|
39922
41710
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39923
41711
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39924
41712
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
41713
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39925
41714
|
context.report({
|
|
39926
41715
|
node,
|
|
39927
41716
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40740,6 +42529,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40740
42529
|
} })
|
|
40741
42530
|
});
|
|
40742
42531
|
//#endregion
|
|
42532
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
42533
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
42534
|
+
//#endregion
|
|
40743
42535
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40744
42536
|
const rerenderLazyRefInit = defineRule({
|
|
40745
42537
|
id: "rerender-lazy-ref-init",
|
|
@@ -40750,7 +42542,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40750
42542
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40751
42543
|
create: (context) => ({ CallExpression(node) {
|
|
40752
42544
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40753
|
-
const initializer = node.arguments[0];
|
|
42545
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40754
42546
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40755
42547
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40756
42548
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40758,7 +42550,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40758
42550
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40759
42551
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40760
42552
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40761
|
-
if (
|
|
42553
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40762
42554
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40763
42555
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40764
42556
|
context.report({
|
|
@@ -40794,11 +42586,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40794
42586
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40795
42587
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40796
42588
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40797
|
-
|
|
40798
|
-
if (isNodeOfType(
|
|
40799
|
-
if (isNodeOfType(
|
|
40800
|
-
if (isNodeOfType(
|
|
40801
|
-
if (isNodeOfType(
|
|
42589
|
+
const innerExpression = stripParenExpression(expression);
|
|
42590
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
42591
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
42592
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
42593
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40802
42594
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40803
42595
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40804
42596
|
if (spreadCall) return spreadCall;
|
|
@@ -40821,7 +42613,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40821
42613
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40822
42614
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40823
42615
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40824
|
-
if (
|
|
42616
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40825
42617
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40826
42618
|
if (isReactHookName(calleeName)) return;
|
|
40827
42619
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41488,7 +43280,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41488
43280
|
const body = reactionFn.body;
|
|
41489
43281
|
let singleAssignment = null;
|
|
41490
43282
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41491
|
-
const statements = body.body ?? [];
|
|
43283
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41492
43284
|
if (statements.length !== 1) return;
|
|
41493
43285
|
const onlyStatement = statements[0];
|
|
41494
43286
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41562,7 +43354,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41562
43354
|
"catch",
|
|
41563
43355
|
"finally"
|
|
41564
43356
|
]);
|
|
41565
|
-
const findChainRoot = (
|
|
43357
|
+
const findChainRoot = (wrappedNode) => {
|
|
43358
|
+
const node = stripParenExpression(wrappedNode);
|
|
41566
43359
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41567
43360
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41568
43361
|
calleeName: node.callee.name,
|
|
@@ -42116,7 +43909,7 @@ const rnNoDeprecatedModules = defineRule({
|
|
|
42116
43909
|
for (const specifier of node.specifiers ?? []) {
|
|
42117
43910
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
42118
43911
|
if (specifier.importKind === "type") continue;
|
|
42119
|
-
const importedName = getImportedName
|
|
43912
|
+
const importedName = getImportedName(specifier);
|
|
42120
43913
|
if (!importedName) continue;
|
|
42121
43914
|
if (!DEPRECATED_RN_MODULE_REPLACEMENTS.get(importedName)) continue;
|
|
42122
43915
|
context.report({
|
|
@@ -42150,20 +43943,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42150
43943
|
};
|
|
42151
43944
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42152
43945
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42153
|
-
|
|
42154
|
-
|
|
43946
|
+
const receiver = stripParenExpression(callee.object);
|
|
43947
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
43948
|
+
const localName = receiver.name;
|
|
42155
43949
|
const binding = findVariableInitializer(node, localName);
|
|
42156
43950
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42157
43951
|
return localName === "Dimensions";
|
|
42158
43952
|
}
|
|
42159
|
-
if (isNodeOfType(
|
|
42160
|
-
if (getInitializerModuleSource(node,
|
|
42161
|
-
const rootName = getRootIdentifierName(
|
|
43953
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
43954
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
43955
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42162
43956
|
if (rootName === null) return false;
|
|
42163
43957
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42164
43958
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42165
43959
|
}
|
|
42166
|
-
if (getRequireCallSource(
|
|
43960
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42167
43961
|
return false;
|
|
42168
43962
|
};
|
|
42169
43963
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42607,7 +44401,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42607
44401
|
},
|
|
42608
44402
|
CallExpression(node) {
|
|
42609
44403
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42610
|
-
|
|
44404
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
44405
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42611
44406
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42612
44407
|
const stylesArgument = node.arguments?.[0];
|
|
42613
44408
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -42666,7 +44461,7 @@ const rnNoPanresponder = defineRule({
|
|
|
42666
44461
|
for (const specifier of node.specifiers ?? []) {
|
|
42667
44462
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
42668
44463
|
if (specifier.importKind === "type") continue;
|
|
42669
|
-
if (getImportedName
|
|
44464
|
+
if (getImportedName(specifier) !== "PanResponder") continue;
|
|
42670
44465
|
context.report({
|
|
42671
44466
|
node: specifier,
|
|
42672
44467
|
message: "PanResponder runs gesture handling on the JS thread, which stutters under load. Use react-native-gesture-handler (`Gesture.Pan()`) so gestures run on the native UI thread."
|
|
@@ -43457,7 +45252,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43457
45252
|
const callee = node.callee;
|
|
43458
45253
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43459
45254
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43460
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
45255
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43461
45256
|
context.report({
|
|
43462
45257
|
node,
|
|
43463
45258
|
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."
|
|
@@ -43596,7 +45391,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43596
45391
|
for (const specifier of node.specifiers ?? []) {
|
|
43597
45392
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
43598
45393
|
if (specifier.importKind === "type") continue;
|
|
43599
|
-
const importedName = getImportedName
|
|
45394
|
+
const importedName = getImportedName(specifier);
|
|
43600
45395
|
if (importedName !== "Image" && importedName !== "ImageBackground") continue;
|
|
43601
45396
|
flaggedImports.push({
|
|
43602
45397
|
localName: specifier.local.name,
|
|
@@ -43658,7 +45453,7 @@ const rnPreferPressable = defineRule({
|
|
|
43658
45453
|
for (const specifier of node.specifiers ?? []) {
|
|
43659
45454
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
43660
45455
|
if (specifier.importKind === "type") continue;
|
|
43661
|
-
const importedName = getImportedName
|
|
45456
|
+
const importedName = getImportedName(specifier);
|
|
43662
45457
|
if (!importedName || !TOUCHABLE_COMPONENTS.has(importedName)) continue;
|
|
43663
45458
|
context.report({
|
|
43664
45459
|
node: specifier,
|
|
@@ -43685,14 +45480,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43685
45480
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43686
45481
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43687
45482
|
const methodName = callee.property.name;
|
|
43688
|
-
|
|
45483
|
+
const receiver = stripParenExpression(callee.object);
|
|
45484
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43689
45485
|
factoryName: methodName,
|
|
43690
45486
|
chainMethodNames,
|
|
43691
45487
|
numberOfTapsArgument
|
|
43692
45488
|
};
|
|
43693
45489
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43694
45490
|
chainMethodNames.push(methodName);
|
|
43695
|
-
cursor =
|
|
45491
|
+
cursor = receiver;
|
|
43696
45492
|
}
|
|
43697
45493
|
return null;
|
|
43698
45494
|
};
|
|
@@ -43759,7 +45555,7 @@ const rnPreferReanimated = defineRule({
|
|
|
43759
45555
|
for (const specifier of node.specifiers ?? []) {
|
|
43760
45556
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
43761
45557
|
if (specifier.importKind === "type") continue;
|
|
43762
|
-
const importedName = getImportedName
|
|
45558
|
+
const importedName = getImportedName(specifier);
|
|
43763
45559
|
if (!importedName || !JS_THREAD_ANIMATION_IMPORTS.has(importedName)) continue;
|
|
43764
45560
|
const suggestion = importedName === "LayoutAnimation" ? "Your users see stutter when LayoutAnimation runs on the JS thread." : "Your users see stutter when Animated from react-native runs on the JS thread.";
|
|
43765
45561
|
context.report({
|
|
@@ -44217,9 +46013,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44217
46013
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44218
46014
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44219
46015
|
if (!roleAttribute) return;
|
|
44220
|
-
const
|
|
44221
|
-
if (
|
|
44222
|
-
const roles =
|
|
46016
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
46017
|
+
if (roleCandidates === null) return;
|
|
46018
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44223
46019
|
for (const role of roles) {
|
|
44224
46020
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44225
46021
|
if (!required) continue;
|
|
@@ -47336,7 +49132,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47336
49132
|
};
|
|
47337
49133
|
//#endregion
|
|
47338
49134
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47339
|
-
const buildMessageDefault = (
|
|
49135
|
+
const buildMessageDefault = (roles, propName) => {
|
|
49136
|
+
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.`;
|
|
49137
|
+
};
|
|
47340
49138
|
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.`;
|
|
47341
49139
|
const roleSupportsAriaProps = defineRule({
|
|
47342
49140
|
id: "role-supports-aria-props",
|
|
@@ -47356,6 +49154,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47356
49154
|
const propName = propRawName.toLowerCase();
|
|
47357
49155
|
if (!propName.startsWith("aria-")) continue;
|
|
47358
49156
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
49157
|
+
const attributeValue = attributeNode.value;
|
|
49158
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47359
49159
|
(ariaAttributes ??= []).push({
|
|
47360
49160
|
attribute,
|
|
47361
49161
|
propName
|
|
@@ -47364,17 +49164,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47364
49164
|
if (!ariaAttributes) return;
|
|
47365
49165
|
const elementType = getElementType(node, context.settings);
|
|
47366
49166
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47367
|
-
const
|
|
47368
|
-
if (
|
|
47369
|
-
|
|
49167
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
49168
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
49169
|
+
const supportedSets = [];
|
|
49170
|
+
for (const role of roleCandidates) {
|
|
49171
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
49172
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
49173
|
+
if (!supported) return;
|
|
49174
|
+
supportedSets.push(supported);
|
|
49175
|
+
}
|
|
47370
49176
|
const isImplicit = !roleAttribute;
|
|
47371
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47372
|
-
if (!supported) return;
|
|
47373
49177
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47374
|
-
if (supported.has(propName)) continue;
|
|
49178
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47375
49179
|
context.report({
|
|
47376
49180
|
node: attribute,
|
|
47377
|
-
message: isImplicit ? buildMessageImplicit(
|
|
49181
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47378
49182
|
});
|
|
47379
49183
|
}
|
|
47380
49184
|
} })
|
|
@@ -47721,21 +49525,6 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
47721
49525
|
}
|
|
47722
49526
|
return false;
|
|
47723
49527
|
};
|
|
47724
|
-
const isUseEffectEventSymbol = (symbol) => {
|
|
47725
|
-
const initializer = symbol.initializer;
|
|
47726
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47727
|
-
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47728
|
-
};
|
|
47729
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47730
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47731
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
47732
|
-
};
|
|
47733
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47734
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47735
|
-
const initializer = symbol.initializer;
|
|
47736
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47737
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47738
|
-
};
|
|
47739
49528
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47740
49529
|
let current = node.parent;
|
|
47741
49530
|
while (current) {
|
|
@@ -47925,8 +49714,7 @@ const rulesOfHooks = defineRule({
|
|
|
47925
49714
|
},
|
|
47926
49715
|
Identifier(node) {
|
|
47927
49716
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47928
|
-
if (!symbol || !
|
|
47929
|
-
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
49717
|
+
if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
|
|
47930
49718
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47931
49719
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47932
49720
|
context.report({
|
|
@@ -48074,7 +49862,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48074
49862
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48075
49863
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48076
49864
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48077
|
-
const
|
|
49865
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
49866
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48078
49867
|
if (!objectName) return;
|
|
48079
49868
|
const methodName = node.callee.property.name;
|
|
48080
49869
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48750,7 +50539,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48750
50539
|
};
|
|
48751
50540
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48752
50541
|
let chainTip = referenceIdentifier;
|
|
48753
|
-
while (chainTip.parent
|
|
50542
|
+
while (chainTip.parent) {
|
|
50543
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
50544
|
+
chainTip = chainTip.parent;
|
|
50545
|
+
continue;
|
|
50546
|
+
}
|
|
50547
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
50548
|
+
chainTip = chainTip.parent;
|
|
50549
|
+
continue;
|
|
50550
|
+
}
|
|
50551
|
+
break;
|
|
50552
|
+
}
|
|
48754
50553
|
return chainTip;
|
|
48755
50554
|
};
|
|
48756
50555
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48775,7 +50574,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48775
50574
|
const callee = callExpression.callee;
|
|
48776
50575
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48777
50576
|
const methodName = getMemberPropertyName$1(callee);
|
|
48778
|
-
|
|
50577
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
50578
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48779
50579
|
}
|
|
48780
50580
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48781
50581
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49505,7 +51305,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49505
51305
|
};
|
|
49506
51306
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49507
51307
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49508
|
-
let currentNode = outerNode.callee.object;
|
|
51308
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49509
51309
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49510
51310
|
const calleeName = getCalleeName$2(currentNode);
|
|
49511
51311
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49516,7 +51316,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49516
51316
|
}
|
|
49517
51317
|
}
|
|
49518
51318
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49519
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
51319
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49520
51320
|
else break;
|
|
49521
51321
|
}
|
|
49522
51322
|
return result;
|
|
@@ -50169,7 +51969,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50169
51969
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50170
51970
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50171
51971
|
if (methodName) methodNames.unshift(methodName);
|
|
50172
|
-
currentNode = currentNode.callee.object;
|
|
51972
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50173
51973
|
}
|
|
50174
51974
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50175
51975
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -50389,7 +52189,7 @@ const useLazyMotion = defineRule({
|
|
|
50389
52189
|
if (node.specifiers?.some((specifier) => {
|
|
50390
52190
|
if (!isNodeOfType(specifier, "ImportSpecifier")) return false;
|
|
50391
52191
|
if (specifier.importKind === "type") return false;
|
|
50392
|
-
return getImportedName
|
|
52192
|
+
return getImportedName(specifier) === "motion";
|
|
50393
52193
|
})) context.report({
|
|
50394
52194
|
node,
|
|
50395
52195
|
message: "Importing \"motion\" ships about 30 kb of extra code and slows page load. Use \"m\" with LazyMotion instead."
|
|
@@ -51416,6 +53216,18 @@ const reactDoctorRules = [
|
|
|
51416
53216
|
requires: [...new Set(["react", ...displayName.requires ?? []])]
|
|
51417
53217
|
}
|
|
51418
53218
|
},
|
|
53219
|
+
{
|
|
53220
|
+
key: "react-doctor/effect-listener-cleanup-mismatch",
|
|
53221
|
+
id: "effect-listener-cleanup-mismatch",
|
|
53222
|
+
source: "react-doctor",
|
|
53223
|
+
originallyExternal: false,
|
|
53224
|
+
rule: {
|
|
53225
|
+
...effectListenerCleanupMismatch,
|
|
53226
|
+
framework: "global",
|
|
53227
|
+
category: "Bugs",
|
|
53228
|
+
requires: [...new Set(["react", ...effectListenerCleanupMismatch.requires ?? []])]
|
|
53229
|
+
}
|
|
53230
|
+
},
|
|
51419
53231
|
{
|
|
51420
53232
|
key: "react-doctor/effect-needs-cleanup",
|
|
51421
53233
|
id: "effect-needs-cleanup",
|
|
@@ -53251,6 +55063,18 @@ const reactDoctorRules = [
|
|
|
53251
55063
|
category: "Bugs"
|
|
53252
55064
|
}
|
|
53253
55065
|
},
|
|
55066
|
+
{
|
|
55067
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
55068
|
+
id: "no-locale-format-in-render",
|
|
55069
|
+
source: "react-doctor",
|
|
55070
|
+
originallyExternal: false,
|
|
55071
|
+
rule: {
|
|
55072
|
+
...noLocaleFormatInRender,
|
|
55073
|
+
framework: "global",
|
|
55074
|
+
category: "Bugs",
|
|
55075
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
55076
|
+
}
|
|
55077
|
+
},
|
|
53254
55078
|
{
|
|
53255
55079
|
key: "react-doctor/no-long-transition-duration",
|
|
53256
55080
|
id: "no-long-transition-duration",
|
|
@@ -53679,6 +55503,18 @@ const reactDoctorRules = [
|
|
|
53679
55503
|
category: "Maintainability"
|
|
53680
55504
|
}
|
|
53681
55505
|
},
|
|
55506
|
+
{
|
|
55507
|
+
key: "react-doctor/no-stale-timer-ref",
|
|
55508
|
+
id: "no-stale-timer-ref",
|
|
55509
|
+
source: "react-doctor",
|
|
55510
|
+
originallyExternal: false,
|
|
55511
|
+
rule: {
|
|
55512
|
+
...noStaleTimerRef,
|
|
55513
|
+
framework: "global",
|
|
55514
|
+
category: "Bugs",
|
|
55515
|
+
requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
|
|
55516
|
+
}
|
|
55517
|
+
},
|
|
53682
55518
|
{
|
|
53683
55519
|
key: "react-doctor/no-static-element-interactions",
|
|
53684
55520
|
id: "no-static-element-interactions",
|
|
@@ -56127,6 +57963,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
56127
57963
|
"nextjs-no-use-search-params-without-suspense",
|
|
56128
57964
|
"no-dynamic-import-path",
|
|
56129
57965
|
"no-full-lodash-import",
|
|
57966
|
+
"no-locale-format-in-render",
|
|
56130
57967
|
"no-mutating-reducer-state",
|
|
56131
57968
|
"prefer-dynamic-import",
|
|
56132
57969
|
"rendering-hydration-mismatch-time",
|
|
@@ -56261,6 +58098,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
56261
58098
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
56262
58099
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
56263
58100
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
58101
|
+
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
56264
58102
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
56265
58103
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
56266
58104
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|