oxlint-plugin-react-doctor 0.7.9-dev.a6e4fb5 → 0.7.9-dev.ac392cb
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 +1030 -372
- 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;
|
|
@@ -40439,6 +40910,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
40439
40910
|
"scrollHeight"
|
|
40440
40911
|
]);
|
|
40441
40912
|
const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
|
|
40913
|
+
const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
|
|
40914
|
+
"blur",
|
|
40915
|
+
"focus",
|
|
40916
|
+
"restoreSelection",
|
|
40917
|
+
"scroll",
|
|
40918
|
+
"scrollBy",
|
|
40919
|
+
"scrollIntoView",
|
|
40920
|
+
"scrollTo",
|
|
40921
|
+
"setRangeText",
|
|
40922
|
+
"setSelectionRange"
|
|
40923
|
+
]);
|
|
40442
40924
|
const subtreeReadsDomMeasurement = (root) => {
|
|
40443
40925
|
if (!root) return false;
|
|
40444
40926
|
let found = false;
|
|
@@ -40457,29 +40939,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
40457
40939
|
});
|
|
40458
40940
|
return found;
|
|
40459
40941
|
};
|
|
40460
|
-
const
|
|
40942
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
40461
40943
|
const names = /* @__PURE__ */ new Set();
|
|
40462
40944
|
walkAst(program, (child) => {
|
|
40463
40945
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
40464
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40946
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
40465
40947
|
return;
|
|
40466
40948
|
}
|
|
40467
40949
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
40468
40950
|
let functionValue = child.init;
|
|
40469
40951
|
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) &&
|
|
40952
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40471
40953
|
});
|
|
40472
40954
|
return names;
|
|
40473
40955
|
};
|
|
40474
|
-
const
|
|
40475
|
-
|
|
40956
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40957
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40958
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
40476
40959
|
let found = false;
|
|
40477
40960
|
walkAst(root, (child) => {
|
|
40478
40961
|
if (found) return false;
|
|
40962
|
+
if (child !== root && isFunctionLike$1(child)) return false;
|
|
40963
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40964
|
+
const callee = stripParenExpression(child.callee);
|
|
40965
|
+
const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
40966
|
+
if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
|
|
40967
|
+
found = true;
|
|
40968
|
+
return false;
|
|
40969
|
+
}
|
|
40970
|
+
});
|
|
40971
|
+
return found;
|
|
40972
|
+
};
|
|
40973
|
+
const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
|
|
40974
|
+
const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
|
|
40975
|
+
if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
|
|
40976
|
+
let found = false;
|
|
40977
|
+
walkAst(root, (child) => {
|
|
40978
|
+
if (found) return false;
|
|
40979
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
40479
40980
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
40480
40981
|
});
|
|
40481
40982
|
return found;
|
|
40482
40983
|
};
|
|
40984
|
+
const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
|
|
40985
|
+
let statement = call;
|
|
40986
|
+
let parent = statement.parent;
|
|
40987
|
+
while (parent) {
|
|
40988
|
+
const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
|
|
40989
|
+
if (statements) {
|
|
40990
|
+
const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
|
|
40991
|
+
if (statementIndex >= 0) {
|
|
40992
|
+
const nextStatement = statements[statementIndex + 1];
|
|
40993
|
+
return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
|
|
40994
|
+
}
|
|
40995
|
+
}
|
|
40996
|
+
if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
40997
|
+
statement = parent;
|
|
40998
|
+
parent = parent.parent;
|
|
40999
|
+
}
|
|
41000
|
+
return false;
|
|
41001
|
+
};
|
|
40483
41002
|
const isInsideStartViewTransition = (node) => {
|
|
40484
41003
|
let cursor = node.parent;
|
|
40485
41004
|
while (cursor) {
|
|
@@ -40520,11 +41039,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
40520
41039
|
};
|
|
40521
41040
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
40522
41041
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
41042
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
40523
41043
|
let exempt = false;
|
|
40524
41044
|
walkAst(program, (child) => {
|
|
40525
41045
|
if (exempt) return false;
|
|
40526
41046
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
40527
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
41047
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
40528
41048
|
exempt = true;
|
|
40529
41049
|
return false;
|
|
40530
41050
|
}
|
|
@@ -41835,7 +42355,7 @@ const noInitializeState = defineRule({
|
|
|
41835
42355
|
tags: ["test-noise"],
|
|
41836
42356
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
41837
42357
|
create: (context) => ({ CallExpression(node) {
|
|
41838
|
-
if (!
|
|
42358
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
41839
42359
|
const dependencies = node.arguments?.[1];
|
|
41840
42360
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41841
42361
|
const analysis = getProgramAnalysis(node);
|
|
@@ -43447,7 +43967,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43447
43967
|
const setterElement = elements[1];
|
|
43448
43968
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43449
43969
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43450
|
-
if (!
|
|
43970
|
+
if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
|
|
43451
43971
|
const initializer = declarator.init.arguments?.[0];
|
|
43452
43972
|
if (!initializer) continue;
|
|
43453
43973
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43465,7 +43985,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43465
43985
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43466
43986
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43467
43987
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43468
|
-
if (!
|
|
43988
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
43469
43989
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43470
43990
|
const depsNode = effectCall.arguments[1];
|
|
43471
43991
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -43874,7 +44394,7 @@ const noMultiComp = defineRule({
|
|
|
43874
44394
|
});
|
|
43875
44395
|
//#endregion
|
|
43876
44396
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
43877
|
-
const collectUseRefBindingNames = (componentBody) => {
|
|
44397
|
+
const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
43878
44398
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
43879
44399
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
43880
44400
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -43882,7 +44402,7 @@ const collectUseRefBindingNames = (componentBody) => {
|
|
|
43882
44402
|
for (const declarator of statement.declarations ?? []) {
|
|
43883
44403
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
43884
44404
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43885
|
-
if (!
|
|
44405
|
+
if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
|
|
43886
44406
|
useRefBindings.add(declarator.id.name);
|
|
43887
44407
|
}
|
|
43888
44408
|
}
|
|
@@ -43917,12 +44437,12 @@ const noMutableInDeps = defineRule({
|
|
|
43917
44437
|
create: (context) => {
|
|
43918
44438
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
43919
44439
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
43920
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44440
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
|
|
43921
44441
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
43922
44442
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
43923
44443
|
walkAst(componentBody, (child) => {
|
|
43924
44444
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
43925
|
-
if (!
|
|
44445
|
+
if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
43926
44446
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
43927
44447
|
const depsNode = child.arguments[1];
|
|
43928
44448
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -45752,6 +46272,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
45752
46272
|
"useMatchMedia",
|
|
45753
46273
|
"useMediaJobProgress",
|
|
45754
46274
|
"useMediaQuery",
|
|
46275
|
+
"useMediaQueryState",
|
|
45755
46276
|
"useResizeObserver",
|
|
45756
46277
|
"useVisibility",
|
|
45757
46278
|
"useWindowSize"
|
|
@@ -45788,9 +46309,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
45788
46309
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
45789
46310
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
45790
46311
|
};
|
|
45791
|
-
const
|
|
46312
|
+
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
46313
|
+
let hookFunction = resolveToFunction(ref);
|
|
46314
|
+
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46315
|
+
const definitionNode = definition.node;
|
|
46316
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
46317
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
46318
|
+
if (!isNodeOfType(initializer, "CallExpression")) continue;
|
|
46319
|
+
const callee = stripParenExpression(initializer.callee);
|
|
46320
|
+
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
46321
|
+
const calleeReference = getRef(analysis, callee);
|
|
46322
|
+
if (!calleeReference) continue;
|
|
46323
|
+
hookFunction = resolveToFunction(calleeReference);
|
|
46324
|
+
if (hookFunction) break;
|
|
46325
|
+
}
|
|
46326
|
+
if (!hookFunction) return null;
|
|
46327
|
+
const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46328
|
+
if (returnedReferences.length === 0) return null;
|
|
46329
|
+
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46330
|
+
};
|
|
46331
|
+
const isExternalSubscriptionHookRef = (analysis, ref) => {
|
|
45792
46332
|
const identifier = ref.identifier;
|
|
45793
46333
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46334
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46335
|
+
if (localHookProof !== null) return localHookProof;
|
|
45794
46336
|
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45795
46337
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
45796
46338
|
const node = def.node;
|
|
@@ -45813,16 +46355,10 @@ const noPassDataToParent = defineRule({
|
|
|
45813
46355
|
tags: ["test-noise"],
|
|
45814
46356
|
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",
|
|
45815
46357
|
create: (context) => {
|
|
45816
|
-
const isReactUseRefCall = (node) =>
|
|
45817
|
-
|
|
45818
|
-
allowUnboundBareCalls: true
|
|
45819
|
-
});
|
|
45820
|
-
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
45821
|
-
allowGlobalReactNamespace: true,
|
|
45822
|
-
allowUnboundBareCalls: true
|
|
45823
|
-
});
|
|
46358
|
+
const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
|
|
46359
|
+
const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
|
|
45824
46360
|
return { CallExpression(node) {
|
|
45825
|
-
if (!
|
|
46361
|
+
if (!isReactUseEffectCall(node)) return;
|
|
45826
46362
|
const analysis = getProgramAnalysis(node);
|
|
45827
46363
|
if (!analysis) return;
|
|
45828
46364
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -45874,11 +46410,11 @@ const noPassDataToParent = defineRule({
|
|
|
45874
46410
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
45875
46411
|
}
|
|
45876
46412
|
return getDownstreamRefs(analysis, argument);
|
|
45877
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46413
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
45878
46414
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45879
46415
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
45880
46416
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
45881
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46417
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
45882
46418
|
if (isProp(analysis, argRef)) return false;
|
|
45883
46419
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
45884
46420
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -46109,7 +46645,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
46109
46645
|
tags: ["test-noise"],
|
|
46110
46646
|
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",
|
|
46111
46647
|
create: (context) => ({ CallExpression(node) {
|
|
46112
|
-
if (!
|
|
46648
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
46113
46649
|
const analysis = getProgramAnalysis(node);
|
|
46114
46650
|
if (!analysis) return;
|
|
46115
46651
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -46524,7 +47060,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
46524
47060
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
46525
47061
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
46526
47062
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
46527
|
-
const calleeName = getCalleeName$
|
|
47063
|
+
const calleeName = getCalleeName$1(binding.initializer);
|
|
46528
47064
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
46529
47065
|
}
|
|
46530
47066
|
return false;
|
|
@@ -46546,7 +47082,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
46546
47082
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
46547
47083
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
46548
47084
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
46549
|
-
if (getCalleeName$
|
|
47085
|
+
if (getCalleeName$1(binding.initializer) !== "useRef") return null;
|
|
46550
47086
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
46551
47087
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
46552
47088
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -46578,7 +47114,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
46578
47114
|
return {
|
|
46579
47115
|
...propStackTracker.visitors,
|
|
46580
47116
|
CallExpression(node) {
|
|
46581
|
-
if (!
|
|
47117
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
46582
47118
|
const callback = getEffectCallback(node);
|
|
46583
47119
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
46584
47120
|
const depsNode = node.arguments[1];
|
|
@@ -48471,7 +49007,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
48471
49007
|
tags: ["test-noise"],
|
|
48472
49008
|
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",
|
|
48473
49009
|
create: (context) => ({ CallExpression(node) {
|
|
48474
|
-
if (!
|
|
49010
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
48475
49011
|
const analysis = getProgramAnalysis(node);
|
|
48476
49012
|
if (!analysis) return;
|
|
48477
49013
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -48740,7 +49276,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
48740
49276
|
if (node.callee.property.name !== "handler") return false;
|
|
48741
49277
|
let currentNode = node.callee.object;
|
|
48742
49278
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
48743
|
-
const calleeName = getCalleeName$
|
|
49279
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
48744
49280
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
48745
49281
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
48746
49282
|
currentNode = currentNode.callee.object;
|
|
@@ -49241,7 +49777,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49241
49777
|
create: (context) => {
|
|
49242
49778
|
const checkFunctionScope = (functionBody) => {
|
|
49243
49779
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
49244
|
-
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49780
|
+
const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
|
|
49245
49781
|
if (useStateBindings.length === 0) return;
|
|
49246
49782
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
49247
49783
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -49250,7 +49786,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49250
49786
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
49251
49787
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
49252
49788
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
49253
|
-
if (!
|
|
49789
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
49254
49790
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
49255
49791
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
49256
49792
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -49336,7 +49872,7 @@ const noSetStateInRender = defineRule({
|
|
|
49336
49872
|
create: (context) => {
|
|
49337
49873
|
const checkComponent = (componentBody) => {
|
|
49338
49874
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
49339
|
-
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49875
|
+
const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
|
|
49340
49876
|
if (setterNames.size === 0) return;
|
|
49341
49877
|
for (const statement of componentBody.body ?? []) {
|
|
49342
49878
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -49585,10 +50121,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
49585
50121
|
});
|
|
49586
50122
|
return facts;
|
|
49587
50123
|
};
|
|
49588
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
50124
|
+
const isEffectCallbackFunction = (functionNode, scopes) => {
|
|
49589
50125
|
const parent = functionNode.parent;
|
|
49590
50126
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
49591
|
-
return
|
|
50127
|
+
return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
|
|
49592
50128
|
};
|
|
49593
50129
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
49594
50130
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -49607,24 +50143,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
49607
50143
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49608
50144
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
49609
50145
|
};
|
|
49610
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50146
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
|
|
49611
50147
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49612
50148
|
if (cleanupBindingName === null) return false;
|
|
49613
50149
|
let isReturnedFromEffect = false;
|
|
49614
50150
|
walkAst(ownerScope, (child) => {
|
|
49615
50151
|
if (isReturnedFromEffect) return false;
|
|
49616
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50152
|
+
if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
49617
50153
|
const effectCallback = getEffectCallback(child);
|
|
49618
50154
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
49619
50155
|
});
|
|
49620
50156
|
return isReturnedFromEffect;
|
|
49621
50157
|
};
|
|
49622
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50158
|
+
const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
|
|
49623
50159
|
let functionNode = findEnclosingFunction$1(node);
|
|
49624
50160
|
while (functionNode) {
|
|
49625
50161
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
49626
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
49627
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
50162
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50163
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
|
|
49628
50164
|
functionNode = outerFunction;
|
|
49629
50165
|
}
|
|
49630
50166
|
return false;
|
|
@@ -49660,10 +50196,10 @@ const noStaleTimerRef = defineRule({
|
|
|
49660
50196
|
if (isShadowedTimerGlobal(node)) return;
|
|
49661
50197
|
const { clearCalleeName, refName } = clearCall;
|
|
49662
50198
|
const refBinding = findVariableInitializer(node, refName);
|
|
49663
|
-
if (!refBinding?.initializer || !
|
|
50199
|
+
if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
|
|
49664
50200
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
49665
50201
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
49666
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50202
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
|
|
49667
50203
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
49668
50204
|
context.report({
|
|
49669
50205
|
node,
|
|
@@ -55467,7 +56003,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
55467
56003
|
allReadsAreInSubHandlers = false;
|
|
55468
56004
|
return;
|
|
55469
56005
|
}
|
|
55470
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
56006
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
|
|
55471
56007
|
});
|
|
55472
56008
|
return {
|
|
55473
56009
|
hasAnyRead,
|
|
@@ -55490,7 +56026,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
55490
56026
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
55491
56027
|
const effectCall = statement.expression;
|
|
55492
56028
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55493
|
-
if (!
|
|
56029
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
55494
56030
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55495
56031
|
const depsNode = effectCall.arguments[1];
|
|
55496
56032
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -55522,11 +56058,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
55522
56058
|
});
|
|
55523
56059
|
//#endregion
|
|
55524
56060
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
55525
|
-
const findUseEffectsInComponent = (componentBody) => {
|
|
56061
|
+
const findUseEffectsInComponent = (componentBody, scopes) => {
|
|
55526
56062
|
const effectCalls = [];
|
|
55527
56063
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
55528
56064
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
55529
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56065
|
+
if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
|
|
55530
56066
|
});
|
|
55531
56067
|
return effectCalls;
|
|
55532
56068
|
};
|
|
@@ -55729,7 +56265,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55729
56265
|
};
|
|
55730
56266
|
const checkComponent = (componentBody) => {
|
|
55731
56267
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
55732
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56268
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
55733
56269
|
if (useStateBindings.length === 0) return;
|
|
55734
56270
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
55735
56271
|
for (const binding of useStateBindings) {
|
|
@@ -55742,7 +56278,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55742
56278
|
}
|
|
55743
56279
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
55744
56280
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
55745
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56281
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55746
56282
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55747
56283
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55748
56284
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55784,7 +56320,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55784
56320
|
})).filter((candidate) => candidate.storeName !== null);
|
|
55785
56321
|
if (snapshotBindings.length === 0) return;
|
|
55786
56322
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
55787
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56323
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55788
56324
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55789
56325
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55790
56326
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55881,7 +56417,7 @@ const preferUseReducer = defineRule({
|
|
|
55881
56417
|
create: (context) => {
|
|
55882
56418
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
55883
56419
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
55884
|
-
const bindings = collectUseStateBindings(body);
|
|
56420
|
+
const bindings = collectUseStateBindings(body, context.scopes);
|
|
55885
56421
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
55886
56422
|
if (setterNames.size < 5) return;
|
|
55887
56423
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -56097,7 +56633,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
56097
56633
|
]);
|
|
56098
56634
|
const isQueryCacheSourceCall = (initializer) => {
|
|
56099
56635
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
56100
|
-
const hookName = getCalleeName$
|
|
56636
|
+
const hookName = getCalleeName$1(initializer);
|
|
56101
56637
|
if (!hookName) return false;
|
|
56102
56638
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
56103
56639
|
};
|
|
@@ -56229,7 +56765,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
56229
56765
|
},
|
|
56230
56766
|
CallExpression(node) {
|
|
56231
56767
|
if (!hasQueryReadUsage) {
|
|
56232
|
-
const callName = getCalleeName$
|
|
56768
|
+
const callName = getCalleeName$1(node);
|
|
56233
56769
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
56234
56770
|
}
|
|
56235
56771
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -57892,6 +58428,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
57892
58428
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
57893
58429
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
57894
58430
|
const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
|
|
58431
|
+
const FILE_READER_READ_METHOD_NAMES = new Set([
|
|
58432
|
+
"readAsArrayBuffer",
|
|
58433
|
+
"readAsBinaryString",
|
|
58434
|
+
"readAsDataURL",
|
|
58435
|
+
"readAsText"
|
|
58436
|
+
]);
|
|
58437
|
+
const isGlobalFileReaderConstruction = (expression, context) => {
|
|
58438
|
+
if (!expression) return false;
|
|
58439
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58440
|
+
if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
58441
|
+
return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
|
|
58442
|
+
};
|
|
58443
|
+
const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
|
|
58444
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58445
|
+
let latestValue = null;
|
|
58446
|
+
let latestStart = null;
|
|
58447
|
+
if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
|
|
58448
|
+
latestValue = readerSymbol.initializer;
|
|
58449
|
+
latestStart = readerSymbol.declarationNode.range[0];
|
|
58450
|
+
}
|
|
58451
|
+
for (const reference of readerSymbol.references) {
|
|
58452
|
+
if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
|
|
58453
|
+
const assignment = reference.identifier.parent;
|
|
58454
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
|
|
58455
|
+
latestValue = assignment.right;
|
|
58456
|
+
latestStart = reference.identifier.range[0];
|
|
58457
|
+
}
|
|
58458
|
+
return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
|
|
58459
|
+
};
|
|
58460
|
+
const resolveLoadingCompletionFunction = (expression, context) => {
|
|
58461
|
+
const directFunction = resolveExactLocalFunction(expression, context.scopes);
|
|
58462
|
+
if (directFunction) return directFunction;
|
|
58463
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58464
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
58465
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
58466
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
58467
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
|
|
58468
|
+
const callback = initializer.arguments?.[0];
|
|
58469
|
+
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
58470
|
+
};
|
|
58471
|
+
const isSetterBooleanCall = (node, setterSymbol, value, context) => {
|
|
58472
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
58473
|
+
const callee = stripParenExpression(node.callee);
|
|
58474
|
+
const argument = node.arguments?.[0];
|
|
58475
|
+
const unwrappedArgument = argument ? stripParenExpression(argument) : null;
|
|
58476
|
+
return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
|
|
58477
|
+
};
|
|
58478
|
+
const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
|
|
58479
|
+
if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
|
|
58480
|
+
visitedFunctions.add(functionNode);
|
|
58481
|
+
let didClearLoadingState = false;
|
|
58482
|
+
walkAst(functionNode.body, (child) => {
|
|
58483
|
+
if (didClearLoadingState) return false;
|
|
58484
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
58485
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58486
|
+
if (isSetterBooleanCall(child, setterSymbol, false, context)) {
|
|
58487
|
+
didClearLoadingState = true;
|
|
58488
|
+
return false;
|
|
58489
|
+
}
|
|
58490
|
+
const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
|
|
58491
|
+
if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
|
|
58492
|
+
didClearLoadingState = true;
|
|
58493
|
+
return false;
|
|
58494
|
+
}
|
|
58495
|
+
});
|
|
58496
|
+
return didClearLoadingState;
|
|
58497
|
+
};
|
|
58498
|
+
const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
|
|
58499
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58500
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return null;
|
|
58501
|
+
let callback = null;
|
|
58502
|
+
let callbackStart = originStart;
|
|
58503
|
+
walkAst(readFunction.body, (child) => {
|
|
58504
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58505
|
+
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;
|
|
58506
|
+
const receiver = stripParenExpression(child.left.object);
|
|
58507
|
+
if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
|
|
58508
|
+
callback = child.right;
|
|
58509
|
+
callbackStart = child.range[0];
|
|
58510
|
+
});
|
|
58511
|
+
return callback;
|
|
58512
|
+
};
|
|
58513
|
+
const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
|
|
58514
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58515
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return false;
|
|
58516
|
+
let didStartLoading = false;
|
|
58517
|
+
walkAst(readFunction.body, (child) => {
|
|
58518
|
+
if (didStartLoading) return false;
|
|
58519
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58520
|
+
if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
|
|
58521
|
+
if (isSetterBooleanCall(child, setterSymbol, true, context)) {
|
|
58522
|
+
didStartLoading = true;
|
|
58523
|
+
return false;
|
|
58524
|
+
}
|
|
58525
|
+
});
|
|
58526
|
+
return didStartLoading;
|
|
58527
|
+
};
|
|
58528
|
+
const setterTracksFileReader = (functionBody, setterSymbol, context) => {
|
|
58529
|
+
let didFindFileReaderLifecycle = false;
|
|
58530
|
+
walkAst(functionBody, (child) => {
|
|
58531
|
+
if (didFindFileReaderLifecycle) return false;
|
|
58532
|
+
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
|
|
58533
|
+
const receiver = stripParenExpression(child.callee.object);
|
|
58534
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
58535
|
+
const readerSymbol = context.scopes.symbolFor(receiver);
|
|
58536
|
+
if (!readerSymbol) return;
|
|
58537
|
+
const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
|
|
58538
|
+
if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
|
|
58539
|
+
const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
|
|
58540
|
+
const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
|
|
58541
|
+
const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
|
|
58542
|
+
const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
|
|
58543
|
+
if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
|
|
58544
|
+
didFindFileReaderLifecycle = true;
|
|
58545
|
+
return false;
|
|
58546
|
+
}
|
|
58547
|
+
});
|
|
58548
|
+
return didFindFileReaderLifecycle;
|
|
58549
|
+
};
|
|
57895
58550
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
57896
58551
|
let found = false;
|
|
57897
58552
|
walkAst(fnBody, (child) => {
|
|
@@ -58065,6 +58720,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58065
58720
|
const fnBody = enclosingFunctionBody(node);
|
|
58066
58721
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
58067
58722
|
if (fnBody && setterName) {
|
|
58723
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58724
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
58068
58725
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
58069
58726
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
58070
58727
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -58396,7 +59053,7 @@ const rerenderDependencies = defineRule({
|
|
|
58396
59053
|
severity: "error",
|
|
58397
59054
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
58398
59055
|
create: (context) => ({ CallExpression(node) {
|
|
58399
|
-
if (!
|
|
59056
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
58400
59057
|
const depsNode = node.arguments[1];
|
|
58401
59058
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
58402
59059
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -58651,7 +59308,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
58651
59308
|
category: "Performance",
|
|
58652
59309
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
58653
59310
|
create: (context) => ({ CallExpression(node) {
|
|
58654
|
-
if (!
|
|
59311
|
+
if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
|
|
58655
59312
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
58656
59313
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
58657
59314
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -58715,7 +59372,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
58715
59372
|
category: "Performance",
|
|
58716
59373
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
58717
59374
|
create: (context) => ({ CallExpression(node) {
|
|
58718
|
-
if (!
|
|
59375
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
58719
59376
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
58720
59377
|
if (!initializer) return;
|
|
58721
59378
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59478,11 +60135,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
59478
60135
|
}
|
|
59479
60136
|
return false;
|
|
59480
60137
|
};
|
|
59481
|
-
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60138
|
+
const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
59482
60139
|
const effectInfos = [];
|
|
59483
60140
|
walkAst(componentBody, (child) => {
|
|
59484
60141
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59485
|
-
if (!
|
|
60142
|
+
if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
59486
60143
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
59487
60144
|
for (const argument of child.arguments ?? []) {
|
|
59488
60145
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -59538,13 +60195,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
|
59538
60195
|
});
|
|
59539
60196
|
return effectInfos;
|
|
59540
60197
|
};
|
|
59541
|
-
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60198
|
+
const collectCustomHookArgumentNames = (componentBody, scopes) => {
|
|
59542
60199
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
59543
60200
|
walkAst(componentBody, (child) => {
|
|
59544
60201
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59545
60202
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
59546
60203
|
const calleeName = child.callee.name;
|
|
59547
60204
|
if (!isReactHookName(calleeName)) return;
|
|
60205
|
+
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
59548
60206
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
59549
60207
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
59550
60208
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -59585,21 +60243,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
59585
60243
|
create: (context) => {
|
|
59586
60244
|
const checkComponent = (componentBody) => {
|
|
59587
60245
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
59588
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
60246
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
59589
60247
|
if (bindings.length === 0) return;
|
|
59590
60248
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
59591
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60249
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
59592
60250
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
59593
|
-
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60251
|
+
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
59594
60252
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
59595
60253
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
59596
60254
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
59597
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60255
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
|
|
59598
60256
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
59599
60257
|
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);
|
|
59600
60258
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
59601
60259
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
59602
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60260
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
59603
60261
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
59604
60262
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
59605
60263
|
walkAst(componentBody, (child) => {
|
|
@@ -67500,7 +68158,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
67500
68158
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
67501
68159
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
67502
68160
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
67503
|
-
const calleeName = getCalleeName$
|
|
68161
|
+
const calleeName = getCalleeName$1(argument);
|
|
67504
68162
|
if (!calleeName) continue;
|
|
67505
68163
|
if (isAuthGuardName(calleeName)) return true;
|
|
67506
68164
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -68140,7 +68798,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
68140
68798
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
68141
68799
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
68142
68800
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
68143
|
-
const calleeName = getCalleeName$
|
|
68801
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
68144
68802
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
68145
68803
|
result.isServerFnChain = true;
|
|
68146
68804
|
const optionsArgument = currentNode.arguments?.[0];
|