oxlint-plugin-react-doctor 0.7.9-dev.21043e0 → 0.7.9-dev.2450582
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.js +1057 -390
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -836,22 +836,131 @@ const getImportedName = (importSpecifier) => {
|
|
|
836
836
|
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
837
837
|
};
|
|
838
838
|
//#endregion
|
|
839
|
-
//#region src/plugin/utils/get-
|
|
840
|
-
const
|
|
841
|
-
if (!isNodeOfType(node, "
|
|
842
|
-
|
|
843
|
-
if (
|
|
839
|
+
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
840
|
+
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
841
|
+
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
842
|
+
const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
|
|
843
|
+
if (node.computed) {
|
|
844
|
+
if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
|
|
845
|
+
if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
if (isNodeOfType(key, "Identifier")) return key.name;
|
|
849
|
+
if (isNodeOfType(key, "Literal")) {
|
|
850
|
+
if (typeof key.value === "string") return key.value;
|
|
851
|
+
if (options.stringifyNonStringLiterals) return String(key.value);
|
|
852
|
+
}
|
|
844
853
|
return null;
|
|
845
854
|
};
|
|
846
855
|
//#endregion
|
|
847
|
-
//#region src/plugin/utils/
|
|
848
|
-
const
|
|
856
|
+
//#region src/plugin/utils/get-static-property-name.ts
|
|
857
|
+
const getStaticPropertyName = (memberExpression) => {
|
|
858
|
+
const property = memberExpression.property;
|
|
859
|
+
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
860
|
+
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
861
|
+
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
862
|
+
return null;
|
|
863
|
+
};
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
866
|
+
const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
|
|
867
|
+
"ParenthesizedExpression",
|
|
868
|
+
"TSAsExpression",
|
|
869
|
+
"TSSatisfiesExpression",
|
|
870
|
+
"TSTypeAssertion",
|
|
871
|
+
"TSNonNullExpression",
|
|
872
|
+
"TSInstantiationExpression",
|
|
873
|
+
"ChainExpression"
|
|
874
|
+
]);
|
|
875
|
+
const stripParenExpression = (node) => {
|
|
876
|
+
let current = node;
|
|
877
|
+
while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
|
|
878
|
+
return current;
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
882
|
+
const resolveConstIdentifierAlias = (identifier, scopes, allowPatternBinding = false) => {
|
|
883
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
884
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
885
|
+
let symbol = scopes.symbolFor(identifier);
|
|
886
|
+
while (symbol?.kind === "const") {
|
|
887
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return null;
|
|
888
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
|
|
889
|
+
visitedSymbolIds.add(symbol.id);
|
|
890
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
891
|
+
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
892
|
+
symbol = scopes.symbolFor(initializer);
|
|
893
|
+
}
|
|
894
|
+
return symbol;
|
|
895
|
+
};
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/plugin/utils/is-react-api-call.ts
|
|
898
|
+
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
899
|
+
const isImportedFromReact = (symbol) => {
|
|
900
|
+
if (symbol.kind !== "import") return false;
|
|
901
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
902
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
903
|
+
};
|
|
904
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
905
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
906
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
907
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
908
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
909
|
+
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
910
|
+
};
|
|
911
|
+
const isReactNamespaceImport = (identifier, scopes) => {
|
|
912
|
+
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
913
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
914
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
915
|
+
};
|
|
916
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
917
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
918
|
+
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
919
|
+
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
920
|
+
};
|
|
921
|
+
const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
|
|
922
|
+
const symbol = scopes.symbolFor(identifier);
|
|
923
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
924
|
+
const pattern = symbol.declarationNode.id;
|
|
925
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
926
|
+
for (const property of pattern.properties) {
|
|
927
|
+
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
928
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
929
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
};
|
|
933
|
+
const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
849
934
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
935
|
+
return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
|
|
936
|
+
};
|
|
937
|
+
const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
|
|
938
|
+
const callee = stripParenExpression(rawCallee);
|
|
939
|
+
if (options.resolveConditionalAliases && isNodeOfType(callee, "ConditionalExpression")) return isReactApiCallee(callee.consequent, apiNames, scopes, options, new Set(visitedSymbolIds)) && isReactApiCallee(callee.alternate, apiNames, scopes, options, new Set(visitedSymbolIds));
|
|
940
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
941
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
942
|
+
if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
|
|
943
|
+
if (options.resolveConditionalAliases) {
|
|
944
|
+
const symbol = scopes.symbolFor(callee);
|
|
945
|
+
if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
|
|
946
|
+
visitedSymbolIds.add(symbol.id);
|
|
947
|
+
return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
951
|
+
}
|
|
952
|
+
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
953
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
853
954
|
};
|
|
854
955
|
//#endregion
|
|
956
|
+
//#region src/plugin/utils/is-react-hook-call.ts
|
|
957
|
+
const isReactHookCall = (node, hookNames, scopes) => isReactApiCall(node, hookNames, scopes, {
|
|
958
|
+
allowGlobalReactNamespace: true,
|
|
959
|
+
allowUnboundBareCalls: true,
|
|
960
|
+
resolveConditionalAliases: true,
|
|
961
|
+
resolveNamedAliases: true
|
|
962
|
+
});
|
|
963
|
+
//#endregion
|
|
855
964
|
//#region src/plugin/utils/is-ast-node.ts
|
|
856
965
|
const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
857
966
|
//#endregion
|
|
@@ -921,12 +1030,12 @@ const collectChildComponentNames = (element, into) => {
|
|
|
921
1030
|
into.add(name);
|
|
922
1031
|
});
|
|
923
1032
|
};
|
|
924
|
-
const countEffectHookCalls = (body) => {
|
|
1033
|
+
const countEffectHookCalls = (body, scopes) => {
|
|
925
1034
|
if (!body) return 0;
|
|
926
1035
|
let count = 0;
|
|
927
1036
|
walkAst(body, (child) => {
|
|
928
1037
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
929
|
-
if (
|
|
1038
|
+
if (isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) count++;
|
|
930
1039
|
});
|
|
931
1040
|
return count;
|
|
932
1041
|
};
|
|
@@ -952,11 +1061,11 @@ const getComponentEffectIndex = (programRoot) => {
|
|
|
952
1061
|
componentEffectIndexCache.set(programRoot, index);
|
|
953
1062
|
return index;
|
|
954
1063
|
};
|
|
955
|
-
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
1064
|
+
const getSameFileComponentEffectCount = (programRoot, componentName, scopes) => {
|
|
956
1065
|
const index = getComponentEffectIndex(programRoot);
|
|
957
1066
|
const cachedCount = index.effectCountByName.get(componentName);
|
|
958
1067
|
if (cachedCount !== void 0) return cachedCount;
|
|
959
|
-
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
1068
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null, scopes);
|
|
960
1069
|
index.effectCountByName.set(componentName, count);
|
|
961
1070
|
return count;
|
|
962
1071
|
};
|
|
@@ -1016,7 +1125,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
1016
1125
|
let totalEffects = 0;
|
|
1017
1126
|
const effectfulChildren = [];
|
|
1018
1127
|
for (const componentName of childComponentNames) {
|
|
1019
|
-
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
1128
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName, context.scopes);
|
|
1020
1129
|
if (effectCount === 0) continue;
|
|
1021
1130
|
totalEffects += effectCount;
|
|
1022
1131
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1207,6 +1316,14 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
1207
1316
|
return best;
|
|
1208
1317
|
};
|
|
1209
1318
|
//#endregion
|
|
1319
|
+
//#region src/plugin/utils/get-callee-name.ts
|
|
1320
|
+
const getCalleeName$1 = (node) => {
|
|
1321
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
|
|
1322
|
+
if (isNodeOfType(node.callee, "Identifier")) return node.callee.name;
|
|
1323
|
+
if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier")) return node.callee.property.name;
|
|
1324
|
+
return null;
|
|
1325
|
+
};
|
|
1326
|
+
//#endregion
|
|
1210
1327
|
//#region src/plugin/utils/is-function-like.ts
|
|
1211
1328
|
/**
|
|
1212
1329
|
* Type-guard for the three "function-like" ESTree node shapes:
|
|
@@ -1220,37 +1337,6 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
1220
1337
|
*/
|
|
1221
1338
|
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
1222
1339
|
//#endregion
|
|
1223
|
-
//#region src/plugin/utils/strip-paren-expression.ts
|
|
1224
|
-
const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
|
|
1225
|
-
"ParenthesizedExpression",
|
|
1226
|
-
"TSAsExpression",
|
|
1227
|
-
"TSSatisfiesExpression",
|
|
1228
|
-
"TSTypeAssertion",
|
|
1229
|
-
"TSNonNullExpression",
|
|
1230
|
-
"TSInstantiationExpression",
|
|
1231
|
-
"ChainExpression"
|
|
1232
|
-
]);
|
|
1233
|
-
const stripParenExpression = (node) => {
|
|
1234
|
-
let current = node;
|
|
1235
|
-
while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
|
|
1236
|
-
return current;
|
|
1237
|
-
};
|
|
1238
|
-
//#endregion
|
|
1239
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
1240
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
1241
|
-
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
1242
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
1243
|
-
let symbol = scopes.symbolFor(identifier);
|
|
1244
|
-
while (symbol?.kind === "const") {
|
|
1245
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
1246
|
-
visitedSymbolIds.add(symbol.id);
|
|
1247
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
1248
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
1249
|
-
symbol = scopes.symbolFor(initializer);
|
|
1250
|
-
}
|
|
1251
|
-
return symbol;
|
|
1252
|
-
};
|
|
1253
|
-
//#endregion
|
|
1254
1340
|
//#region src/plugin/utils/resolve-exact-local-function.ts
|
|
1255
1341
|
const resolveExactLocalFunction = (expression, scopes) => {
|
|
1256
1342
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -1296,9 +1382,17 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1296
1382
|
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1297
1383
|
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1298
1384
|
//#endregion
|
|
1385
|
+
//#region src/plugin/utils/is-hook-call.ts
|
|
1386
|
+
const isHookCall$2 = (node, hookName) => {
|
|
1387
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
1388
|
+
const calleeName = getCalleeName$1(node);
|
|
1389
|
+
if (!calleeName) return false;
|
|
1390
|
+
return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
|
|
1391
|
+
};
|
|
1392
|
+
//#endregion
|
|
1299
1393
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1394
|
+
const REACT_STABLE_HANDLER_HOOK_NAMES = new Set(["useCallback", "useEffectEvent"]);
|
|
1395
|
+
const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1302
1396
|
"useEffectEvent",
|
|
1303
1397
|
"useEvent",
|
|
1304
1398
|
"useEventCallback",
|
|
@@ -1307,21 +1401,21 @@ const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
|
1307
1401
|
]);
|
|
1308
1402
|
const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
|
|
1309
1403
|
const isThrottledHandlerHookCall = (callNode) => {
|
|
1310
|
-
const calleeName = getCalleeName$
|
|
1404
|
+
const calleeName = getCalleeName$1(callNode);
|
|
1311
1405
|
return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
|
|
1312
1406
|
};
|
|
1313
|
-
const isEmptyDepsUseMemoCall = (callNode) => {
|
|
1314
|
-
if (!
|
|
1407
|
+
const isEmptyDepsUseMemoCall = (callNode, scopes) => {
|
|
1408
|
+
if (!isReactHookCall(callNode, "useMemo", scopes)) return false;
|
|
1315
1409
|
const memoDepsNode = callNode.arguments?.[1];
|
|
1316
1410
|
return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
|
|
1317
1411
|
};
|
|
1318
|
-
const isStableHandlerInitializer = (initializer) => {
|
|
1319
|
-
if (isNodeOfType(initializer, "CallExpression")) return isHookCall$2(initializer,
|
|
1412
|
+
const isStableHandlerInitializer = (initializer, scopes) => {
|
|
1413
|
+
if (isNodeOfType(initializer, "CallExpression")) return isReactHookCall(initializer, REACT_STABLE_HANDLER_HOOK_NAMES, scopes) || isHookCall$2(initializer, CUSTOM_STABLE_HANDLER_HOOK_NAMES) || isEmptyDepsUseMemoCall(initializer, scopes) || isThrottledHandlerHookCall(initializer);
|
|
1320
1414
|
return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
|
|
1321
1415
|
};
|
|
1322
|
-
const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
|
|
1416
|
+
const isStableRefReceiverDep = (referenceNode, receiverDepName, scopes) => {
|
|
1323
1417
|
const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
|
|
1324
|
-
return Boolean(receiverBinding?.initializer &&
|
|
1418
|
+
return Boolean(receiverBinding?.initializer && isReactHookCall(receiverBinding.initializer, "useRef", scopes));
|
|
1325
1419
|
};
|
|
1326
1420
|
const advancedEventHandlerRefs = defineRule({
|
|
1327
1421
|
id: "advanced-event-handler-refs",
|
|
@@ -1331,7 +1425,7 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1331
1425
|
category: "Performance",
|
|
1332
1426
|
recommendation: "Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",
|
|
1333
1427
|
create: (context) => ({ CallExpression(node) {
|
|
1334
|
-
if (!
|
|
1428
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
1335
1429
|
if ((node.arguments?.length ?? 0) < 2) return;
|
|
1336
1430
|
const callback = getEffectCallback(node);
|
|
1337
1431
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -1355,8 +1449,8 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1355
1449
|
});
|
|
1356
1450
|
if (!registeredHandlerName) return;
|
|
1357
1451
|
const handlerBinding = findVariableInitializer(node, registeredHandlerName);
|
|
1358
|
-
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
|
|
1359
|
-
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName))) return;
|
|
1452
|
+
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer, context.scopes)) return;
|
|
1453
|
+
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName, context.scopes))) return;
|
|
1360
1454
|
context.report({
|
|
1361
1455
|
node,
|
|
1362
1456
|
message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
|
|
@@ -1843,15 +1937,6 @@ const flattenJsxName$1 = (node) => {
|
|
|
1843
1937
|
return null;
|
|
1844
1938
|
};
|
|
1845
1939
|
//#endregion
|
|
1846
|
-
//#region src/plugin/utils/get-static-property-name.ts
|
|
1847
|
-
const getStaticPropertyName = (memberExpression) => {
|
|
1848
|
-
const property = memberExpression.property;
|
|
1849
|
-
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
1850
|
-
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
1851
|
-
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
1852
|
-
return null;
|
|
1853
|
-
};
|
|
1854
|
-
//#endregion
|
|
1855
1940
|
//#region src/plugin/utils/is-generated-image-renderer-call.ts
|
|
1856
1941
|
const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
1857
1942
|
"next/og",
|
|
@@ -4859,23 +4944,6 @@ const containsDirectAwait = (node) => {
|
|
|
4859
4944
|
return foundAwait;
|
|
4860
4945
|
};
|
|
4861
4946
|
//#endregion
|
|
4862
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
4863
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
4864
|
-
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
4865
|
-
const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
|
|
4866
|
-
if (node.computed) {
|
|
4867
|
-
if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
|
|
4868
|
-
if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
|
|
4869
|
-
return null;
|
|
4870
|
-
}
|
|
4871
|
-
if (isNodeOfType(key, "Identifier")) return key.name;
|
|
4872
|
-
if (isNodeOfType(key, "Literal")) {
|
|
4873
|
-
if (typeof key.value === "string") return key.value;
|
|
4874
|
-
if (options.stringifyNonStringLiterals) return String(key.value);
|
|
4875
|
-
}
|
|
4876
|
-
return null;
|
|
4877
|
-
};
|
|
4878
|
-
//#endregion
|
|
4879
4947
|
//#region src/plugin/utils/get-destructured-binding-property-name.ts
|
|
4880
4948
|
const getDestructuredBindingPropertyName = (bindingIdentifier) => {
|
|
4881
4949
|
let bindingNode = bindingIdentifier;
|
|
@@ -8940,65 +9008,6 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
|
|
|
8940
9008
|
}
|
|
8941
9009
|
};
|
|
8942
9010
|
//#endregion
|
|
8943
|
-
//#region src/plugin/utils/is-react-api-call.ts
|
|
8944
|
-
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
8945
|
-
const isImportedFromReact = (symbol) => {
|
|
8946
|
-
if (symbol.kind !== "import") return false;
|
|
8947
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
8948
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
8949
|
-
};
|
|
8950
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
8951
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
8952
|
-
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
8953
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8954
|
-
const importedName = getImportedName(symbol.declarationNode);
|
|
8955
|
-
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
8956
|
-
};
|
|
8957
|
-
const isReactNamespaceImport = (identifier, scopes) => {
|
|
8958
|
-
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
8959
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8960
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8961
|
-
};
|
|
8962
|
-
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8963
|
-
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8964
|
-
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8965
|
-
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
8966
|
-
};
|
|
8967
|
-
const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
|
|
8968
|
-
const symbol = scopes.symbolFor(identifier);
|
|
8969
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
8970
|
-
const pattern = symbol.declarationNode.id;
|
|
8971
|
-
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
8972
|
-
for (const property of pattern.properties) {
|
|
8973
|
-
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8974
|
-
const propertyName = getStaticPropertyKeyName(property);
|
|
8975
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8976
|
-
}
|
|
8977
|
-
return false;
|
|
8978
|
-
};
|
|
8979
|
-
const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
8980
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
8981
|
-
return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
|
|
8982
|
-
};
|
|
8983
|
-
const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
|
|
8984
|
-
const callee = stripParenExpression(rawCallee);
|
|
8985
|
-
if (options.resolveConditionalAliases && isNodeOfType(callee, "ConditionalExpression")) return isReactApiCallee(callee.consequent, apiNames, scopes, options, new Set(visitedSymbolIds)) && isReactApiCallee(callee.alternate, apiNames, scopes, options, new Set(visitedSymbolIds));
|
|
8986
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8987
|
-
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
8988
|
-
if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
|
|
8989
|
-
if (options.resolveConditionalAliases) {
|
|
8990
|
-
const symbol = scopes.symbolFor(callee);
|
|
8991
|
-
if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
|
|
8992
|
-
visitedSymbolIds.add(symbol.id);
|
|
8993
|
-
return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
|
|
8994
|
-
}
|
|
8995
|
-
}
|
|
8996
|
-
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8997
|
-
}
|
|
8998
|
-
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
8999
|
-
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
9000
|
-
};
|
|
9001
|
-
//#endregion
|
|
9002
9011
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
9003
9012
|
const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
|
|
9004
9013
|
"AbortSignal",
|
|
@@ -13652,7 +13661,7 @@ const getPromiseChainCallForCallback = (candidate) => {
|
|
|
13652
13661
|
if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
|
|
13653
13662
|
return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
|
|
13654
13663
|
};
|
|
13655
|
-
const
|
|
13664
|
+
const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
|
|
13656
13665
|
const invokedFunctions = new Set([effectCallback]);
|
|
13657
13666
|
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
13658
13667
|
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
@@ -13686,12 +13695,14 @@ const collectEffectInvokedFunctions = (effectCallback) => {
|
|
|
13686
13695
|
calledBindingNames.add(callee.name);
|
|
13687
13696
|
return;
|
|
13688
13697
|
}
|
|
13689
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13698
|
+
if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13690
13699
|
});
|
|
13691
13700
|
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
13692
13701
|
}
|
|
13693
13702
|
return invokedFunctions;
|
|
13694
13703
|
};
|
|
13704
|
+
const collectEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, true);
|
|
13705
|
+
const collectSynchronouslyEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, false);
|
|
13695
13706
|
//#endregion
|
|
13696
13707
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
13697
13708
|
const isReactHookName = (name) => {
|
|
@@ -13850,15 +13861,19 @@ const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
|
13850
13861
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
13851
13862
|
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
13852
13863
|
};
|
|
13853
|
-
const
|
|
13864
|
+
const resolveReactRefCurrentOriginSymbol = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13854
13865
|
const expression = stripParenExpression(node);
|
|
13855
|
-
|
|
13856
|
-
if (
|
|
13857
|
-
|
|
13858
|
-
|
|
13866
|
+
const refSymbol = resolveReactRefSymbol(expression, scopes);
|
|
13867
|
+
if (refSymbol) return refSymbol;
|
|
13868
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
13869
|
+
const symbol = scopes.symbolFor(expression);
|
|
13870
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
13871
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
13872
|
+
if (!initializer) return null;
|
|
13859
13873
|
visitedSymbolIds.add(symbol.id);
|
|
13860
|
-
return
|
|
13874
|
+
return resolveReactRefCurrentOriginSymbol(initializer, scopes, visitedSymbolIds);
|
|
13861
13875
|
};
|
|
13876
|
+
const hasReactRefCurrentOrigin = (node, scopes) => resolveReactRefCurrentOriginSymbol(node, scopes) !== null;
|
|
13862
13877
|
//#endregion
|
|
13863
13878
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
13864
13879
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -13876,6 +13891,7 @@ const walkInsideStatementBlocks = (node, visitor) => {
|
|
|
13876
13891
|
};
|
|
13877
13892
|
//#endregion
|
|
13878
13893
|
//#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
|
|
13894
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13879
13895
|
const getSubscribeLikeMethodName = (node) => {
|
|
13880
13896
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
13881
13897
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
@@ -13886,6 +13902,11 @@ const isSubscribeLikeCallExpression = (node) => {
|
|
|
13886
13902
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13887
13903
|
return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
|
|
13888
13904
|
};
|
|
13905
|
+
const getSubscribeOrObserveMethodName = (node) => {
|
|
13906
|
+
const methodName = getSubscribeLikeMethodName(node);
|
|
13907
|
+
return methodName !== null && (SUBSCRIPTION_METHOD_NAMES.has(methodName) || methodName === "observe") ? methodName : null;
|
|
13908
|
+
};
|
|
13909
|
+
const isSubscribeOrObserveCallExpression = (node) => getSubscribeOrObserveMethodName(node) !== null;
|
|
13889
13910
|
const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
13890
13911
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13891
13912
|
if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
|
|
@@ -13938,7 +13959,6 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13938
13959
|
};
|
|
13939
13960
|
//#endregion
|
|
13940
13961
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13941
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13942
13962
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13943
13963
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13944
13964
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -13948,10 +13968,6 @@ const RESOURCE_NOUN_BY_KIND = {
|
|
|
13948
13968
|
socket: "connection"
|
|
13949
13969
|
};
|
|
13950
13970
|
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
13951
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
13952
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
13953
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
13954
|
-
};
|
|
13955
13971
|
const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13956
13972
|
if (!expression) return null;
|
|
13957
13973
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -14053,12 +14069,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
14053
14069
|
});
|
|
14054
14070
|
return;
|
|
14055
14071
|
}
|
|
14056
|
-
|
|
14072
|
+
const subscribeOrObserveMethodName = getSubscribeOrObserveMethodName(child);
|
|
14073
|
+
if (subscribeOrObserveMethodName !== null) {
|
|
14057
14074
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
14058
14075
|
usages.push({
|
|
14059
14076
|
kind: "subscribe",
|
|
14060
14077
|
node: child,
|
|
14061
|
-
resourceName:
|
|
14078
|
+
resourceName: subscribeOrObserveMethodName,
|
|
14062
14079
|
handleKey: findAssignedResourceKey(child, context),
|
|
14063
14080
|
...registrationDetails
|
|
14064
14081
|
});
|
|
@@ -14340,6 +14357,39 @@ const findContainingCollectionKey = (resourceNode, context) => {
|
|
|
14340
14357
|
}
|
|
14341
14358
|
return null;
|
|
14342
14359
|
};
|
|
14360
|
+
const findPushedResourceCollectionKey = (usage, context) => {
|
|
14361
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return null;
|
|
14362
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
14363
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed) return null;
|
|
14364
|
+
const resourceIdentifier = stripParenExpression(registrationCallee.object);
|
|
14365
|
+
if (!isPrivatePlainConstIdentifier(resourceIdentifier, context)) return null;
|
|
14366
|
+
const resourceSymbol = context.scopes.symbolFor(resourceIdentifier);
|
|
14367
|
+
if (!resourceSymbol) return null;
|
|
14368
|
+
const pushCalls = resourceSymbol.references.flatMap((reference) => {
|
|
14369
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14370
|
+
const callNode = referenceRoot.parent;
|
|
14371
|
+
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((argument) => argument === referenceRoot)) return [];
|
|
14372
|
+
const pushCallee = stripParenExpression(callNode.callee);
|
|
14373
|
+
return isNodeOfType(pushCallee, "MemberExpression") && !pushCallee.computed && isNodeOfType(pushCallee.object, "Identifier") && isNodeOfType(pushCallee.property, "Identifier") && pushCallee.property.name === "push" ? [callNode] : [];
|
|
14374
|
+
});
|
|
14375
|
+
if (pushCalls.length !== 1) return null;
|
|
14376
|
+
const pushCall = pushCalls[0];
|
|
14377
|
+
if (findEnclosingFunction$1(pushCall) !== findEnclosingFunction$1(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [pushCall], context)) return null;
|
|
14378
|
+
const pushCallee = stripParenExpression(pushCall.callee);
|
|
14379
|
+
if (!isNodeOfType(pushCallee, "MemberExpression") || !isNodeOfType(pushCallee.object, "Identifier") || !isPrivatePlainConstIdentifier(pushCallee.object, context)) return null;
|
|
14380
|
+
const collectionSymbol = context.scopes.symbolFor(pushCallee.object);
|
|
14381
|
+
const collectionInitializer = collectionSymbol?.initializer ? stripParenExpression(collectionSymbol.initializer) : null;
|
|
14382
|
+
if (!collectionSymbol || !isNodeOfType(collectionInitializer, "ArrayExpression") || (collectionInitializer.elements?.length ?? 0) !== 0 || findEnclosingFunction$1(collectionSymbol.declarationNode) !== findEnclosingFunction$1(usage.node)) return null;
|
|
14383
|
+
return collectionSymbol.references.every((reference) => {
|
|
14384
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14385
|
+
const forOfStatement = referenceRoot.parent;
|
|
14386
|
+
if (isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.right === referenceRoot && forOfStatement.await !== true) return true;
|
|
14387
|
+
const memberNode = referenceRoot.parent;
|
|
14388
|
+
const callNode = memberNode?.parent;
|
|
14389
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== referenceRoot || memberNode.computed || !isNodeOfType(memberNode.property, "Identifier") || !isNodeOfType(callNode, "CallExpression") || callNode.callee !== memberNode) return false;
|
|
14390
|
+
return memberNode.property.name === "forEach" || memberNode.property.name === "push";
|
|
14391
|
+
}) ? resolveExpressionKey(pushCallee.object, context) : null;
|
|
14392
|
+
};
|
|
14343
14393
|
const isWithinAssignmentTarget = (identifier) => {
|
|
14344
14394
|
let currentNode = identifier;
|
|
14345
14395
|
let parentNode = currentNode.parent;
|
|
@@ -14398,6 +14448,14 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
14398
14448
|
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
14399
14449
|
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
14400
14450
|
};
|
|
14451
|
+
const findEnclosingForEachCall = (node) => {
|
|
14452
|
+
const callbackNode = findEnclosingFunction$1(node);
|
|
14453
|
+
if (!callbackNode) return null;
|
|
14454
|
+
const callNode = callbackNode.parent;
|
|
14455
|
+
if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
|
|
14456
|
+
const callee = stripParenExpression(callNode.callee);
|
|
14457
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" ? callNode : null;
|
|
14458
|
+
};
|
|
14401
14459
|
const findDirectCallForReference = (identifier) => {
|
|
14402
14460
|
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
14403
14461
|
const callNode = expressionRoot.parent;
|
|
@@ -14412,7 +14470,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
|
14412
14470
|
const callNode = findDirectCallForReference(reference.identifier);
|
|
14413
14471
|
return callNode ? [callNode] : [];
|
|
14414
14472
|
});
|
|
14415
|
-
if (invocationCalls.length !== 1) return null;
|
|
14473
|
+
if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
|
|
14416
14474
|
const invocationCall = invocationCalls[0];
|
|
14417
14475
|
return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
14418
14476
|
};
|
|
@@ -14446,7 +14504,15 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
14446
14504
|
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
14447
14505
|
const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
14448
14506
|
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
14449
|
-
const
|
|
14507
|
+
const cleanupForEachCall = findEnclosingForEachCall(cleanupChild);
|
|
14508
|
+
const cleanupCallee = isNodeOfType(cleanupCall, "CallExpression") ? stripParenExpression(cleanupCall.callee) : null;
|
|
14509
|
+
const cleanupReceiverForOfStatement = isNodeOfType(cleanupCallee, "MemberExpression") ? findForOfStatementForIteratorExpression(cleanupCallee.object, context) : null;
|
|
14510
|
+
const cleanupReceiverCollectionKey = cleanupReceiverForOfStatement ? resolveExpressionKey(cleanupReceiverForOfStatement.right, context) : isNodeOfType(cleanupCallee, "MemberExpression") ? resolveIteratorCollectionKey(cleanupCallee.object, context) : null;
|
|
14511
|
+
if (cleanupReceiverCollectionKey !== null && findEnclosingFunction$1(cleanupChild) !== cleanupFunction) {
|
|
14512
|
+
if (cleanupForEachCall && findPushedResourceCollectionKey(usage, context) === cleanupReceiverCollectionKey) matchingLoopOrHelperAnchors.push(cleanupForEachCall);
|
|
14513
|
+
return;
|
|
14514
|
+
}
|
|
14515
|
+
const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context) ?? cleanupReceiverForOfStatement;
|
|
14450
14516
|
if (!cleanupForOfStatement) {
|
|
14451
14517
|
didCleanupFunctionMatch = true;
|
|
14452
14518
|
return false;
|
|
@@ -14490,28 +14556,28 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
|
14490
14556
|
});
|
|
14491
14557
|
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
14492
14558
|
};
|
|
14493
|
-
const
|
|
14494
|
-
if (
|
|
14495
|
-
const
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
const unwrappedOperand = stripParenExpression(operand);
|
|
14501
|
-
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14502
|
-
};
|
|
14503
|
-
return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
|
|
14559
|
+
const doesTestRequireLiveExpressionKey = (test, expressionKey, context) => {
|
|
14560
|
+
if (resolveExpressionKey(test, context) === expressionKey) return true;
|
|
14561
|
+
const unwrappedTest = stripParenExpression(test);
|
|
14562
|
+
if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
|
|
14563
|
+
const isNullishOperand = (operand) => {
|
|
14564
|
+
const unwrappedOperand = stripParenExpression(operand);
|
|
14565
|
+
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14504
14566
|
};
|
|
14567
|
+
return resolveExpressionKey(unwrappedTest.left, context) === expressionKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === expressionKey && isNullishOperand(unwrappedTest.left);
|
|
14568
|
+
};
|
|
14569
|
+
const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, context) => {
|
|
14505
14570
|
let ancestor = releaseCall.parent;
|
|
14506
14571
|
while (ancestor && ancestor !== owner) {
|
|
14507
14572
|
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
14508
|
-
if (ancestor.alternate !== null || !
|
|
14573
|
+
if (ancestor.alternate !== null || !doesTestRequireLiveExpressionKey(ancestor.test, expressionKey, context) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
|
|
14509
14574
|
return ancestor;
|
|
14510
14575
|
}
|
|
14511
14576
|
ancestor = ancestor.parent;
|
|
14512
14577
|
}
|
|
14513
14578
|
return null;
|
|
14514
14579
|
};
|
|
14580
|
+
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
|
|
14515
14581
|
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
14516
14582
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
14517
14583
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
@@ -14540,7 +14606,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
14540
14606
|
walkAst(componentFunction.body, (child) => {
|
|
14541
14607
|
if (didFindUnmountCleanup) return false;
|
|
14542
14608
|
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction) return;
|
|
14543
|
-
if (!
|
|
14609
|
+
if (!isReactHookCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
14544
14610
|
const dependencyList = child.arguments?.[1];
|
|
14545
14611
|
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
14546
14612
|
const cleanupCallback = getEffectCallback(child);
|
|
@@ -14699,7 +14765,108 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
|
|
|
14699
14765
|
});
|
|
14700
14766
|
return hasPotentialInterruption;
|
|
14701
14767
|
};
|
|
14768
|
+
const getNumericReactRefCurrentKey = (expression, context) => {
|
|
14769
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(expression), context.scopes);
|
|
14770
|
+
const initializer = refSymbol?.initializer ? stripParenExpression(refSymbol.initializer) : null;
|
|
14771
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
14772
|
+
const initialValue = initializer.arguments?.[0] ? stripParenExpression(initializer.arguments[0]) : null;
|
|
14773
|
+
if (!isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return null;
|
|
14774
|
+
return resolveExpressionKey(expression, context);
|
|
14775
|
+
};
|
|
14776
|
+
const getBlockingGenerationKey = (expression, context) => {
|
|
14777
|
+
const test = stripParenExpression(expression);
|
|
14778
|
+
if (isNodeOfType(test, "LogicalExpression") && test.operator === "||") return getBlockingGenerationKey(test.left, context) ?? getBlockingGenerationKey(test.right, context);
|
|
14779
|
+
if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "!==" && test.operator !== "!=") return null;
|
|
14780
|
+
const leftKey = getNumericReactRefCurrentKey(test.left, context);
|
|
14781
|
+
const rightKey = getNumericReactRefCurrentKey(test.right, context);
|
|
14782
|
+
const snapshotExpression = leftKey ? stripParenExpression(test.right) : stripParenExpression(test.left);
|
|
14783
|
+
const key = leftKey ?? rightKey;
|
|
14784
|
+
return key && isNodeOfType(snapshotExpression, "Identifier") ? key : null;
|
|
14785
|
+
};
|
|
14786
|
+
const findGenerationGuardKeyForDeferredUsage = (usageFunction, usageNode, context) => {
|
|
14787
|
+
if (!isFunctionLike$1(usageFunction)) return null;
|
|
14788
|
+
let generationKey = null;
|
|
14789
|
+
walkAst(usageFunction.body, (child) => {
|
|
14790
|
+
if (generationKey) return false;
|
|
14791
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
14792
|
+
if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
|
|
14793
|
+
const key = getBlockingGenerationKey(child.test, context);
|
|
14794
|
+
if (!key || canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, usageFunction, context) || !doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], usageFunction, context)) return;
|
|
14795
|
+
generationKey = key;
|
|
14796
|
+
});
|
|
14797
|
+
return generationKey;
|
|
14798
|
+
};
|
|
14799
|
+
const isGenerationAdvance = (node, generationKey, context) => {
|
|
14800
|
+
if (isNodeOfType(node, "UpdateExpression") && resolveExpressionKey(node.argument, context) === generationKey) return true;
|
|
14801
|
+
if (!isNodeOfType(node, "AssignmentExpression") || resolveExpressionKey(node.left, context) !== generationKey || node.operator !== "+=" && node.operator !== "-=") return false;
|
|
14802
|
+
const amount = stripParenExpression(node.right);
|
|
14803
|
+
return isNodeOfType(amount, "Literal") && typeof amount.value === "number" && amount.value !== 0;
|
|
14804
|
+
};
|
|
14805
|
+
const functionAdvancesGeneration = (owner, generationKey, context) => {
|
|
14806
|
+
if (!isFunctionLike$1(owner)) return false;
|
|
14807
|
+
let didAdvanceGeneration = false;
|
|
14808
|
+
walkAst(owner.body, (child) => {
|
|
14809
|
+
if (didAdvanceGeneration) return false;
|
|
14810
|
+
if (child !== owner.body && isFunctionLike$1(child)) return false;
|
|
14811
|
+
if (isGenerationAdvance(child, generationKey, context)) {
|
|
14812
|
+
didAdvanceGeneration = true;
|
|
14813
|
+
return false;
|
|
14814
|
+
}
|
|
14815
|
+
});
|
|
14816
|
+
return didAdvanceGeneration;
|
|
14817
|
+
};
|
|
14818
|
+
const cleanupReturnsReleaseUsage = (cleanupReturns, usage, context) => cleanupReturns.length > 0 && cleanupReturns.every((cleanupReturn) => {
|
|
14819
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
|
|
14820
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14821
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
14822
|
+
});
|
|
14823
|
+
const getOwnedFunctionReference = (reference, usageFunction, usageNode, callback, cleanupReturns, context) => {
|
|
14824
|
+
const directCall = findDirectCallForReference(reference);
|
|
14825
|
+
if (directCall) {
|
|
14826
|
+
const referenceOwner = findEnclosingFunction$1(directCall);
|
|
14827
|
+
if (referenceOwner && referenceOwner !== usageFunction && collectSynchronouslyEffectInvokedFunctions(callback).has(referenceOwner)) return { generationKey: null };
|
|
14828
|
+
const generationKey = referenceOwner ? findGenerationGuardKeyForDeferredUsage(referenceOwner, directCall, context) : null;
|
|
14829
|
+
return generationKey ? { generationKey } : null;
|
|
14830
|
+
}
|
|
14831
|
+
const referenceRoot = findTransparentExpressionRoot(reference);
|
|
14832
|
+
const schedulerCall = referenceRoot.parent;
|
|
14833
|
+
if (!isNodeOfType(schedulerCall, "CallExpression") || !schedulerCall.arguments.some((argument) => argument === referenceRoot) || !isNodeOfType(schedulerCall.callee, "Identifier") || schedulerCall.callee.name !== "setTimeout" || !context.scopes.isGlobalReference(schedulerCall.callee)) return null;
|
|
14834
|
+
const schedulerUsage = {
|
|
14835
|
+
kind: "timer",
|
|
14836
|
+
node: schedulerCall,
|
|
14837
|
+
resourceName: schedulerCall.callee.name,
|
|
14838
|
+
handleKey: findAssignedResourceKey(schedulerCall, context),
|
|
14839
|
+
receiverKey: null,
|
|
14840
|
+
registrationVerbName: schedulerCall.callee.name,
|
|
14841
|
+
eventKey: null,
|
|
14842
|
+
handlerKey: null
|
|
14843
|
+
};
|
|
14844
|
+
const generationKey = findGenerationGuardKeyForDeferredUsage(usageFunction, usageNode, context);
|
|
14845
|
+
return schedulerUsage.handleKey !== null && cleanupReturnsReleaseUsage(cleanupReturns, schedulerUsage, context) && generationKey ? { generationKey } : null;
|
|
14846
|
+
};
|
|
14847
|
+
const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14848
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14849
|
+
const usageExpression = findTransparentExpressionRoot(usage.node);
|
|
14850
|
+
const usageAssignment = usageExpression.parent;
|
|
14851
|
+
if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !doMatchingNodesCoverEveryPathFromFunctionEntry(callback, cleanupReturns, context)) return false;
|
|
14852
|
+
const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
|
|
14853
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
|
|
14854
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14855
|
+
return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
|
|
14856
|
+
});
|
|
14857
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(usageFunction);
|
|
14858
|
+
const functionSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
14859
|
+
if (!functionSymbol || functionSymbol.references.length === 0) return false;
|
|
14860
|
+
const ownedReferences = functionSymbol.references.map((reference) => getOwnedFunctionReference(reference.identifier, usageFunction, usage.node, callback, cleanupReturns, context));
|
|
14861
|
+
if (ownedReferences.some((reference) => reference === null)) return false;
|
|
14862
|
+
const generationKeys = new Set(ownedReferences.flatMap((reference) => reference?.generationKey ? [reference.generationKey] : []));
|
|
14863
|
+
if (generationKeys.size !== 1) return false;
|
|
14864
|
+
const generationKey = generationKeys.values().next().value;
|
|
14865
|
+
if (typeof generationKey !== "string") return false;
|
|
14866
|
+
return [...collectSynchronouslyEffectInvokedFunctions(callback), ...cleanupFunctions].some((owner) => functionAdvancesGeneration(owner, generationKey, context));
|
|
14867
|
+
};
|
|
14702
14868
|
const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14869
|
+
if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
|
|
14703
14870
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14704
14871
|
const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
|
|
14705
14872
|
if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
|
|
@@ -14870,7 +15037,7 @@ const getReleaseVerbName = (node) => {
|
|
|
14870
15037
|
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
14871
15038
|
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
14872
15039
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14873
|
-
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !
|
|
15040
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction, context) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
14874
15041
|
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
14875
15042
|
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
14876
15043
|
if (controllerKey === null || refCurrentKey === null) return false;
|
|
@@ -14890,6 +15057,62 @@ const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) =>
|
|
|
14890
15057
|
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
14891
15058
|
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
14892
15059
|
};
|
|
15060
|
+
const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
|
|
15061
|
+
const isFunctionForwardedToReactRef = (functionNode, context) => {
|
|
15062
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15063
|
+
if (!bindingIdentifier) return false;
|
|
15064
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15065
|
+
if (!symbol) return false;
|
|
15066
|
+
return symbol.references.some((reference) => {
|
|
15067
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15068
|
+
const expressionContainer = referenceRoot.parent;
|
|
15069
|
+
return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
|
|
15070
|
+
});
|
|
15071
|
+
};
|
|
15072
|
+
const isFunctionReturnedFromReactHook = (functionNode, context, requireRefPropertyName) => {
|
|
15073
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15074
|
+
if (!bindingIdentifier) return false;
|
|
15075
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15076
|
+
if (!symbol) return false;
|
|
15077
|
+
return symbol.references.some((reference) => {
|
|
15078
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15079
|
+
const property = referenceRoot.parent;
|
|
15080
|
+
const propertyName = isNodeOfType(property, "Property") ? getStaticPropertyKeyName(property) : null;
|
|
15081
|
+
if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression") || requireRefPropertyName && propertyName !== "ref" && !propertyName?.endsWith("Ref")) return false;
|
|
15082
|
+
const returnedObject = findTransparentExpressionRoot(property.parent);
|
|
15083
|
+
const returnStatement = returnedObject.parent;
|
|
15084
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
|
|
15085
|
+
const ownerFunction = findEnclosingFunction$1(returnStatement);
|
|
15086
|
+
return Boolean(ownerFunction && isReactHookName(getFunctionBindingIdentifier$1(ownerFunction)?.name ?? ""));
|
|
15087
|
+
});
|
|
15088
|
+
};
|
|
15089
|
+
const isFunctionUsedAsReactRef = (functionNode, context) => isFunctionForwardedToReactRef(functionNode, context) || isFunctionReturnedFromReactHook(functionNode, context, true);
|
|
15090
|
+
const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
15091
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
15092
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15093
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction) || usageFunction !== findEnclosingFunction$1(releaseCall) || !isFunctionUsedAsReactRef(usageFunction, context)) return false;
|
|
15094
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
15095
|
+
const releaseCallee = stripParenExpression(releaseCall.callee);
|
|
15096
|
+
const releaseRefSymbol = isNodeOfType(releaseCallee, "MemberExpression") ? resolveReactRefCurrentOriginSymbol(releaseCallee.object, context.scopes) : null;
|
|
15097
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed || !isNodeOfType(registrationCallee.property, "Identifier") || registrationCallee.property.name !== "addEventListener" || !isNodeOfType(releaseCallee, "MemberExpression") || releaseCallee.computed || !isNodeOfType(releaseCallee.property, "Identifier") || releaseCallee.property.name !== "removeEventListener" || !releaseRefSymbol) return false;
|
|
15098
|
+
const registrationReceiverKey = resolveExpressionKey(stripParenExpression(registrationCallee.object), context);
|
|
15099
|
+
const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
|
|
15100
|
+
const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
|
|
15101
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
|
|
15102
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15103
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15104
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15105
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
15106
|
+
const matchingOwnershipAssignments = [];
|
|
15107
|
+
const usageFunctionBody = usageFunction.body;
|
|
15108
|
+
walkAst(usageFunctionBody, (child) => {
|
|
15109
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
15110
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.operator === "=" && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === releaseRefSymbol.id && resolveExpressionKey(child.right, context) === registrationReceiverKey && releaseStart !== null && (getRangeStart(child) ?? -1) > releaseStart) matchingOwnershipAssignments.push(child);
|
|
15111
|
+
});
|
|
15112
|
+
const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
|
|
15113
|
+
const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
|
|
15114
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15115
|
+
};
|
|
14893
15116
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14894
15117
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14895
15118
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14906,14 +15129,21 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14906
15129
|
if (!releaseVerbName) return false;
|
|
14907
15130
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
14908
15131
|
const releaseReceiverKey = resolveExpressionKey(callee.object, context);
|
|
15132
|
+
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
15133
|
+
const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
15134
|
+
const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
|
|
15135
|
+
const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
|
|
15136
|
+
const releaseReceiverCollectionKey = releaseReceiverForOfStatement ? resolveExpressionKey(releaseReceiverForOfStatement.right, context) : resolveIteratorCollectionKey(callee.object, context);
|
|
15137
|
+
if (pairedReleaseVerbNames && matchesPairedReleaseVerb(releaseVerbName, pairedReleaseVerbNames) && pushedResourceCollectionKey !== null && pushedResourceCollectionKey === releaseReceiverCollectionKey && (releaseVerbName !== "unobserve" || usage.eventKey !== null && releaseEventKey === usage.eventKey)) return true;
|
|
15138
|
+
if (isReactRefListenerReplacementRelease(callNode, usage, context)) return true;
|
|
14909
15139
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14910
15140
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
14911
15141
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
14912
15142
|
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14913
15143
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
15144
|
+
if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
14914
15145
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14915
15146
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
14916
|
-
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
14917
15147
|
const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
|
|
14918
15148
|
const releaseEventArgument = callNode.arguments?.[0];
|
|
14919
15149
|
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
@@ -14954,7 +15184,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14954
15184
|
return true;
|
|
14955
15185
|
};
|
|
14956
15186
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
14957
|
-
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
15187
|
+
const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
14958
15188
|
let currentNode = functionNode;
|
|
14959
15189
|
let parentNode = currentNode.parent;
|
|
14960
15190
|
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
@@ -14963,16 +15193,147 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14963
15193
|
}
|
|
14964
15194
|
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14965
15195
|
const effectCall = effectCallback?.parent;
|
|
14966
|
-
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") &&
|
|
15196
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes));
|
|
14967
15197
|
};
|
|
14968
15198
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
14969
|
-
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
15199
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
|
|
14970
15200
|
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
14971
15201
|
if (!bindingIdentifier) return false;
|
|
14972
15202
|
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
14973
15203
|
if (!symbol) return false;
|
|
14974
15204
|
return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
|
|
14975
15205
|
};
|
|
15206
|
+
const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
|
|
15207
|
+
if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
|
|
15208
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15209
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
|
|
15210
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
15211
|
+
const collectAssignment = (expression) => {
|
|
15212
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
15213
|
+
const assignment = expressionRoot.parent;
|
|
15214
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
|
|
15215
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
15216
|
+
const refCurrentKey = resolveExpressionKey(assignment.left, context);
|
|
15217
|
+
const retainedFunction = findEnclosingFunction$1(assignment);
|
|
15218
|
+
const assignmentStart = getRangeStart(assignment);
|
|
15219
|
+
if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
|
|
15220
|
+
assignments.set(assignmentStart, {
|
|
15221
|
+
assignmentNode: assignment,
|
|
15222
|
+
refCurrentKey,
|
|
15223
|
+
retainedFunction
|
|
15224
|
+
});
|
|
15225
|
+
};
|
|
15226
|
+
collectAssignment(disposerFunction);
|
|
15227
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
|
|
15228
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
15229
|
+
for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
|
|
15230
|
+
walkAst(usageFunction.body, (child) => {
|
|
15231
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
15232
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
|
|
15233
|
+
});
|
|
15234
|
+
return [...assignments.values()];
|
|
15235
|
+
};
|
|
15236
|
+
const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
|
|
15237
|
+
const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
|
|
15238
|
+
let hasUnsafeOverwrite = false;
|
|
15239
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15240
|
+
if (hasUnsafeOverwrite) return false;
|
|
15241
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15242
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
|
|
15243
|
+
const storedValue = resolveStableValue(child.right, context);
|
|
15244
|
+
if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
|
|
15245
|
+
hasUnsafeOverwrite = true;
|
|
15246
|
+
return false;
|
|
15247
|
+
}
|
|
15248
|
+
});
|
|
15249
|
+
return hasUnsafeOverwrite;
|
|
15250
|
+
};
|
|
15251
|
+
const hasEffectCleanupInvocation = (storage, usage, context) => {
|
|
15252
|
+
const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
|
|
15253
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
|
|
15254
|
+
const cleanupFunctionInvokesRef = (cleanupFunction) => {
|
|
15255
|
+
if (!isFunctionLike$1(cleanupFunction)) return false;
|
|
15256
|
+
let didFindCleanupCall = false;
|
|
15257
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
15258
|
+
if (didFindCleanupCall) return false;
|
|
15259
|
+
if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15260
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
|
|
15261
|
+
const callRoot = findTransparentExpressionRoot(child);
|
|
15262
|
+
const callStatement = callRoot.parent;
|
|
15263
|
+
const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
|
|
15264
|
+
const isConciseBody = cleanupFunction.body === callRoot;
|
|
15265
|
+
if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
|
|
15266
|
+
didFindCleanupCall = true;
|
|
15267
|
+
return false;
|
|
15268
|
+
}
|
|
15269
|
+
}
|
|
15270
|
+
});
|
|
15271
|
+
return didFindCleanupCall;
|
|
15272
|
+
};
|
|
15273
|
+
const effectReturnsCleanup = (effectCallback) => {
|
|
15274
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
15275
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
15276
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
|
|
15277
|
+
return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
|
|
15278
|
+
}
|
|
15279
|
+
const matchingReturns = [];
|
|
15280
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15281
|
+
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15282
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
|
|
15283
|
+
if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
|
|
15284
|
+
matchingReturns.push(child);
|
|
15285
|
+
});
|
|
15286
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15287
|
+
};
|
|
15288
|
+
let didFindInvocation = false;
|
|
15289
|
+
walkAst(componentFunction.body, (child) => {
|
|
15290
|
+
if (didFindInvocation) return false;
|
|
15291
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
|
|
15292
|
+
const effectCallback = getEffectCallback(child);
|
|
15293
|
+
if (effectCallback && effectReturnsCleanup(effectCallback)) {
|
|
15294
|
+
didFindInvocation = true;
|
|
15295
|
+
return false;
|
|
15296
|
+
}
|
|
15297
|
+
});
|
|
15298
|
+
return didFindInvocation;
|
|
15299
|
+
};
|
|
15300
|
+
const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
|
|
15301
|
+
const isReturnedCallbackRefShape = () => {
|
|
15302
|
+
if (!isFunctionLike$1(storage.retainedFunction)) return false;
|
|
15303
|
+
const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
|
|
15304
|
+
if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
|
|
15305
|
+
const nodeParameter = storage.retainedFunction.params?.[0];
|
|
15306
|
+
const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
|
|
15307
|
+
if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
|
|
15308
|
+
if (!isFunctionReturnedFromReactHook(storage.retainedFunction, context, false)) return false;
|
|
15309
|
+
const usageStart = getRangeStart(usage.node);
|
|
15310
|
+
if (usageStart === null) return false;
|
|
15311
|
+
let hasNullExit = false;
|
|
15312
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15313
|
+
if (hasNullExit) return false;
|
|
15314
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15315
|
+
if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
|
|
15316
|
+
const test = stripParenExpression(child.test);
|
|
15317
|
+
if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
|
|
15318
|
+
const consequent = child.consequent;
|
|
15319
|
+
hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
|
|
15320
|
+
if (hasNullExit) return false;
|
|
15321
|
+
});
|
|
15322
|
+
return hasNullExit;
|
|
15323
|
+
};
|
|
15324
|
+
if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
|
|
15325
|
+
const cleanupCalls = [];
|
|
15326
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15327
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15328
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
|
|
15329
|
+
});
|
|
15330
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
|
|
15331
|
+
};
|
|
15332
|
+
const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
15333
|
+
const disposerFunction = findEnclosingFunction$1(releaseNode);
|
|
15334
|
+
if (!disposerFunction) return false;
|
|
15335
|
+
return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
|
|
15336
|
+
};
|
|
14976
15337
|
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
14977
15338
|
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
14978
15339
|
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
@@ -14997,6 +15358,7 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
|
14997
15358
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
14998
15359
|
if (!releaseFunction) return true;
|
|
14999
15360
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
15361
|
+
if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
|
|
15000
15362
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15001
15363
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15002
15364
|
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
@@ -15331,7 +15693,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
|
15331
15693
|
return false;
|
|
15332
15694
|
}
|
|
15333
15695
|
}
|
|
15334
|
-
if (
|
|
15696
|
+
if (isSubscribeOrObserveCallExpression(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
15335
15697
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15336
15698
|
const subscriptionUsage = {
|
|
15337
15699
|
kind: "subscribe",
|
|
@@ -15539,7 +15901,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
|
15539
15901
|
if (!isFunctionLike$1(functionNode)) return false;
|
|
15540
15902
|
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15541
15903
|
const callbackCall = functionRoot.parent;
|
|
15542
|
-
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot &&
|
|
15904
|
+
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isReactHookCall(callbackCall, "useCallback", context.scopes) && isDirectJsxEventHandlerValue(callbackCall)) return true;
|
|
15543
15905
|
const parentNode = functionNode.parent;
|
|
15544
15906
|
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
15545
15907
|
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
@@ -15575,12 +15937,12 @@ const effectNeedsCleanup = defineRule({
|
|
|
15575
15937
|
};
|
|
15576
15938
|
return {
|
|
15577
15939
|
CallExpression(node) {
|
|
15578
|
-
if (
|
|
15940
|
+
if (isReactHookCall(node, "useCallback", context.scopes)) {
|
|
15579
15941
|
const retainedCallback = getEffectCallback(node);
|
|
15580
15942
|
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
15581
15943
|
return;
|
|
15582
15944
|
}
|
|
15583
|
-
if (!
|
|
15945
|
+
if (!isReactHookCall(node, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
15584
15946
|
const callback = getEffectCallback(node);
|
|
15585
15947
|
if (!callback) return;
|
|
15586
15948
|
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
@@ -15588,7 +15950,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
15588
15950
|
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
15589
15951
|
if (!firstUsage) return;
|
|
15590
15952
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
15591
|
-
const hookName = getCalleeName$
|
|
15953
|
+
const hookName = getCalleeName$1(node) ?? "effect";
|
|
15592
15954
|
context.report({
|
|
15593
15955
|
node,
|
|
15594
15956
|
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.`
|
|
@@ -16931,7 +17293,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
|
16931
17293
|
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
16932
17294
|
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
16933
17295
|
};
|
|
16934
|
-
const
|
|
17296
|
+
const isAssignmentTarget = (node) => {
|
|
17297
|
+
let currentNode = findTransparentExpressionRoot(node);
|
|
17298
|
+
while (currentNode.parent) {
|
|
17299
|
+
const parentNode = currentNode.parent;
|
|
17300
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
17301
|
+
if (isNodeOfType(parentNode, "UpdateExpression")) return parentNode.argument === currentNode;
|
|
17302
|
+
if (isNodeOfType(parentNode, "UnaryExpression")) return parentNode.operator === "delete" && parentNode.argument === currentNode;
|
|
17303
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
17304
|
+
if (isNodeOfType(parentNode, "ArrayPattern") && parentNode.elements.some((element) => element === currentNode) || isNodeOfType(parentNode, "ObjectPattern") && parentNode.properties.some((property) => property === currentNode) || isNodeOfType(parentNode, "RestElement") && parentNode.argument === currentNode || isNodeOfType(parentNode, "AssignmentPattern") && parentNode.left === currentNode || isNodeOfType(parentNode, "Property") && parentNode.value === currentNode && isNodeOfType(parentNode.parent, "ObjectPattern")) {
|
|
17305
|
+
currentNode = parentNode;
|
|
17306
|
+
continue;
|
|
17307
|
+
}
|
|
17308
|
+
return false;
|
|
17309
|
+
}
|
|
17310
|
+
return false;
|
|
17311
|
+
};
|
|
17312
|
+
const symbolHasStableRefLazyInitialization = (symbol, scopes) => {
|
|
17313
|
+
if (symbol.kind !== "const" || symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
17314
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
17315
|
+
if (!isNodeOfType(initializer, "AssignmentExpression") || initializer.operator !== "??=") return false;
|
|
17316
|
+
const refSymbol = resolveReactRefSymbol(unwrapExpression$3(initializer.left), scopes);
|
|
17317
|
+
if (!refSymbol) return false;
|
|
17318
|
+
return refSymbol.references.every((reference) => {
|
|
17319
|
+
const memberExpression = findTransparentExpressionRoot(reference.identifier).parent;
|
|
17320
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || unwrapExpression$3(memberExpression.object) !== reference.identifier || getStaticPropertyName(memberExpression) !== "current") return false;
|
|
17321
|
+
const referenceRoot = findTransparentExpressionRoot(memberExpression);
|
|
17322
|
+
const parentNode = referenceRoot.parent;
|
|
17323
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.left === referenceRoot) return parentNode === initializer;
|
|
17324
|
+
return !isAssignmentTarget(referenceRoot);
|
|
17325
|
+
});
|
|
17326
|
+
};
|
|
17327
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableRefLazyInitialization(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
16935
17328
|
//#endregion
|
|
16936
17329
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
16937
17330
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -18114,7 +18507,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
18114
18507
|
if (!isUsed) continue;
|
|
18115
18508
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
18116
18509
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
18117
|
-
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18510
|
+
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18118
18511
|
context.report({
|
|
18119
18512
|
node: reportNode,
|
|
18120
18513
|
message: buildUnstableDepMessage(hookName, declaredKey)
|
|
@@ -18430,7 +18823,7 @@ const flattenCalleeName = (callee) => {
|
|
|
18430
18823
|
const PRAGMA = "React";
|
|
18431
18824
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
18432
18825
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
18433
|
-
if (getCalleeName$
|
|
18826
|
+
if (getCalleeName$1(node) !== expectedCall) return false;
|
|
18434
18827
|
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
18435
18828
|
const receiver = stripParenExpression(node.callee.object);
|
|
18436
18829
|
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
@@ -18628,10 +19021,14 @@ const hookUseState = defineRule({
|
|
|
18628
19021
|
create: (context) => {
|
|
18629
19022
|
const { allowDestructuredState } = resolveSettings$40(context.settings);
|
|
18630
19023
|
return { CallExpression(node) {
|
|
18631
|
-
|
|
19024
|
+
const callExpressionNode = node;
|
|
19025
|
+
if (!isReactFunctionCall(callExpressionNode, "useState")) return;
|
|
19026
|
+
const expressionRoot = findTransparentExpressionRoot(callExpressionNode);
|
|
19027
|
+
const expressionParent = expressionRoot.parent;
|
|
19028
|
+
if (isNodeOfType(expressionParent, "ReturnStatement")) return;
|
|
19029
|
+
if (isNodeOfType(expressionParent, "ArrowFunctionExpression") && expressionParent.body === expressionRoot) return;
|
|
18632
19030
|
const parent = node.parent;
|
|
18633
19031
|
if (!parent) return;
|
|
18634
|
-
if (isNodeOfType(parent, "ReturnStatement")) return;
|
|
18635
19032
|
if (!isNodeOfType(parent, "VariableDeclarator")) {
|
|
18636
19033
|
context.report({
|
|
18637
19034
|
node,
|
|
@@ -18739,8 +19136,8 @@ const hooksNoNanInDeps = defineRule({
|
|
|
18739
19136
|
severity: "warn",
|
|
18740
19137
|
recommendation: "Remove `NaN` (or `Number.NaN`) from the dependency array. If a value can be NaN at runtime, normalise it (`Number.isNaN(x) ? 0 : x`) before passing it.",
|
|
18741
19138
|
create: (context) => ({ CallExpression(node) {
|
|
18742
|
-
if (!
|
|
18743
|
-
const depsIndex =
|
|
19139
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEP_ARRAY, context.scopes)) return;
|
|
19140
|
+
const depsIndex = isReactHookCall(node, "useImperativeHandle", context.scopes) ? 2 : 1;
|
|
18744
19141
|
const depsArgument = node.arguments[depsIndex];
|
|
18745
19142
|
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
18746
19143
|
for (const element of depsArgument.elements) {
|
|
@@ -19911,7 +20308,7 @@ const isImportedSelectAtom = (callExpression) => {
|
|
|
19911
20308
|
const isDeferredCallbackPosition$1 = (functionNode) => {
|
|
19912
20309
|
const parent = functionNode.parent;
|
|
19913
20310
|
if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
|
|
19914
|
-
const hookName = getCalleeName$
|
|
20311
|
+
const hookName = getCalleeName$1(parent);
|
|
19915
20312
|
if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
|
|
19916
20313
|
if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
|
|
19917
20314
|
}
|
|
@@ -29982,7 +30379,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
|
|
|
29982
30379
|
"on",
|
|
29983
30380
|
"once"
|
|
29984
30381
|
]);
|
|
29985
|
-
const getCalleeName
|
|
30382
|
+
const getCalleeName = (callee) => {
|
|
29986
30383
|
if (!callee) return null;
|
|
29987
30384
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
29988
30385
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
@@ -29994,11 +30391,11 @@ const isDeferredCallbackPosition = (expression) => {
|
|
|
29994
30391
|
const parent = parentOf(expression);
|
|
29995
30392
|
if (!parent) return false;
|
|
29996
30393
|
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
29997
|
-
const name = getCalleeName
|
|
30394
|
+
const name = getCalleeName(parent.callee);
|
|
29998
30395
|
if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
|
|
29999
30396
|
}
|
|
30000
30397
|
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
30001
|
-
const name = getCalleeName
|
|
30398
|
+
const name = getCalleeName(parent.callee);
|
|
30002
30399
|
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
30003
30400
|
}
|
|
30004
30401
|
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
@@ -30418,14 +30815,6 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
30418
30815
|
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30419
30816
|
return false;
|
|
30420
30817
|
};
|
|
30421
|
-
const isUseEffect = (node) => {
|
|
30422
|
-
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
30423
|
-
const callee = node.callee;
|
|
30424
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
30425
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
30426
|
-
const receiver = stripParenExpression(callee.object);
|
|
30427
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
30428
|
-
};
|
|
30429
30818
|
const getEffectFn = (analysis, node) => {
|
|
30430
30819
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30431
30820
|
const fn = node.arguments?.[0];
|
|
@@ -30523,7 +30912,27 @@ const isRefCurrent = (ref) => {
|
|
|
30523
30912
|
if (!isNodeOfType(parent.property, "Identifier")) return false;
|
|
30524
30913
|
return parent.property.name === "current";
|
|
30525
30914
|
};
|
|
30526
|
-
const
|
|
30915
|
+
const resolveStateSetterReference = (analysis, ref) => {
|
|
30916
|
+
const visitedReferences = /* @__PURE__ */ new Set();
|
|
30917
|
+
let currentReference = ref;
|
|
30918
|
+
while (currentReference && !visitedReferences.has(currentReference)) {
|
|
30919
|
+
if (isStateSetter(analysis, currentReference)) return currentReference;
|
|
30920
|
+
visitedReferences.add(currentReference);
|
|
30921
|
+
const definitions = currentReference.resolved?.defs ?? [];
|
|
30922
|
+
if (definitions.length !== 1) return null;
|
|
30923
|
+
const definitionNode = definitions[0].node;
|
|
30924
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator")) return null;
|
|
30925
|
+
if (!isNodeOfType(definitionNode.id, "Identifier")) return null;
|
|
30926
|
+
const declaration = definitionNode.parent;
|
|
30927
|
+
if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30928
|
+
if (!definitionNode.init) return null;
|
|
30929
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
30930
|
+
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
30931
|
+
currentReference = getRef(analysis, initializer);
|
|
30932
|
+
}
|
|
30933
|
+
return null;
|
|
30934
|
+
};
|
|
30935
|
+
const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => resolveStateSetterReference(analysis, innerRef) !== null);
|
|
30527
30936
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
30528
30937
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
30529
30938
|
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
@@ -30674,9 +31083,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
|
30674
31083
|
};
|
|
30675
31084
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
30676
31085
|
const getUseStateDecl = (analysis, ref) => {
|
|
30677
|
-
|
|
30678
|
-
|
|
30679
|
-
|
|
31086
|
+
const definition = getUpstreamRefs(analysis, ref).find((upstreamReference) => isState(analysis, upstreamReference) || isStateSetter(analysis, upstreamReference))?.resolved?.defs.find((candidateDefinition) => {
|
|
31087
|
+
const definitionNode = candidateDefinition.node;
|
|
31088
|
+
return isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && isHookCallee$1(analysis, definitionNode.init.callee, "useState");
|
|
31089
|
+
});
|
|
31090
|
+
return definition ? definition.node : null;
|
|
30680
31091
|
};
|
|
30681
31092
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
30682
31093
|
if (isFunctionLike$1(node)) return true;
|
|
@@ -31865,6 +32276,7 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
31865
32276
|
sourceReferences,
|
|
31866
32277
|
isDeferred: frame.isDeferred,
|
|
31867
32278
|
isRenderKnownCopy,
|
|
32279
|
+
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
31868
32280
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
31869
32281
|
resetsSourceState: false
|
|
31870
32282
|
});
|
|
@@ -31880,22 +32292,71 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
31880
32292
|
});
|
|
31881
32293
|
};
|
|
31882
32294
|
//#endregion
|
|
32295
|
+
//#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
|
|
32296
|
+
const DEFERRED_MEMBER_NAMES = new Set([
|
|
32297
|
+
"catch",
|
|
32298
|
+
"finally",
|
|
32299
|
+
"then"
|
|
32300
|
+
]);
|
|
32301
|
+
const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
|
|
32302
|
+
const effectFunction = getEffectFn(analysis, effectNode);
|
|
32303
|
+
if (!effectFunction) return false;
|
|
32304
|
+
if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
|
|
32305
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
|
|
32306
|
+
let didFindDeferredOrExternalWork = false;
|
|
32307
|
+
walkAst(effectFunction, (child) => {
|
|
32308
|
+
if (didFindDeferredOrExternalWork) return false;
|
|
32309
|
+
if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
32310
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
32311
|
+
const assignmentTarget = child.left;
|
|
32312
|
+
if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
|
|
32313
|
+
didFindDeferredOrExternalWork = true;
|
|
32314
|
+
return false;
|
|
32315
|
+
}
|
|
32316
|
+
}
|
|
32317
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32318
|
+
if (isSubscribeOrObserveCallExpression(child)) {
|
|
32319
|
+
didFindDeferredOrExternalWork = true;
|
|
32320
|
+
return false;
|
|
32321
|
+
}
|
|
32322
|
+
const localFunction = resolveExactLocalFunction(child.callee, scopes);
|
|
32323
|
+
if (isFunctionLike$1(localFunction) && localFunction.async) {
|
|
32324
|
+
didFindDeferredOrExternalWork = true;
|
|
32325
|
+
return false;
|
|
32326
|
+
}
|
|
32327
|
+
const callee = child.callee;
|
|
32328
|
+
if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
|
|
32329
|
+
didFindDeferredOrExternalWork = true;
|
|
32330
|
+
return false;
|
|
32331
|
+
}
|
|
32332
|
+
const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
32333
|
+
if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
|
|
32334
|
+
didFindDeferredOrExternalWork = true;
|
|
32335
|
+
return false;
|
|
32336
|
+
}
|
|
32337
|
+
});
|
|
32338
|
+
return didFindDeferredOrExternalWork;
|
|
32339
|
+
};
|
|
32340
|
+
//#endregion
|
|
31883
32341
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
31884
32342
|
const noAdjustStateOnPropChange = defineRule({
|
|
31885
32343
|
id: "no-adjust-state-on-prop-change",
|
|
31886
|
-
title: "State
|
|
32344
|
+
title: "State adjusted after a prop changes",
|
|
31887
32345
|
severity: "warn",
|
|
31888
32346
|
tags: ["test-noise"],
|
|
31889
|
-
recommendation: "
|
|
32347
|
+
recommendation: "Remove the adjustment effect by deriving values during render, resetting the component with a key, or updating related state in the event that changes the prop. Avoid tracking the previous prop in more state, which preserves the duplication. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
31890
32348
|
create: (context) => ({ CallExpression(node) {
|
|
31891
|
-
if (!
|
|
32349
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
31892
32350
|
const analysis = getProgramAnalysis(node);
|
|
31893
32351
|
if (!analysis) return;
|
|
31894
32352
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31895
32353
|
if (!dependencyReferences) return;
|
|
31896
32354
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31897
|
-
|
|
31898
|
-
|
|
32355
|
+
const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
|
|
32356
|
+
if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
|
|
32357
|
+
for (const fact of facts) {
|
|
32358
|
+
if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
|
|
32359
|
+
if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
|
|
31899
32360
|
context.report({
|
|
31900
32361
|
node: fact.callExpression,
|
|
31901
32362
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
@@ -33908,7 +34369,7 @@ const declarationBodyContainsHookCall = (symbol) => {
|
|
|
33908
34369
|
walkAst(componentFunction, (descendant) => {
|
|
33909
34370
|
if (didFindHookCall) return false;
|
|
33910
34371
|
if (!isNodeOfType(descendant, "CallExpression")) return;
|
|
33911
|
-
const calleeName = getCalleeName$
|
|
34372
|
+
const calleeName = getCalleeName$1(descendant);
|
|
33912
34373
|
if (calleeName && isReactHookName(calleeName)) {
|
|
33913
34374
|
didFindHookCall = true;
|
|
33914
34375
|
return false;
|
|
@@ -33933,7 +34394,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
|
|
|
33933
34394
|
if (isNodeOfType(parent, "ArrowFunctionExpression")) {
|
|
33934
34395
|
if (parent.body !== current) return false;
|
|
33935
34396
|
const grandparent = parent.parent;
|
|
33936
|
-
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$
|
|
34397
|
+
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
|
|
33937
34398
|
}
|
|
33938
34399
|
if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
|
|
33939
34400
|
current = parent;
|
|
@@ -34615,7 +35076,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34615
35076
|
if (!callExpr) continue;
|
|
34616
35077
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34617
35078
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
34618
|
-
const declarator = getUseStateDeclarator(ref);
|
|
35079
|
+
const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34619
35080
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
34620
35081
|
}
|
|
34621
35082
|
for (const ref of effectFnRefs) {
|
|
@@ -34624,7 +35085,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34624
35085
|
if (!callExpr) continue;
|
|
34625
35086
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34626
35087
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
34627
|
-
const setterDeclarator = getUseStateDeclarator(ref);
|
|
35088
|
+
const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34628
35089
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
34629
35090
|
const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
|
|
34630
35091
|
const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
|
|
@@ -36465,7 +36926,7 @@ const noDerivedState = defineRule({
|
|
|
36465
36926
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36466
36927
|
} }).visitors,
|
|
36467
36928
|
CallExpression(node) {
|
|
36468
|
-
if (!
|
|
36929
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
36469
36930
|
const analysis = getProgramAnalysis(node);
|
|
36470
36931
|
if (!analysis) return;
|
|
36471
36932
|
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
@@ -36485,7 +36946,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
36485
36946
|
tags: ["test-noise"],
|
|
36486
36947
|
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
36487
36948
|
create: (context) => ({ CallExpression(node) {
|
|
36488
|
-
if (!
|
|
36949
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
36489
36950
|
const analysis = getProgramAnalysis(node);
|
|
36490
36951
|
if (!analysis) return;
|
|
36491
36952
|
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
@@ -36608,7 +37069,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
|
|
|
36608
37069
|
if (isFunctionLike$1(cursor)) {
|
|
36609
37070
|
const parent = cursor.parent ?? null;
|
|
36610
37071
|
if (parent && isNodeOfType(parent, "CallExpression")) {
|
|
36611
|
-
const calleeName = getCalleeName$
|
|
37072
|
+
const calleeName = getCalleeName$1(parent);
|
|
36612
37073
|
if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
|
|
36613
37074
|
}
|
|
36614
37075
|
}
|
|
@@ -36679,7 +37140,7 @@ const isNonHandlerHookCallback = (functionNode) => {
|
|
|
36679
37140
|
const parent = functionNode.parent ?? null;
|
|
36680
37141
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
36681
37142
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
36682
|
-
const calleeName = getCalleeName$
|
|
37143
|
+
const calleeName = getCalleeName$1(parent);
|
|
36683
37144
|
return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
|
|
36684
37145
|
};
|
|
36685
37146
|
const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
@@ -36761,7 +37222,7 @@ const noDerivedUseState = defineRule({
|
|
|
36761
37222
|
return {
|
|
36762
37223
|
...propStackTracker.visitors,
|
|
36763
37224
|
CallExpression(node) {
|
|
36764
|
-
if (!
|
|
37225
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
36765
37226
|
const seed = unwrapInitializerSeed(node.arguments[0]);
|
|
36766
37227
|
const reportStalePropCopy = (propName) => {
|
|
36767
37228
|
if (isIntentionalSnapshotState(node)) return;
|
|
@@ -37437,7 +37898,7 @@ const noDirectMutationState = defineRule({
|
|
|
37437
37898
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
37438
37899
|
//#endregion
|
|
37439
37900
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
37440
|
-
const collectUseStateBindings = (componentBody) => {
|
|
37901
|
+
const collectUseStateBindings = (componentBody, scopes) => {
|
|
37441
37902
|
const bindings = [];
|
|
37442
37903
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
37443
37904
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -37450,7 +37911,7 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
37450
37911
|
const setterElement = elements[1];
|
|
37451
37912
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37452
37913
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37453
|
-
if (!
|
|
37914
|
+
if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
|
|
37454
37915
|
bindings.push({
|
|
37455
37916
|
valueName: valueElement.name,
|
|
37456
37917
|
setterName: setterElement.name,
|
|
@@ -37617,7 +38078,7 @@ const noDirectStateMutation = defineRule({
|
|
|
37617
38078
|
create: (context) => {
|
|
37618
38079
|
const checkComponent = (componentBody) => {
|
|
37619
38080
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
37620
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
38081
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
37621
38082
|
if (bindings.length === 0) return;
|
|
37622
38083
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
37623
38084
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
@@ -38177,25 +38638,31 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
38177
38638
|
};
|
|
38178
38639
|
//#endregion
|
|
38179
38640
|
//#region src/plugin/rules/state-and-effects/no-effect-chain.ts
|
|
38180
|
-
const findTopLevelEffectCalls = (componentBody) => {
|
|
38641
|
+
const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
38181
38642
|
const effectCalls = [];
|
|
38182
38643
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
38183
38644
|
for (const statement of componentBody.body ?? []) {
|
|
38184
38645
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38185
38646
|
const expression = unwrapDiscardedExpression(statement);
|
|
38186
38647
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
38187
|
-
if (!
|
|
38648
|
+
if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
|
|
38188
38649
|
effectCalls.push(expression);
|
|
38189
38650
|
}
|
|
38190
38651
|
return effectCalls;
|
|
38191
38652
|
};
|
|
38192
|
-
const
|
|
38193
|
-
const
|
|
38194
|
-
if (!isNodeOfType(effectNode, "CallExpression")) return
|
|
38653
|
+
const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
|
|
38654
|
+
const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
|
|
38655
|
+
if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
|
|
38195
38656
|
const depsNode = effectNode.arguments?.[1];
|
|
38196
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) return
|
|
38197
|
-
for (const element of depsNode.elements ?? [])
|
|
38198
|
-
|
|
38657
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyStateSymbolIds;
|
|
38658
|
+
for (const element of depsNode.elements ?? []) {
|
|
38659
|
+
if (!element || isNodeOfType(element, "SpreadElement")) continue;
|
|
38660
|
+
const rootIdentifier = getRootIdentifier$1(element);
|
|
38661
|
+
if (!isNodeOfType(rootIdentifier, "Identifier")) continue;
|
|
38662
|
+
const symbol = resolveConstIdentifierAlias(rootIdentifier, scopes, true);
|
|
38663
|
+
if (symbol && stateSymbolIds.has(symbol.id)) dependencyStateSymbolIds.add(symbol.id);
|
|
38664
|
+
}
|
|
38665
|
+
return dependencyStateSymbolIds;
|
|
38199
38666
|
};
|
|
38200
38667
|
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
38201
38668
|
const analysisFunctions = new Set([effectCallback]);
|
|
@@ -38301,12 +38768,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
|
|
|
38301
38768
|
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
38302
38769
|
return readStaticEffectValue(argument, scopes, null, null);
|
|
38303
38770
|
};
|
|
38304
|
-
const collectStateWritesInEffect = (analysisFunctions,
|
|
38771
|
+
const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
|
|
38305
38772
|
const stateWrites = /* @__PURE__ */ new Map();
|
|
38306
38773
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38307
38774
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38308
38775
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
38309
|
-
const
|
|
38776
|
+
const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
|
|
38777
|
+
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
38310
38778
|
if (!stateName) return;
|
|
38311
38779
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
38312
38780
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -38392,11 +38860,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
38392
38860
|
"keys",
|
|
38393
38861
|
"values"
|
|
38394
38862
|
]);
|
|
38395
|
-
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
38863
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
38396
38864
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
38397
38865
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
38398
38866
|
if (isNodeOfType(returnedValue.callee, "Identifier")) {
|
|
38399
|
-
|
|
38867
|
+
const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
|
|
38868
|
+
if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
38400
38869
|
if (isSetterIdentifier(returnedValue.callee.name)) return true;
|
|
38401
38870
|
}
|
|
38402
38871
|
return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
@@ -38420,7 +38889,7 @@ const collectStorageHookSetterNames = (componentBody) => {
|
|
|
38420
38889
|
for (const declarator of statement.declarations ?? []) {
|
|
38421
38890
|
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
38422
38891
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
38423
|
-
const calleeName = getCalleeName$
|
|
38892
|
+
const calleeName = getCalleeName$1(declarator.init);
|
|
38424
38893
|
if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
|
|
38425
38894
|
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
|
|
38426
38895
|
}
|
|
@@ -38563,11 +39032,11 @@ const isExternalSyncNode = (node) => {
|
|
|
38563
39032
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
38564
39033
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
38565
39034
|
};
|
|
38566
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
39035
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
38567
39036
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
38568
39037
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
38569
|
-
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
38570
|
-
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
39038
|
+
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, setterSymbolIdToStateName, scopes, false)) return true;
|
|
39039
|
+
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
|
|
38571
39040
|
let didFindExternalCall = false;
|
|
38572
39041
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38573
39042
|
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
@@ -38583,10 +39052,11 @@ const noEffectChain = defineRule({
|
|
|
38583
39052
|
create: (context) => {
|
|
38584
39053
|
const checkComponent = (componentBody) => {
|
|
38585
39054
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38586
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
39055
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
38587
39056
|
if (useStateBindings.length === 0) return;
|
|
38588
39057
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
38589
39058
|
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
39059
|
+
const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
|
|
38590
39060
|
for (const binding of useStateBindings) {
|
|
38591
39061
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
38592
39062
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
@@ -38595,21 +39065,27 @@ const noEffectChain = defineRule({
|
|
|
38595
39065
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
38596
39066
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
38597
39067
|
}
|
|
39068
|
+
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39069
|
+
if (isNodeOfType(setterIdentifier, "Identifier")) {
|
|
39070
|
+
const setterSymbol = context.scopes.symbolFor(setterIdentifier);
|
|
39071
|
+
if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
|
|
39072
|
+
}
|
|
38598
39073
|
}
|
|
38599
39074
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
39075
|
+
const stateSymbolIdSet = new Set(stateSymbolIds.values());
|
|
38600
39076
|
const effectInfos = [];
|
|
38601
|
-
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
39077
|
+
for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
|
|
38602
39078
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
38603
39079
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
38604
39080
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
38605
|
-
const stateWrites = collectStateWritesInEffect(analysisFunctions,
|
|
39081
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
|
|
38606
39082
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
38607
39083
|
effectInfos.push({
|
|
38608
39084
|
node: effectCall,
|
|
38609
|
-
|
|
39085
|
+
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
38610
39086
|
stateWrites,
|
|
38611
39087
|
analysisFunctions,
|
|
38612
|
-
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
39088
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
38613
39089
|
});
|
|
38614
39090
|
}
|
|
38615
39091
|
if (effectInfos.length < 2) return;
|
|
@@ -38620,10 +39096,11 @@ const noEffectChain = defineRule({
|
|
|
38620
39096
|
for (const readerEffect of effectInfos) {
|
|
38621
39097
|
if (readerEffect === writerEffect) continue;
|
|
38622
39098
|
if (readerEffect.isExternalSync) continue;
|
|
38623
|
-
if (readerEffect.
|
|
39099
|
+
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
38624
39100
|
let chainedStateName = null;
|
|
38625
39101
|
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
38626
|
-
|
|
39102
|
+
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39103
|
+
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
38627
39104
|
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
38628
39105
|
chainedStateName = writtenName;
|
|
38629
39106
|
break;
|
|
@@ -38900,7 +39377,7 @@ const noEffectEventHandler = defineRule({
|
|
|
38900
39377
|
return {
|
|
38901
39378
|
...propStackTracker.visitors,
|
|
38902
39379
|
CallExpression(node) {
|
|
38903
|
-
if (!
|
|
39380
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
38904
39381
|
const callback = getEffectCallback(node);
|
|
38905
39382
|
if (!callback) return;
|
|
38906
39383
|
const analysis = getProgramAnalysis(node);
|
|
@@ -39020,14 +39497,14 @@ const noEffectEventInDeps = defineRule({
|
|
|
39020
39497
|
if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
|
|
39021
39498
|
const initializer = declaratorNode.init;
|
|
39022
39499
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
39023
|
-
if (!
|
|
39500
|
+
if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
|
|
39024
39501
|
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
39025
39502
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
39026
39503
|
} });
|
|
39027
39504
|
return {
|
|
39028
39505
|
...componentBindings.visitors,
|
|
39029
39506
|
CallExpression(node) {
|
|
39030
|
-
if (!
|
|
39507
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
39031
39508
|
if (!componentBindings.isInsideComponent()) return;
|
|
39032
39509
|
const depsNode = node.arguments[1];
|
|
39033
39510
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -39084,7 +39561,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
39084
39561
|
node: finding.reportNode,
|
|
39085
39562
|
message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
|
|
39086
39563
|
});
|
|
39087
|
-
if (!
|
|
39564
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
39088
39565
|
const args = node.arguments ?? [];
|
|
39089
39566
|
if (args.length < 2) return;
|
|
39090
39567
|
const depsNode = args[1];
|
|
@@ -39345,7 +39822,7 @@ const noEventHandler = defineRule({
|
|
|
39345
39822
|
severity: "warn",
|
|
39346
39823
|
recommendation: "Run the side effect in the event handler that triggers it, instead of watching its state from a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers",
|
|
39347
39824
|
create: (context) => ({ CallExpression(node) {
|
|
39348
|
-
if (!
|
|
39825
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
39349
39826
|
const analysis = getProgramAnalysis(node);
|
|
39350
39827
|
if (!analysis || hasCleanup(analysis, node)) return;
|
|
39351
39828
|
const frames = collectBoundedEffectExecutionFrames(analysis, node);
|
|
@@ -39807,25 +40284,25 @@ const addDeclarationBindings = (statement, scope) => {
|
|
|
39807
40284
|
}
|
|
39808
40285
|
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
|
|
39809
40286
|
};
|
|
39810
|
-
const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40287
|
+
const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39811
40288
|
let hasReturn = false;
|
|
39812
|
-
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
|
|
40289
|
+
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, scopes, eventHandlerReferenceNames)) hasReturn = true;
|
|
39813
40290
|
else addDeclarationBindings(statement, scope);
|
|
39814
40291
|
return hasReturn;
|
|
39815
40292
|
};
|
|
39816
|
-
const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
|
|
40293
|
+
const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
|
|
39817
40294
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
39818
40295
|
if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
|
|
39819
40296
|
return true;
|
|
39820
40297
|
}
|
|
39821
|
-
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) && !
|
|
40298
|
+
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && (isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) || isReactHookCall(statement.expression, BUILTIN_HOOK_NAMES, scopes)) && !isReactHookCall(statement.expression, EFFECT_HOOK_NAMES$1, scopes)) {
|
|
39822
40299
|
for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
|
|
39823
40300
|
return false;
|
|
39824
40301
|
}
|
|
39825
|
-
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
|
|
40302
|
+
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), scopes, eventHandlerReferenceNames);
|
|
39826
40303
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39827
|
-
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
|
|
39828
|
-
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
|
|
40304
|
+
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, scopes, eventHandlerReferenceNames);
|
|
40305
|
+
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
39829
40306
|
if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39830
40307
|
return consequentHasReturn || alternateHasReturn;
|
|
39831
40308
|
}
|
|
@@ -39833,7 +40310,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39833
40310
|
let hasReturn = false;
|
|
39834
40311
|
for (const switchCase of statement.cases ?? []) {
|
|
39835
40312
|
const caseScope = createBlockBindingScope(scope);
|
|
39836
|
-
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
|
|
40313
|
+
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
|
|
39837
40314
|
hasReturn = true;
|
|
39838
40315
|
if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
|
|
39839
40316
|
}
|
|
@@ -39841,25 +40318,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39841
40318
|
return hasReturn;
|
|
39842
40319
|
}
|
|
39843
40320
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39844
|
-
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
|
|
39845
|
-
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
|
|
39846
|
-
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
|
|
40321
|
+
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, scopes, eventHandlerReferenceNames);
|
|
40322
|
+
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
40323
|
+
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
39847
40324
|
return blockHasReturn || handlerHasReturn || finalizerHasReturn;
|
|
39848
40325
|
}
|
|
39849
40326
|
if (isNodeOfType(statement, "CatchClause")) {
|
|
39850
40327
|
const catchScope = createBlockBindingScope(scope);
|
|
39851
40328
|
addPatternBindings(statement.param, catchScope);
|
|
39852
|
-
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
|
|
40329
|
+
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
|
|
39853
40330
|
}
|
|
39854
40331
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39855
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40332
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39856
40333
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39857
40334
|
return bodyHasReturn;
|
|
39858
40335
|
}
|
|
39859
40336
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39860
40337
|
const loopScope = createBlockBindingScope(scope);
|
|
39861
40338
|
if (statement.init) addDeclarationBindings(statement.init, loopScope);
|
|
39862
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40339
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39863
40340
|
if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
|
|
39864
40341
|
if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
|
|
39865
40342
|
if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
|
|
@@ -39869,22 +40346,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39869
40346
|
const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
|
|
39870
40347
|
const loopScope = createBlockBindingScope(scope);
|
|
39871
40348
|
if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
|
|
39872
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40349
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39873
40350
|
addNames(names, rightNames);
|
|
39874
40351
|
return true;
|
|
39875
40352
|
}
|
|
39876
|
-
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40353
|
+
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39877
40354
|
if (isNodeOfType(statement, "WithStatement")) {
|
|
39878
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40355
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39879
40356
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
|
|
39880
40357
|
return bodyHasReturn;
|
|
39881
40358
|
}
|
|
39882
40359
|
return false;
|
|
39883
40360
|
};
|
|
39884
|
-
const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40361
|
+
const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39885
40362
|
const names = /* @__PURE__ */ new Set();
|
|
39886
40363
|
if (!isNodeOfType(componentBody, "BlockStatement")) return names;
|
|
39887
|
-
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
|
|
40364
|
+
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
|
|
39888
40365
|
return names;
|
|
39889
40366
|
};
|
|
39890
40367
|
//#endregion
|
|
@@ -39907,42 +40384,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
|
|
|
39907
40384
|
};
|
|
39908
40385
|
//#endregion
|
|
39909
40386
|
//#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
|
|
39910
|
-
const
|
|
39911
|
-
|
|
39912
|
-
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
39913
|
-
if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
|
|
39914
|
-
return null;
|
|
39915
|
-
};
|
|
39916
|
-
const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
|
|
39917
|
-
if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
|
|
40387
|
+
const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
|
|
40388
|
+
if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
|
|
39918
40389
|
if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
|
|
39919
40390
|
const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
|
|
39920
40391
|
return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
|
|
39921
40392
|
};
|
|
39922
|
-
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
|
|
40393
|
+
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
|
|
39923
40394
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
39924
40395
|
for (const property of node.properties ?? []) {
|
|
39925
40396
|
if (!isNodeOfType(property, "Property")) continue;
|
|
39926
40397
|
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
39927
40398
|
if (!propertyName) continue;
|
|
39928
|
-
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
40399
|
+
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
|
|
39929
40400
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
39930
40401
|
}
|
|
39931
40402
|
};
|
|
39932
|
-
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40403
|
+
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39933
40404
|
if (!isNodeOfType(statement, "VariableDeclaration")) return;
|
|
39934
40405
|
const declarationScope = getVariableDeclarationScope(statement, scope);
|
|
39935
40406
|
for (const declarator of statement.declarations ?? []) {
|
|
39936
40407
|
const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
|
|
39937
40408
|
if (!declarator.init) continue;
|
|
39938
|
-
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
|
|
40409
|
+
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39939
40410
|
for (const declaredBindingName of declaredBindingNames) {
|
|
39940
40411
|
if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
|
|
39941
|
-
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
|
|
40412
|
+
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39942
40413
|
}
|
|
39943
40414
|
}
|
|
39944
40415
|
};
|
|
39945
|
-
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40416
|
+
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39946
40417
|
if (isNodeOfType(statement, "FunctionDeclaration")) {
|
|
39947
40418
|
if (statement.id) {
|
|
39948
40419
|
const declaredBindingNames = addPatternBindings(statement.id, scope);
|
|
@@ -39951,61 +40422,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
|
|
|
39951
40422
|
return;
|
|
39952
40423
|
}
|
|
39953
40424
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
39954
|
-
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40425
|
+
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
39955
40426
|
return;
|
|
39956
40427
|
}
|
|
39957
40428
|
if (isNodeOfType(statement, "BlockStatement")) {
|
|
39958
|
-
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40429
|
+
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
39959
40430
|
return;
|
|
39960
40431
|
}
|
|
39961
40432
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39962
|
-
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
|
|
39963
|
-
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
|
|
40433
|
+
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope, scopes);
|
|
40434
|
+
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope, scopes);
|
|
39964
40435
|
return;
|
|
39965
40436
|
}
|
|
39966
40437
|
if (isNodeOfType(statement, "SwitchStatement")) {
|
|
39967
|
-
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40438
|
+
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
39968
40439
|
return;
|
|
39969
40440
|
}
|
|
39970
40441
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39971
|
-
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
|
|
40442
|
+
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
|
|
39972
40443
|
if (statement.handler) {
|
|
39973
40444
|
const catchScope = createBlockBindingScope(scope);
|
|
39974
40445
|
addPatternBindings(statement.handler.param, catchScope);
|
|
39975
|
-
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
|
|
40446
|
+
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
|
|
39976
40447
|
}
|
|
39977
|
-
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
|
|
40448
|
+
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
|
|
39978
40449
|
return;
|
|
39979
40450
|
}
|
|
39980
40451
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39981
40452
|
const loopScope = createBlockBindingScope(scope);
|
|
39982
|
-
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
|
|
39983
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40453
|
+
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope, scopes);
|
|
40454
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
39984
40455
|
return;
|
|
39985
40456
|
}
|
|
39986
40457
|
if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
|
|
39987
40458
|
const loopScope = createBlockBindingScope(scope);
|
|
39988
|
-
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
|
|
40459
|
+
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
|
|
39989
40460
|
else addPatternBindings(statement.left, loopScope);
|
|
39990
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40461
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
39991
40462
|
return;
|
|
39992
40463
|
}
|
|
39993
40464
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39994
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40465
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39995
40466
|
return;
|
|
39996
40467
|
}
|
|
39997
|
-
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40468
|
+
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39998
40469
|
};
|
|
39999
|
-
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
|
|
40000
|
-
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40470
|
+
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope, scopes) => {
|
|
40471
|
+
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
40001
40472
|
};
|
|
40002
|
-
const collectFunctionLikeLocalNames = (componentBody) => {
|
|
40473
|
+
const collectFunctionLikeLocalNames = (componentBody, scopes) => {
|
|
40003
40474
|
const functionLikeLocalNames = /* @__PURE__ */ new Set();
|
|
40004
40475
|
if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
|
|
40005
40476
|
let previousSize = -1;
|
|
40006
40477
|
while (previousSize !== functionLikeLocalNames.size) {
|
|
40007
40478
|
previousSize = functionLikeLocalNames.size;
|
|
40008
|
-
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
|
|
40479
|
+
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
|
|
40009
40480
|
}
|
|
40010
40481
|
return functionLikeLocalNames;
|
|
40011
40482
|
};
|
|
@@ -40045,17 +40516,17 @@ const noEventTriggerState = defineRule({
|
|
|
40045
40516
|
create: (context) => {
|
|
40046
40517
|
const checkComponent = (componentBody) => {
|
|
40047
40518
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
40048
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
40519
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
40049
40520
|
if (useStateBindings.length === 0) return;
|
|
40050
40521
|
const analysis = getProgramAnalysis(componentBody);
|
|
40051
40522
|
if (!analysis) return;
|
|
40052
40523
|
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
40053
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
40524
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
40054
40525
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
40055
|
-
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
40526
|
+
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
|
|
40056
40527
|
walkAst(componentBody, (effectCall) => {
|
|
40057
40528
|
if (!isNodeOfType(effectCall, "CallExpression")) return;
|
|
40058
|
-
if (!
|
|
40529
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
40059
40530
|
if ((effectCall.arguments?.length ?? 0) < 2) return;
|
|
40060
40531
|
const depsNode = effectCall.arguments[1];
|
|
40061
40532
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -40120,10 +40591,17 @@ const isShadowedByLocalBinding = (identifier) => {
|
|
|
40120
40591
|
};
|
|
40121
40592
|
const isRealFetchCall = (node) => {
|
|
40122
40593
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
40123
|
-
|
|
40124
|
-
|
|
40594
|
+
const callee = stripParenExpression(node.callee);
|
|
40595
|
+
if (isNodeOfType(callee, "Identifier") && FETCH_CALLEE_NAMES.has(callee.name)) return !isShadowedByLocalBinding(callee);
|
|
40596
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
40597
|
+
const receiver = stripParenExpression(callee.object);
|
|
40598
|
+
return isNodeOfType(receiver, "Identifier") && FETCH_MEMBER_OBJECTS.has(receiver.name) && !isShadowedByLocalBinding(receiver);
|
|
40599
|
+
};
|
|
40600
|
+
const isXmlHttpRequestConstruction = (node) => {
|
|
40601
|
+
if (!isNodeOfType(node, "NewExpression")) return false;
|
|
40602
|
+
const callee = stripParenExpression(node.callee);
|
|
40603
|
+
return isNodeOfType(callee, "Identifier") && callee.name === "XMLHttpRequest";
|
|
40125
40604
|
};
|
|
40126
|
-
const isXmlHttpRequestConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "XMLHttpRequest";
|
|
40127
40605
|
const isNetworkRequest = (node) => isRealFetchCall(node) || isXmlHttpRequestConstruction(node);
|
|
40128
40606
|
const resolveLocalFunction = (expression, context) => {
|
|
40129
40607
|
if (!expression) return null;
|
|
@@ -40439,6 +40917,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
40439
40917
|
"scrollHeight"
|
|
40440
40918
|
]);
|
|
40441
40919
|
const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
|
|
40920
|
+
const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
|
|
40921
|
+
"blur",
|
|
40922
|
+
"focus",
|
|
40923
|
+
"restoreSelection",
|
|
40924
|
+
"scroll",
|
|
40925
|
+
"scrollBy",
|
|
40926
|
+
"scrollIntoView",
|
|
40927
|
+
"scrollTo",
|
|
40928
|
+
"setRangeText",
|
|
40929
|
+
"setSelectionRange"
|
|
40930
|
+
]);
|
|
40442
40931
|
const subtreeReadsDomMeasurement = (root) => {
|
|
40443
40932
|
if (!root) return false;
|
|
40444
40933
|
let found = false;
|
|
@@ -40457,29 +40946,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
40457
40946
|
});
|
|
40458
40947
|
return found;
|
|
40459
40948
|
};
|
|
40460
|
-
const
|
|
40949
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
40461
40950
|
const names = /* @__PURE__ */ new Set();
|
|
40462
40951
|
walkAst(program, (child) => {
|
|
40463
40952
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
40464
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40953
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
40465
40954
|
return;
|
|
40466
40955
|
}
|
|
40467
40956
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
40468
40957
|
let functionValue = child.init;
|
|
40469
40958
|
if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
|
|
40470
|
-
if (functionValue && isFunctionLike$1(functionValue) &&
|
|
40959
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40471
40960
|
});
|
|
40472
40961
|
return names;
|
|
40473
40962
|
};
|
|
40474
|
-
const
|
|
40475
|
-
|
|
40963
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40964
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40965
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
40476
40966
|
let found = false;
|
|
40477
40967
|
walkAst(root, (child) => {
|
|
40478
40968
|
if (found) return false;
|
|
40969
|
+
if (child !== root && isFunctionLike$1(child)) return false;
|
|
40970
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40971
|
+
const callee = stripParenExpression(child.callee);
|
|
40972
|
+
const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
40973
|
+
if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
|
|
40974
|
+
found = true;
|
|
40975
|
+
return false;
|
|
40976
|
+
}
|
|
40977
|
+
});
|
|
40978
|
+
return found;
|
|
40979
|
+
};
|
|
40980
|
+
const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
|
|
40981
|
+
const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
|
|
40982
|
+
if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
|
|
40983
|
+
let found = false;
|
|
40984
|
+
walkAst(root, (child) => {
|
|
40985
|
+
if (found) return false;
|
|
40986
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
40479
40987
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
40480
40988
|
});
|
|
40481
40989
|
return found;
|
|
40482
40990
|
};
|
|
40991
|
+
const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
|
|
40992
|
+
let statement = call;
|
|
40993
|
+
let parent = statement.parent;
|
|
40994
|
+
while (parent) {
|
|
40995
|
+
const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
|
|
40996
|
+
if (statements) {
|
|
40997
|
+
const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
|
|
40998
|
+
if (statementIndex >= 0) {
|
|
40999
|
+
const nextStatement = statements[statementIndex + 1];
|
|
41000
|
+
return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
|
|
41001
|
+
}
|
|
41002
|
+
}
|
|
41003
|
+
if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
41004
|
+
statement = parent;
|
|
41005
|
+
parent = parent.parent;
|
|
41006
|
+
}
|
|
41007
|
+
return false;
|
|
41008
|
+
};
|
|
40483
41009
|
const isInsideStartViewTransition = (node) => {
|
|
40484
41010
|
let cursor = node.parent;
|
|
40485
41011
|
while (cursor) {
|
|
@@ -40520,11 +41046,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
40520
41046
|
};
|
|
40521
41047
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
40522
41048
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
41049
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
40523
41050
|
let exempt = false;
|
|
40524
41051
|
walkAst(program, (child) => {
|
|
40525
41052
|
if (exempt) return false;
|
|
40526
41053
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
40527
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
41054
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
40528
41055
|
exempt = true;
|
|
40529
41056
|
return false;
|
|
40530
41057
|
}
|
|
@@ -41835,7 +42362,7 @@ const noInitializeState = defineRule({
|
|
|
41835
42362
|
tags: ["test-noise"],
|
|
41836
42363
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
41837
42364
|
create: (context) => ({ CallExpression(node) {
|
|
41838
|
-
if (!
|
|
42365
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
41839
42366
|
const dependencies = node.arguments?.[1];
|
|
41840
42367
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41841
42368
|
const analysis = getProgramAnalysis(node);
|
|
@@ -42198,7 +42725,8 @@ const noJsxElementType = defineRule({
|
|
|
42198
42725
|
create: (context) => {
|
|
42199
42726
|
let isJsxImported = false;
|
|
42200
42727
|
const flaggedAnnotations = [];
|
|
42201
|
-
const
|
|
42728
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42729
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
42202
42730
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
42203
42731
|
if (!typeAnnotation) return;
|
|
42204
42732
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -42208,19 +42736,16 @@ const noJsxElementType = defineRule({
|
|
|
42208
42736
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
42209
42737
|
},
|
|
42210
42738
|
FunctionDeclaration(node) {
|
|
42211
|
-
|
|
42739
|
+
collectComponentReturnType(node, node.returnType);
|
|
42212
42740
|
},
|
|
42213
42741
|
ArrowFunctionExpression(node) {
|
|
42214
|
-
|
|
42742
|
+
collectComponentReturnType(node, node.returnType);
|
|
42215
42743
|
},
|
|
42216
42744
|
FunctionExpression(node) {
|
|
42217
|
-
|
|
42745
|
+
collectComponentReturnType(node, node.returnType);
|
|
42218
42746
|
},
|
|
42219
42747
|
TSDeclareFunction(node) {
|
|
42220
|
-
|
|
42221
|
-
},
|
|
42222
|
-
TSMethodSignature(node) {
|
|
42223
|
-
checkReturnType(node.returnType);
|
|
42748
|
+
collectComponentReturnType(node, node.returnType);
|
|
42224
42749
|
},
|
|
42225
42750
|
"Program:exit"() {
|
|
42226
42751
|
if (isJsxImported) return;
|
|
@@ -43449,7 +43974,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43449
43974
|
const setterElement = elements[1];
|
|
43450
43975
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43451
43976
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43452
|
-
if (!
|
|
43977
|
+
if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
|
|
43453
43978
|
const initializer = declarator.init.arguments?.[0];
|
|
43454
43979
|
if (!initializer) continue;
|
|
43455
43980
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43467,7 +43992,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43467
43992
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43468
43993
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43469
43994
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43470
|
-
if (!
|
|
43995
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
43471
43996
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43472
43997
|
const depsNode = effectCall.arguments[1];
|
|
43473
43998
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -43876,7 +44401,7 @@ const noMultiComp = defineRule({
|
|
|
43876
44401
|
});
|
|
43877
44402
|
//#endregion
|
|
43878
44403
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
43879
|
-
const collectUseRefBindingNames = (componentBody) => {
|
|
44404
|
+
const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
43880
44405
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
43881
44406
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
43882
44407
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -43884,7 +44409,7 @@ const collectUseRefBindingNames = (componentBody) => {
|
|
|
43884
44409
|
for (const declarator of statement.declarations ?? []) {
|
|
43885
44410
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
43886
44411
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43887
|
-
if (!
|
|
44412
|
+
if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
|
|
43888
44413
|
useRefBindings.add(declarator.id.name);
|
|
43889
44414
|
}
|
|
43890
44415
|
}
|
|
@@ -43919,12 +44444,12 @@ const noMutableInDeps = defineRule({
|
|
|
43919
44444
|
create: (context) => {
|
|
43920
44445
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
43921
44446
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
43922
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44447
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
|
|
43923
44448
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
43924
44449
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
43925
44450
|
walkAst(componentBody, (child) => {
|
|
43926
44451
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
43927
|
-
if (!
|
|
44452
|
+
if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
43928
44453
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
43929
44454
|
const depsNode = child.arguments[1];
|
|
43930
44455
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -45754,6 +46279,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
45754
46279
|
"useMatchMedia",
|
|
45755
46280
|
"useMediaJobProgress",
|
|
45756
46281
|
"useMediaQuery",
|
|
46282
|
+
"useMediaQueryState",
|
|
45757
46283
|
"useResizeObserver",
|
|
45758
46284
|
"useVisibility",
|
|
45759
46285
|
"useWindowSize"
|
|
@@ -45790,9 +46316,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
45790
46316
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
45791
46317
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
45792
46318
|
};
|
|
45793
|
-
const
|
|
46319
|
+
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
46320
|
+
let hookFunction = resolveToFunction(ref);
|
|
46321
|
+
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46322
|
+
const definitionNode = definition.node;
|
|
46323
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
46324
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
46325
|
+
if (!isNodeOfType(initializer, "CallExpression")) continue;
|
|
46326
|
+
const callee = stripParenExpression(initializer.callee);
|
|
46327
|
+
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
46328
|
+
const calleeReference = getRef(analysis, callee);
|
|
46329
|
+
if (!calleeReference) continue;
|
|
46330
|
+
hookFunction = resolveToFunction(calleeReference);
|
|
46331
|
+
if (hookFunction) break;
|
|
46332
|
+
}
|
|
46333
|
+
if (!hookFunction) return null;
|
|
46334
|
+
const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46335
|
+
if (returnedReferences.length === 0) return null;
|
|
46336
|
+
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46337
|
+
};
|
|
46338
|
+
const isExternalSubscriptionHookRef = (analysis, ref) => {
|
|
45794
46339
|
const identifier = ref.identifier;
|
|
45795
46340
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46341
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46342
|
+
if (localHookProof !== null) return localHookProof;
|
|
45796
46343
|
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45797
46344
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
45798
46345
|
const node = def.node;
|
|
@@ -45815,16 +46362,10 @@ const noPassDataToParent = defineRule({
|
|
|
45815
46362
|
tags: ["test-noise"],
|
|
45816
46363
|
recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
|
|
45817
46364
|
create: (context) => {
|
|
45818
|
-
const isReactUseRefCall = (node) =>
|
|
45819
|
-
|
|
45820
|
-
allowUnboundBareCalls: true
|
|
45821
|
-
});
|
|
45822
|
-
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
45823
|
-
allowGlobalReactNamespace: true,
|
|
45824
|
-
allowUnboundBareCalls: true
|
|
45825
|
-
});
|
|
46365
|
+
const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
|
|
46366
|
+
const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
|
|
45826
46367
|
return { CallExpression(node) {
|
|
45827
|
-
if (!
|
|
46368
|
+
if (!isReactUseEffectCall(node)) return;
|
|
45828
46369
|
const analysis = getProgramAnalysis(node);
|
|
45829
46370
|
if (!analysis) return;
|
|
45830
46371
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -45876,11 +46417,11 @@ const noPassDataToParent = defineRule({
|
|
|
45876
46417
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
45877
46418
|
}
|
|
45878
46419
|
return getDownstreamRefs(analysis, argument);
|
|
45879
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46420
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
45880
46421
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45881
46422
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
45882
46423
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
45883
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46424
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
45884
46425
|
if (isProp(analysis, argRef)) return false;
|
|
45885
46426
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
45886
46427
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -46111,7 +46652,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
46111
46652
|
tags: ["test-noise"],
|
|
46112
46653
|
recommendation: "Move the state up to the parent (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes",
|
|
46113
46654
|
create: (context) => ({ CallExpression(node) {
|
|
46114
|
-
if (!
|
|
46655
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
46115
46656
|
const analysis = getProgramAnalysis(node);
|
|
46116
46657
|
if (!analysis) return;
|
|
46117
46658
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -46526,7 +47067,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
46526
47067
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
46527
47068
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
46528
47069
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
46529
|
-
const calleeName = getCalleeName$
|
|
47070
|
+
const calleeName = getCalleeName$1(binding.initializer);
|
|
46530
47071
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
46531
47072
|
}
|
|
46532
47073
|
return false;
|
|
@@ -46548,7 +47089,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
46548
47089
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
46549
47090
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
46550
47091
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
46551
|
-
if (getCalleeName$
|
|
47092
|
+
if (getCalleeName$1(binding.initializer) !== "useRef") return null;
|
|
46552
47093
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
46553
47094
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
46554
47095
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -46580,7 +47121,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
46580
47121
|
return {
|
|
46581
47122
|
...propStackTracker.visitors,
|
|
46582
47123
|
CallExpression(node) {
|
|
46583
|
-
if (!
|
|
47124
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
46584
47125
|
const callback = getEffectCallback(node);
|
|
46585
47126
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
46586
47127
|
const depsNode = node.arguments[1];
|
|
@@ -48473,7 +49014,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
48473
49014
|
tags: ["test-noise"],
|
|
48474
49015
|
recommendation: "Pass the prop as `key` so React resets the component for you when the prop changes, instead of clearing every state value by hand in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes",
|
|
48475
49016
|
create: (context) => ({ CallExpression(node) {
|
|
48476
|
-
if (!
|
|
49017
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
48477
49018
|
const analysis = getProgramAnalysis(node);
|
|
48478
49019
|
if (!analysis) return;
|
|
48479
49020
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -48742,7 +49283,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
48742
49283
|
if (node.callee.property.name !== "handler") return false;
|
|
48743
49284
|
let currentNode = node.callee.object;
|
|
48744
49285
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
48745
|
-
const calleeName = getCalleeName$
|
|
49286
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
48746
49287
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
48747
49288
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
48748
49289
|
currentNode = currentNode.callee.object;
|
|
@@ -49243,7 +49784,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49243
49784
|
create: (context) => {
|
|
49244
49785
|
const checkFunctionScope = (functionBody) => {
|
|
49245
49786
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
49246
|
-
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49787
|
+
const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
|
|
49247
49788
|
if (useStateBindings.length === 0) return;
|
|
49248
49789
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
49249
49790
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -49252,7 +49793,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49252
49793
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
49253
49794
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
49254
49795
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
49255
|
-
if (!
|
|
49796
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
49256
49797
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
49257
49798
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
49258
49799
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -49338,7 +49879,7 @@ const noSetStateInRender = defineRule({
|
|
|
49338
49879
|
create: (context) => {
|
|
49339
49880
|
const checkComponent = (componentBody) => {
|
|
49340
49881
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
49341
|
-
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49882
|
+
const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
|
|
49342
49883
|
if (setterNames.size === 0) return;
|
|
49343
49884
|
for (const statement of componentBody.body ?? []) {
|
|
49344
49885
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -49587,10 +50128,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
49587
50128
|
});
|
|
49588
50129
|
return facts;
|
|
49589
50130
|
};
|
|
49590
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
50131
|
+
const isEffectCallbackFunction = (functionNode, scopes) => {
|
|
49591
50132
|
const parent = functionNode.parent;
|
|
49592
50133
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
49593
|
-
return
|
|
50134
|
+
return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
|
|
49594
50135
|
};
|
|
49595
50136
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
49596
50137
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -49609,24 +50150,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
49609
50150
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49610
50151
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
49611
50152
|
};
|
|
49612
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50153
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
|
|
49613
50154
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49614
50155
|
if (cleanupBindingName === null) return false;
|
|
49615
50156
|
let isReturnedFromEffect = false;
|
|
49616
50157
|
walkAst(ownerScope, (child) => {
|
|
49617
50158
|
if (isReturnedFromEffect) return false;
|
|
49618
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50159
|
+
if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
49619
50160
|
const effectCallback = getEffectCallback(child);
|
|
49620
50161
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
49621
50162
|
});
|
|
49622
50163
|
return isReturnedFromEffect;
|
|
49623
50164
|
};
|
|
49624
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50165
|
+
const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
|
|
49625
50166
|
let functionNode = findEnclosingFunction$1(node);
|
|
49626
50167
|
while (functionNode) {
|
|
49627
50168
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
49628
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
49629
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
50169
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50170
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
|
|
49630
50171
|
functionNode = outerFunction;
|
|
49631
50172
|
}
|
|
49632
50173
|
return false;
|
|
@@ -49662,10 +50203,10 @@ const noStaleTimerRef = defineRule({
|
|
|
49662
50203
|
if (isShadowedTimerGlobal(node)) return;
|
|
49663
50204
|
const { clearCalleeName, refName } = clearCall;
|
|
49664
50205
|
const refBinding = findVariableInitializer(node, refName);
|
|
49665
|
-
if (!refBinding?.initializer || !
|
|
50206
|
+
if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
|
|
49666
50207
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
49667
50208
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
49668
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50209
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
|
|
49669
50210
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
49670
50211
|
context.report({
|
|
49671
50212
|
node,
|
|
@@ -55469,7 +56010,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
55469
56010
|
allReadsAreInSubHandlers = false;
|
|
55470
56011
|
return;
|
|
55471
56012
|
}
|
|
55472
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
56013
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
|
|
55473
56014
|
});
|
|
55474
56015
|
return {
|
|
55475
56016
|
hasAnyRead,
|
|
@@ -55492,7 +56033,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
55492
56033
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
55493
56034
|
const effectCall = statement.expression;
|
|
55494
56035
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55495
|
-
if (!
|
|
56036
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
55496
56037
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55497
56038
|
const depsNode = effectCall.arguments[1];
|
|
55498
56039
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -55524,11 +56065,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
55524
56065
|
});
|
|
55525
56066
|
//#endregion
|
|
55526
56067
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
55527
|
-
const findUseEffectsInComponent = (componentBody) => {
|
|
56068
|
+
const findUseEffectsInComponent = (componentBody, scopes) => {
|
|
55528
56069
|
const effectCalls = [];
|
|
55529
56070
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
55530
56071
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
55531
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56072
|
+
if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
|
|
55532
56073
|
});
|
|
55533
56074
|
return effectCalls;
|
|
55534
56075
|
};
|
|
@@ -55731,7 +56272,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55731
56272
|
};
|
|
55732
56273
|
const checkComponent = (componentBody) => {
|
|
55733
56274
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
55734
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56275
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
55735
56276
|
if (useStateBindings.length === 0) return;
|
|
55736
56277
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
55737
56278
|
for (const binding of useStateBindings) {
|
|
@@ -55744,7 +56285,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55744
56285
|
}
|
|
55745
56286
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
55746
56287
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
55747
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56288
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55748
56289
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55749
56290
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55750
56291
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55786,7 +56327,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55786
56327
|
})).filter((candidate) => candidate.storeName !== null);
|
|
55787
56328
|
if (snapshotBindings.length === 0) return;
|
|
55788
56329
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
55789
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56330
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55790
56331
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55791
56332
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55792
56333
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55883,7 +56424,7 @@ const preferUseReducer = defineRule({
|
|
|
55883
56424
|
create: (context) => {
|
|
55884
56425
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
55885
56426
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
55886
|
-
const bindings = collectUseStateBindings(body);
|
|
56427
|
+
const bindings = collectUseStateBindings(body, context.scopes);
|
|
55887
56428
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
55888
56429
|
if (setterNames.size < 5) return;
|
|
55889
56430
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -56099,7 +56640,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
56099
56640
|
]);
|
|
56100
56641
|
const isQueryCacheSourceCall = (initializer) => {
|
|
56101
56642
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
56102
|
-
const hookName = getCalleeName$
|
|
56643
|
+
const hookName = getCalleeName$1(initializer);
|
|
56103
56644
|
if (!hookName) return false;
|
|
56104
56645
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
56105
56646
|
};
|
|
@@ -56231,7 +56772,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
56231
56772
|
},
|
|
56232
56773
|
CallExpression(node) {
|
|
56233
56774
|
if (!hasQueryReadUsage) {
|
|
56234
|
-
const callName = getCalleeName$
|
|
56775
|
+
const callName = getCalleeName$1(node);
|
|
56235
56776
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
56236
56777
|
}
|
|
56237
56778
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -56724,26 +57265,30 @@ const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
|
56724
57265
|
["useCallback", "This `useCallback` is dead weight, since React Compiler already caches every function here. Delete it."],
|
|
56725
57266
|
["memo", "This `memo()` is dead weight, since React Compiler already caches the component's output. Delete it."]
|
|
56726
57267
|
]);
|
|
56727
|
-
const resolveReactApiNameForIdentifier = (callee) => {
|
|
57268
|
+
const resolveReactApiNameForIdentifier = (callee, context) => {
|
|
56728
57269
|
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
57270
|
+
if (context.scopes.symbolFor(callee)?.kind !== "import") return null;
|
|
56729
57271
|
const importedName = getImportedNameFromModule(callee, callee.name, "react");
|
|
56730
57272
|
if (importedName && REMOVAL_MESSAGE_BY_REACT_API_NAME.has(importedName)) return importedName;
|
|
56731
57273
|
return null;
|
|
56732
57274
|
};
|
|
56733
|
-
const resolveReactApiNameForMemberExpression = (callee) => {
|
|
57275
|
+
const resolveReactApiNameForMemberExpression = (callee, context) => {
|
|
56734
57276
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
56735
57277
|
if (callee.computed) return null;
|
|
56736
|
-
const namespaceIdentifier = callee.object;
|
|
57278
|
+
const namespaceIdentifier = stripParenExpression(callee.object);
|
|
56737
57279
|
const propertyIdentifier = callee.property;
|
|
56738
57280
|
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return null;
|
|
56739
57281
|
if (!isNodeOfType(propertyIdentifier, "Identifier")) return null;
|
|
56740
57282
|
if (!REMOVAL_MESSAGE_BY_REACT_API_NAME.has(propertyIdentifier.name)) return null;
|
|
56741
57283
|
const namespaceName = namespaceIdentifier.name;
|
|
56742
|
-
if (
|
|
56743
|
-
if (
|
|
57284
|
+
if (context.scopes.symbolFor(namespaceIdentifier)?.kind === "import" && isImportedFromModule(namespaceIdentifier, namespaceName, "react")) return propertyIdentifier.name;
|
|
57285
|
+
if (isCanonicalReactNamespaceName(namespaceName) && context.scopes.isGlobalReference(namespaceIdentifier)) return propertyIdentifier.name;
|
|
56744
57286
|
return null;
|
|
56745
57287
|
};
|
|
56746
|
-
const resolveReactApiNameForCallee = (callee) =>
|
|
57288
|
+
const resolveReactApiNameForCallee = (callee, context) => {
|
|
57289
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
57290
|
+
return resolveReactApiNameForIdentifier(unwrappedCallee, context) ?? resolveReactApiNameForMemberExpression(unwrappedCallee, context);
|
|
57291
|
+
};
|
|
56747
57292
|
const isNullishComparatorArgument = (argumentNode) => isNodeOfType(argumentNode, "Identifier") && argumentNode.name === "undefined" || isNodeOfType(argumentNode, "Literal") && argumentNode.value === null;
|
|
56748
57293
|
const COMPILER_INFERABLE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
56749
57294
|
const calleeTrailingName = (callee) => {
|
|
@@ -56769,7 +57314,7 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
56769
57314
|
requires: ["react-compiler"],
|
|
56770
57315
|
recommendation: "Delete the `useMemo` / `useCallback` / `memo` call and use the plain value or component. React Compiler caches it for you.",
|
|
56771
57316
|
create: (context) => ({ CallExpression(node) {
|
|
56772
|
-
const apiName = resolveReactApiNameForCallee(node.callee);
|
|
57317
|
+
const apiName = resolveReactApiNameForCallee(node.callee, context);
|
|
56773
57318
|
if (!apiName) return;
|
|
56774
57319
|
if (apiName === "memo") {
|
|
56775
57320
|
const comparatorArgument = node.arguments?.[1];
|
|
@@ -57894,6 +58439,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
57894
58439
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
57895
58440
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
57896
58441
|
const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
|
|
58442
|
+
const FILE_READER_READ_METHOD_NAMES = new Set([
|
|
58443
|
+
"readAsArrayBuffer",
|
|
58444
|
+
"readAsBinaryString",
|
|
58445
|
+
"readAsDataURL",
|
|
58446
|
+
"readAsText"
|
|
58447
|
+
]);
|
|
58448
|
+
const isGlobalFileReaderConstruction = (expression, context) => {
|
|
58449
|
+
if (!expression) return false;
|
|
58450
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58451
|
+
if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
58452
|
+
return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
|
|
58453
|
+
};
|
|
58454
|
+
const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
|
|
58455
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58456
|
+
let latestValue = null;
|
|
58457
|
+
let latestStart = null;
|
|
58458
|
+
if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
|
|
58459
|
+
latestValue = readerSymbol.initializer;
|
|
58460
|
+
latestStart = readerSymbol.declarationNode.range[0];
|
|
58461
|
+
}
|
|
58462
|
+
for (const reference of readerSymbol.references) {
|
|
58463
|
+
if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
|
|
58464
|
+
const assignment = reference.identifier.parent;
|
|
58465
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
|
|
58466
|
+
latestValue = assignment.right;
|
|
58467
|
+
latestStart = reference.identifier.range[0];
|
|
58468
|
+
}
|
|
58469
|
+
return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
|
|
58470
|
+
};
|
|
58471
|
+
const resolveLoadingCompletionFunction = (expression, context) => {
|
|
58472
|
+
const directFunction = resolveExactLocalFunction(expression, context.scopes);
|
|
58473
|
+
if (directFunction) return directFunction;
|
|
58474
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58475
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
58476
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
58477
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
58478
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
|
|
58479
|
+
const callback = initializer.arguments?.[0];
|
|
58480
|
+
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
58481
|
+
};
|
|
58482
|
+
const isSetterBooleanCall = (node, setterSymbol, value, context) => {
|
|
58483
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
58484
|
+
const callee = stripParenExpression(node.callee);
|
|
58485
|
+
const argument = node.arguments?.[0];
|
|
58486
|
+
const unwrappedArgument = argument ? stripParenExpression(argument) : null;
|
|
58487
|
+
return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
|
|
58488
|
+
};
|
|
58489
|
+
const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
|
|
58490
|
+
if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
|
|
58491
|
+
visitedFunctions.add(functionNode);
|
|
58492
|
+
let didClearLoadingState = false;
|
|
58493
|
+
walkAst(functionNode.body, (child) => {
|
|
58494
|
+
if (didClearLoadingState) return false;
|
|
58495
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
58496
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58497
|
+
if (isSetterBooleanCall(child, setterSymbol, false, context)) {
|
|
58498
|
+
didClearLoadingState = true;
|
|
58499
|
+
return false;
|
|
58500
|
+
}
|
|
58501
|
+
const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
|
|
58502
|
+
if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
|
|
58503
|
+
didClearLoadingState = true;
|
|
58504
|
+
return false;
|
|
58505
|
+
}
|
|
58506
|
+
});
|
|
58507
|
+
return didClearLoadingState;
|
|
58508
|
+
};
|
|
58509
|
+
const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
|
|
58510
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58511
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return null;
|
|
58512
|
+
let callback = null;
|
|
58513
|
+
let callbackStart = originStart;
|
|
58514
|
+
walkAst(readFunction.body, (child) => {
|
|
58515
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58516
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=" || child.range[0] >= readCall.range[0] || child.range[0] <= callbackStart || !isNodeOfType(child.left, "MemberExpression") || getStaticPropertyName(child.left) !== propertyName) return;
|
|
58517
|
+
const receiver = stripParenExpression(child.left.object);
|
|
58518
|
+
if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
|
|
58519
|
+
callback = child.right;
|
|
58520
|
+
callbackStart = child.range[0];
|
|
58521
|
+
});
|
|
58522
|
+
return callback;
|
|
58523
|
+
};
|
|
58524
|
+
const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
|
|
58525
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58526
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return false;
|
|
58527
|
+
let didStartLoading = false;
|
|
58528
|
+
walkAst(readFunction.body, (child) => {
|
|
58529
|
+
if (didStartLoading) return false;
|
|
58530
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58531
|
+
if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
|
|
58532
|
+
if (isSetterBooleanCall(child, setterSymbol, true, context)) {
|
|
58533
|
+
didStartLoading = true;
|
|
58534
|
+
return false;
|
|
58535
|
+
}
|
|
58536
|
+
});
|
|
58537
|
+
return didStartLoading;
|
|
58538
|
+
};
|
|
58539
|
+
const setterTracksFileReader = (functionBody, setterSymbol, context) => {
|
|
58540
|
+
let didFindFileReaderLifecycle = false;
|
|
58541
|
+
walkAst(functionBody, (child) => {
|
|
58542
|
+
if (didFindFileReaderLifecycle) return false;
|
|
58543
|
+
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
|
|
58544
|
+
const receiver = stripParenExpression(child.callee.object);
|
|
58545
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
58546
|
+
const readerSymbol = context.scopes.symbolFor(receiver);
|
|
58547
|
+
if (!readerSymbol) return;
|
|
58548
|
+
const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
|
|
58549
|
+
if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
|
|
58550
|
+
const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
|
|
58551
|
+
const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
|
|
58552
|
+
const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
|
|
58553
|
+
const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
|
|
58554
|
+
if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
|
|
58555
|
+
didFindFileReaderLifecycle = true;
|
|
58556
|
+
return false;
|
|
58557
|
+
}
|
|
58558
|
+
});
|
|
58559
|
+
return didFindFileReaderLifecycle;
|
|
58560
|
+
};
|
|
57897
58561
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
57898
58562
|
let found = false;
|
|
57899
58563
|
walkAst(fnBody, (child) => {
|
|
@@ -58067,6 +58731,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58067
58731
|
const fnBody = enclosingFunctionBody(node);
|
|
58068
58732
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
58069
58733
|
if (fnBody && setterName) {
|
|
58734
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58735
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
58070
58736
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
58071
58737
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
58072
58738
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -58398,7 +59064,7 @@ const rerenderDependencies = defineRule({
|
|
|
58398
59064
|
severity: "error",
|
|
58399
59065
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
58400
59066
|
create: (context) => ({ CallExpression(node) {
|
|
58401
|
-
if (!
|
|
59067
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
58402
59068
|
const depsNode = node.arguments[1];
|
|
58403
59069
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
58404
59070
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -58653,7 +59319,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
58653
59319
|
category: "Performance",
|
|
58654
59320
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
58655
59321
|
create: (context) => ({ CallExpression(node) {
|
|
58656
|
-
if (!
|
|
59322
|
+
if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
|
|
58657
59323
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
58658
59324
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
58659
59325
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -58717,7 +59383,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
58717
59383
|
category: "Performance",
|
|
58718
59384
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
58719
59385
|
create: (context) => ({ CallExpression(node) {
|
|
58720
|
-
if (!
|
|
59386
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
58721
59387
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
58722
59388
|
if (!initializer) return;
|
|
58723
59389
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59480,11 +60146,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
59480
60146
|
}
|
|
59481
60147
|
return false;
|
|
59482
60148
|
};
|
|
59483
|
-
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60149
|
+
const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
59484
60150
|
const effectInfos = [];
|
|
59485
60151
|
walkAst(componentBody, (child) => {
|
|
59486
60152
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59487
|
-
if (!
|
|
60153
|
+
if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
59488
60154
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
59489
60155
|
for (const argument of child.arguments ?? []) {
|
|
59490
60156
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -59540,13 +60206,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
|
59540
60206
|
});
|
|
59541
60207
|
return effectInfos;
|
|
59542
60208
|
};
|
|
59543
|
-
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60209
|
+
const collectCustomHookArgumentNames = (componentBody, scopes) => {
|
|
59544
60210
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
59545
60211
|
walkAst(componentBody, (child) => {
|
|
59546
60212
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59547
60213
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
59548
60214
|
const calleeName = child.callee.name;
|
|
59549
60215
|
if (!isReactHookName(calleeName)) return;
|
|
60216
|
+
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
59550
60217
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
59551
60218
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
59552
60219
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -59587,21 +60254,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
59587
60254
|
create: (context) => {
|
|
59588
60255
|
const checkComponent = (componentBody) => {
|
|
59589
60256
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
59590
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
60257
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
59591
60258
|
if (bindings.length === 0) return;
|
|
59592
60259
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
59593
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60260
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
59594
60261
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
59595
|
-
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60262
|
+
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
59596
60263
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
59597
60264
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
59598
60265
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
59599
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60266
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
|
|
59600
60267
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
59601
60268
|
for (const binding of bindings) if (effectInfos.some((effectInfo) => effectInfo.dependencyNames.has(binding.valueName) && effectInfo.synchronouslyCalledFunctionNames.has(binding.setterName) && !effectInfo.payloadReadNames.has(binding.valueName) && !effectInfo.nestedCallbackCalledFunctionNames.has(binding.setterName))) selfEchoValueNames.add(binding.valueName);
|
|
59602
60269
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
59603
60270
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
59604
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60271
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
59605
60272
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
59606
60273
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
59607
60274
|
walkAst(componentBody, (child) => {
|
|
@@ -67502,7 +68169,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
67502
68169
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
67503
68170
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
67504
68171
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
67505
|
-
const calleeName = getCalleeName$
|
|
68172
|
+
const calleeName = getCalleeName$1(argument);
|
|
67506
68173
|
if (!calleeName) continue;
|
|
67507
68174
|
if (isAuthGuardName(calleeName)) return true;
|
|
67508
68175
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -68142,7 +68809,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
68142
68809
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
68143
68810
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
68144
68811
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
68145
|
-
const calleeName = getCalleeName$
|
|
68812
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
68146
68813
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
68147
68814
|
result.isServerFnChain = true;
|
|
68148
68815
|
const optionsArgument = currentNode.arguments?.[0];
|