oxlint-plugin-react-doctor 0.7.9-dev.2979a9b → 0.7.9-dev.44d3ad8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +1485 -401
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -836,21 +836,130 @@ 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
|
-
if (!calleeName) return false;
|
|
852
|
-
return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
|
|
935
|
+
return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
|
|
853
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);
|
|
954
|
+
};
|
|
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
|
+
});
|
|
854
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";
|
|
@@ -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;
|
|
@@ -14947,13 +15177,14 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14947
15177
|
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14948
15178
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14949
15179
|
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
14950
|
-
|
|
15180
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
|
|
15181
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
14951
15182
|
}
|
|
14952
15183
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14953
15184
|
return true;
|
|
14954
15185
|
};
|
|
14955
15186
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
14956
|
-
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
15187
|
+
const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
14957
15188
|
let currentNode = functionNode;
|
|
14958
15189
|
let parentNode = currentNode.parent;
|
|
14959
15190
|
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
@@ -14962,23 +15193,175 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14962
15193
|
}
|
|
14963
15194
|
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14964
15195
|
const effectCall = effectCallback?.parent;
|
|
14965
|
-
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") &&
|
|
15196
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes));
|
|
14966
15197
|
};
|
|
14967
15198
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
14968
|
-
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
15199
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
|
|
14969
15200
|
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
14970
15201
|
if (!bindingIdentifier) return false;
|
|
14971
15202
|
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
14972
15203
|
if (!symbol) return false;
|
|
14973
15204
|
return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
|
|
14974
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
|
+
};
|
|
15337
|
+
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
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;
|
|
15339
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15340
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15341
|
+
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15342
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15343
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15344
|
+
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15345
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15346
|
+
const triggerRegistrations = [];
|
|
15347
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15348
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15349
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15350
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15351
|
+
if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
|
|
15352
|
+
});
|
|
15353
|
+
if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
|
|
15354
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
|
|
15355
|
+
};
|
|
14975
15356
|
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
14976
15357
|
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
14977
15358
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
14978
15359
|
if (!releaseFunction) return true;
|
|
14979
15360
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
15361
|
+
if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
|
|
14980
15362
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14981
15363
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15364
|
+
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
14982
15365
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
14983
15366
|
};
|
|
14984
15367
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -15310,7 +15693,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
|
15310
15693
|
return false;
|
|
15311
15694
|
}
|
|
15312
15695
|
}
|
|
15313
|
-
if (
|
|
15696
|
+
if (isSubscribeOrObserveCallExpression(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
15314
15697
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15315
15698
|
const subscriptionUsage = {
|
|
15316
15699
|
kind: "subscribe",
|
|
@@ -15518,7 +15901,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
|
15518
15901
|
if (!isFunctionLike$1(functionNode)) return false;
|
|
15519
15902
|
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15520
15903
|
const callbackCall = functionRoot.parent;
|
|
15521
|
-
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;
|
|
15522
15905
|
const parentNode = functionNode.parent;
|
|
15523
15906
|
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
15524
15907
|
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
@@ -15554,12 +15937,12 @@ const effectNeedsCleanup = defineRule({
|
|
|
15554
15937
|
};
|
|
15555
15938
|
return {
|
|
15556
15939
|
CallExpression(node) {
|
|
15557
|
-
if (
|
|
15940
|
+
if (isReactHookCall(node, "useCallback", context.scopes)) {
|
|
15558
15941
|
const retainedCallback = getEffectCallback(node);
|
|
15559
15942
|
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
15560
15943
|
return;
|
|
15561
15944
|
}
|
|
15562
|
-
if (!
|
|
15945
|
+
if (!isReactHookCall(node, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
15563
15946
|
const callback = getEffectCallback(node);
|
|
15564
15947
|
if (!callback) return;
|
|
15565
15948
|
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
@@ -15567,7 +15950,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
15567
15950
|
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
15568
15951
|
if (!firstUsage) return;
|
|
15569
15952
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
15570
|
-
const hookName = getCalleeName$
|
|
15953
|
+
const hookName = getCalleeName$1(node) ?? "effect";
|
|
15571
15954
|
context.report({
|
|
15572
15955
|
node,
|
|
15573
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.`
|
|
@@ -16910,7 +17293,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
|
16910
17293
|
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
16911
17294
|
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
16912
17295
|
};
|
|
16913
|
-
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);
|
|
16914
17328
|
//#endregion
|
|
16915
17329
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
16916
17330
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -17112,7 +17526,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
|
|
|
17112
17526
|
keys.add(depKey);
|
|
17113
17527
|
continue;
|
|
17114
17528
|
}
|
|
17115
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17529
|
+
const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17116
17530
|
if (identitySourceKeys) {
|
|
17117
17531
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
17118
17532
|
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
@@ -17195,6 +17609,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
|
17195
17609
|
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
17196
17610
|
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17197
17611
|
};
|
|
17612
|
+
const isPureDerivedExpression = (expression) => {
|
|
17613
|
+
const candidate = unwrapExpression$3(expression);
|
|
17614
|
+
if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
|
|
17615
|
+
if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
|
|
17616
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
|
|
17617
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
|
|
17618
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
|
|
17619
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
|
|
17620
|
+
return false;
|
|
17621
|
+
};
|
|
17622
|
+
const isPureDerivedStatement = (statement) => {
|
|
17623
|
+
if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
|
|
17624
|
+
if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
|
|
17625
|
+
if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
|
|
17626
|
+
return false;
|
|
17627
|
+
};
|
|
17628
|
+
const isPureDerivedFunction = (functionNode) => {
|
|
17629
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
|
|
17630
|
+
if (functionNode.async || functionNode.generator) return false;
|
|
17631
|
+
return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
|
|
17632
|
+
};
|
|
17633
|
+
const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
|
|
17634
|
+
if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
|
|
17635
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
17636
|
+
const callExpression = referenceRoot.parent;
|
|
17637
|
+
if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
|
|
17638
|
+
const functionNode = getFunctionValueNode(symbol);
|
|
17639
|
+
if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
|
|
17640
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17641
|
+
for (const capturedReference of closureCaptures(functionNode, scopes)) {
|
|
17642
|
+
const capturedSymbol = capturedReference.resolvedSymbol;
|
|
17643
|
+
if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
|
|
17644
|
+
if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
|
|
17645
|
+
const capturedKey = computeDepKey(capturedReference);
|
|
17646
|
+
if (!capturedKey) return null;
|
|
17647
|
+
if (capturedKey === capturedSymbol.name) {
|
|
17648
|
+
const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
|
|
17649
|
+
if (nestedSourceKeys) {
|
|
17650
|
+
for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
|
|
17651
|
+
continue;
|
|
17652
|
+
}
|
|
17653
|
+
}
|
|
17654
|
+
sourceKeys.add(capturedKey);
|
|
17655
|
+
}
|
|
17656
|
+
return sourceKeys.size > 0 ? sourceKeys : null;
|
|
17657
|
+
};
|
|
17658
|
+
const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
17659
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17660
|
+
for (const expression of expressions) {
|
|
17661
|
+
const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
|
|
17662
|
+
if (!expressionSourceKeys) return null;
|
|
17663
|
+
for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
|
|
17664
|
+
}
|
|
17665
|
+
return sourceKeys;
|
|
17666
|
+
};
|
|
17667
|
+
const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
|
|
17668
|
+
const candidate = unwrapExpression$3(expression);
|
|
17669
|
+
if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
|
|
17670
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
17671
|
+
if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
|
|
17672
|
+
const sourceSymbol = scopes.symbolFor(candidate);
|
|
17673
|
+
if (!sourceSymbol) return null;
|
|
17674
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17675
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
|
|
17676
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
17677
|
+
const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
17678
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
17679
|
+
if (sourceKeys) return sourceKeys;
|
|
17680
|
+
}
|
|
17681
|
+
return new Set([sourceSymbol.name]);
|
|
17682
|
+
}
|
|
17683
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
17684
|
+
if (hasComputedMemberExpression(candidate)) return null;
|
|
17685
|
+
const sourceKey = stringifyMemberChain(candidate);
|
|
17686
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
17687
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
17688
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
17689
|
+
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17690
|
+
return new Set([sourceKey]);
|
|
17691
|
+
}
|
|
17692
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
|
|
17693
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
|
|
17694
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
|
|
17695
|
+
candidate.test,
|
|
17696
|
+
candidate.consequent,
|
|
17697
|
+
candidate.alternate
|
|
17698
|
+
], scopes, visitedSymbolIds);
|
|
17699
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
|
|
17700
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
17701
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
17702
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
|
|
17703
|
+
const argumentsToAnalyze = [];
|
|
17704
|
+
for (const argument of candidate.arguments) {
|
|
17705
|
+
if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
|
|
17706
|
+
argumentsToAnalyze.push(argument);
|
|
17707
|
+
}
|
|
17708
|
+
return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
|
|
17709
|
+
}
|
|
17710
|
+
return null;
|
|
17711
|
+
};
|
|
17712
|
+
const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
|
|
17713
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17714
|
+
let currentNode = assignment;
|
|
17715
|
+
while (currentNode.parent && currentNode.parent !== boundaryFunction) {
|
|
17716
|
+
const parentNode = currentNode.parent;
|
|
17717
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
17718
|
+
if (parentNode.test === currentNode) return null;
|
|
17719
|
+
const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
|
|
17720
|
+
if (!testSourceKeys) return null;
|
|
17721
|
+
for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
|
|
17722
|
+
} else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
|
|
17723
|
+
currentNode = parentNode;
|
|
17724
|
+
}
|
|
17725
|
+
return currentNode.parent === boundaryFunction ? sourceKeys : null;
|
|
17726
|
+
};
|
|
17727
|
+
const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
|
|
17728
|
+
const referenceRoot = findTransparentExpressionRoot(referenceNode);
|
|
17729
|
+
const callExpression = referenceRoot.parent;
|
|
17730
|
+
return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
|
|
17731
|
+
allowGlobalReactNamespace: true,
|
|
17732
|
+
allowUnboundBareCalls: true,
|
|
17733
|
+
resolveNamedAliases: true
|
|
17734
|
+
});
|
|
17735
|
+
};
|
|
17736
|
+
const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
|
|
17737
|
+
if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
17738
|
+
const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
17739
|
+
if (!boundaryFunction) return null;
|
|
17740
|
+
const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
|
|
17741
|
+
if (!capturingFunction || capturingFunction === boundaryFunction) return null;
|
|
17742
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17743
|
+
if (symbol.initializer) {
|
|
17744
|
+
const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17745
|
+
if (!initializerSourceKeys) return null;
|
|
17746
|
+
for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
|
|
17747
|
+
}
|
|
17748
|
+
let writeCount = 0;
|
|
17749
|
+
for (const symbolReference of symbol.references) {
|
|
17750
|
+
if (symbolReference.flag === "read") {
|
|
17751
|
+
if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
|
|
17752
|
+
continue;
|
|
17753
|
+
}
|
|
17754
|
+
if (symbolReference.flag !== "write") return null;
|
|
17755
|
+
const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
|
|
17756
|
+
const assignment = referenceRoot.parent;
|
|
17757
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
|
|
17758
|
+
const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
|
|
17759
|
+
const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
|
|
17760
|
+
if (!assignmentSourceKeys || !controlSourceKeys) return null;
|
|
17761
|
+
for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
|
|
17762
|
+
for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
|
|
17763
|
+
writeCount += 1;
|
|
17764
|
+
}
|
|
17765
|
+
return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
|
|
17766
|
+
};
|
|
17198
17767
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
17199
17768
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
17200
17769
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
@@ -17938,7 +18507,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
17938
18507
|
if (!isUsed) continue;
|
|
17939
18508
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
17940
18509
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
17941
|
-
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18510
|
+
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
17942
18511
|
context.report({
|
|
17943
18512
|
node: reportNode,
|
|
17944
18513
|
message: buildUnstableDepMessage(hookName, declaredKey)
|
|
@@ -18254,7 +18823,7 @@ const flattenCalleeName = (callee) => {
|
|
|
18254
18823
|
const PRAGMA = "React";
|
|
18255
18824
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
18256
18825
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
18257
|
-
if (getCalleeName$
|
|
18826
|
+
if (getCalleeName$1(node) !== expectedCall) return false;
|
|
18258
18827
|
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
18259
18828
|
const receiver = stripParenExpression(node.callee.object);
|
|
18260
18829
|
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
@@ -18563,8 +19132,8 @@ const hooksNoNanInDeps = defineRule({
|
|
|
18563
19132
|
severity: "warn",
|
|
18564
19133
|
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.",
|
|
18565
19134
|
create: (context) => ({ CallExpression(node) {
|
|
18566
|
-
if (!
|
|
18567
|
-
const depsIndex =
|
|
19135
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEP_ARRAY, context.scopes)) return;
|
|
19136
|
+
const depsIndex = isReactHookCall(node, "useImperativeHandle", context.scopes) ? 2 : 1;
|
|
18568
19137
|
const depsArgument = node.arguments[depsIndex];
|
|
18569
19138
|
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
18570
19139
|
for (const element of depsArgument.elements) {
|
|
@@ -19735,7 +20304,7 @@ const isImportedSelectAtom = (callExpression) => {
|
|
|
19735
20304
|
const isDeferredCallbackPosition$1 = (functionNode) => {
|
|
19736
20305
|
const parent = functionNode.parent;
|
|
19737
20306
|
if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
|
|
19738
|
-
const hookName = getCalleeName$
|
|
20307
|
+
const hookName = getCalleeName$1(parent);
|
|
19739
20308
|
if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
|
|
19740
20309
|
if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
|
|
19741
20310
|
}
|
|
@@ -29806,7 +30375,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
|
|
|
29806
30375
|
"on",
|
|
29807
30376
|
"once"
|
|
29808
30377
|
]);
|
|
29809
|
-
const getCalleeName
|
|
30378
|
+
const getCalleeName = (callee) => {
|
|
29810
30379
|
if (!callee) return null;
|
|
29811
30380
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
29812
30381
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
@@ -29818,11 +30387,11 @@ const isDeferredCallbackPosition = (expression) => {
|
|
|
29818
30387
|
const parent = parentOf(expression);
|
|
29819
30388
|
if (!parent) return false;
|
|
29820
30389
|
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
29821
|
-
const name = getCalleeName
|
|
30390
|
+
const name = getCalleeName(parent.callee);
|
|
29822
30391
|
if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
|
|
29823
30392
|
}
|
|
29824
30393
|
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
29825
|
-
const name = getCalleeName
|
|
30394
|
+
const name = getCalleeName(parent.callee);
|
|
29826
30395
|
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
29827
30396
|
}
|
|
29828
30397
|
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
@@ -30242,14 +30811,6 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
30242
30811
|
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30243
30812
|
return false;
|
|
30244
30813
|
};
|
|
30245
|
-
const isUseEffect = (node) => {
|
|
30246
|
-
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
30247
|
-
const callee = node.callee;
|
|
30248
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
30249
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
30250
|
-
const receiver = stripParenExpression(callee.object);
|
|
30251
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
30252
|
-
};
|
|
30253
30814
|
const getEffectFn = (analysis, node) => {
|
|
30254
30815
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30255
30816
|
const fn = node.arguments?.[0];
|
|
@@ -30347,7 +30908,27 @@ const isRefCurrent = (ref) => {
|
|
|
30347
30908
|
if (!isNodeOfType(parent.property, "Identifier")) return false;
|
|
30348
30909
|
return parent.property.name === "current";
|
|
30349
30910
|
};
|
|
30350
|
-
const
|
|
30911
|
+
const resolveStateSetterReference = (analysis, ref) => {
|
|
30912
|
+
const visitedReferences = /* @__PURE__ */ new Set();
|
|
30913
|
+
let currentReference = ref;
|
|
30914
|
+
while (currentReference && !visitedReferences.has(currentReference)) {
|
|
30915
|
+
if (isStateSetter(analysis, currentReference)) return currentReference;
|
|
30916
|
+
visitedReferences.add(currentReference);
|
|
30917
|
+
const definitions = currentReference.resolved?.defs ?? [];
|
|
30918
|
+
if (definitions.length !== 1) return null;
|
|
30919
|
+
const definitionNode = definitions[0].node;
|
|
30920
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator")) return null;
|
|
30921
|
+
if (!isNodeOfType(definitionNode.id, "Identifier")) return null;
|
|
30922
|
+
const declaration = definitionNode.parent;
|
|
30923
|
+
if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30924
|
+
if (!definitionNode.init) return null;
|
|
30925
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
30926
|
+
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
30927
|
+
currentReference = getRef(analysis, initializer);
|
|
30928
|
+
}
|
|
30929
|
+
return null;
|
|
30930
|
+
};
|
|
30931
|
+
const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => resolveStateSetterReference(analysis, innerRef) !== null);
|
|
30351
30932
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
30352
30933
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
30353
30934
|
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
@@ -30498,9 +31079,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
|
30498
31079
|
};
|
|
30499
31080
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
30500
31081
|
const getUseStateDecl = (analysis, ref) => {
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
|
|
31082
|
+
const definition = getUpstreamRefs(analysis, ref).find((upstreamReference) => isState(analysis, upstreamReference) || isStateSetter(analysis, upstreamReference))?.resolved?.defs.find((candidateDefinition) => {
|
|
31083
|
+
const definitionNode = candidateDefinition.node;
|
|
31084
|
+
return isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && isHookCallee$1(analysis, definitionNode.init.callee, "useState");
|
|
31085
|
+
});
|
|
31086
|
+
return definition ? definition.node : null;
|
|
30504
31087
|
};
|
|
30505
31088
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
30506
31089
|
if (isFunctionLike$1(node)) return true;
|
|
@@ -31689,6 +32272,7 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
31689
32272
|
sourceReferences,
|
|
31690
32273
|
isDeferred: frame.isDeferred,
|
|
31691
32274
|
isRenderKnownCopy,
|
|
32275
|
+
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
31692
32276
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
31693
32277
|
resetsSourceState: false
|
|
31694
32278
|
});
|
|
@@ -31704,22 +32288,71 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
31704
32288
|
});
|
|
31705
32289
|
};
|
|
31706
32290
|
//#endregion
|
|
32291
|
+
//#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
|
|
32292
|
+
const DEFERRED_MEMBER_NAMES = new Set([
|
|
32293
|
+
"catch",
|
|
32294
|
+
"finally",
|
|
32295
|
+
"then"
|
|
32296
|
+
]);
|
|
32297
|
+
const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
|
|
32298
|
+
const effectFunction = getEffectFn(analysis, effectNode);
|
|
32299
|
+
if (!effectFunction) return false;
|
|
32300
|
+
if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
|
|
32301
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
|
|
32302
|
+
let didFindDeferredOrExternalWork = false;
|
|
32303
|
+
walkAst(effectFunction, (child) => {
|
|
32304
|
+
if (didFindDeferredOrExternalWork) return false;
|
|
32305
|
+
if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
32306
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
32307
|
+
const assignmentTarget = child.left;
|
|
32308
|
+
if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
|
|
32309
|
+
didFindDeferredOrExternalWork = true;
|
|
32310
|
+
return false;
|
|
32311
|
+
}
|
|
32312
|
+
}
|
|
32313
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32314
|
+
if (isSubscribeOrObserveCallExpression(child)) {
|
|
32315
|
+
didFindDeferredOrExternalWork = true;
|
|
32316
|
+
return false;
|
|
32317
|
+
}
|
|
32318
|
+
const localFunction = resolveExactLocalFunction(child.callee, scopes);
|
|
32319
|
+
if (isFunctionLike$1(localFunction) && localFunction.async) {
|
|
32320
|
+
didFindDeferredOrExternalWork = true;
|
|
32321
|
+
return false;
|
|
32322
|
+
}
|
|
32323
|
+
const callee = child.callee;
|
|
32324
|
+
if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
|
|
32325
|
+
didFindDeferredOrExternalWork = true;
|
|
32326
|
+
return false;
|
|
32327
|
+
}
|
|
32328
|
+
const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
32329
|
+
if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
|
|
32330
|
+
didFindDeferredOrExternalWork = true;
|
|
32331
|
+
return false;
|
|
32332
|
+
}
|
|
32333
|
+
});
|
|
32334
|
+
return didFindDeferredOrExternalWork;
|
|
32335
|
+
};
|
|
32336
|
+
//#endregion
|
|
31707
32337
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
31708
32338
|
const noAdjustStateOnPropChange = defineRule({
|
|
31709
32339
|
id: "no-adjust-state-on-prop-change",
|
|
31710
|
-
title: "State
|
|
32340
|
+
title: "State adjusted after a prop changes",
|
|
31711
32341
|
severity: "warn",
|
|
31712
32342
|
tags: ["test-noise"],
|
|
31713
|
-
recommendation: "
|
|
32343
|
+
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",
|
|
31714
32344
|
create: (context) => ({ CallExpression(node) {
|
|
31715
|
-
if (!
|
|
32345
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
31716
32346
|
const analysis = getProgramAnalysis(node);
|
|
31717
32347
|
if (!analysis) return;
|
|
31718
32348
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31719
32349
|
if (!dependencyReferences) return;
|
|
31720
32350
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31721
|
-
|
|
31722
|
-
|
|
32351
|
+
const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
|
|
32352
|
+
if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
|
|
32353
|
+
for (const fact of facts) {
|
|
32354
|
+
if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
|
|
32355
|
+
if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
|
|
31723
32356
|
context.report({
|
|
31724
32357
|
node: fact.callExpression,
|
|
31725
32358
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
@@ -33732,7 +34365,7 @@ const declarationBodyContainsHookCall = (symbol) => {
|
|
|
33732
34365
|
walkAst(componentFunction, (descendant) => {
|
|
33733
34366
|
if (didFindHookCall) return false;
|
|
33734
34367
|
if (!isNodeOfType(descendant, "CallExpression")) return;
|
|
33735
|
-
const calleeName = getCalleeName$
|
|
34368
|
+
const calleeName = getCalleeName$1(descendant);
|
|
33736
34369
|
if (calleeName && isReactHookName(calleeName)) {
|
|
33737
34370
|
didFindHookCall = true;
|
|
33738
34371
|
return false;
|
|
@@ -33757,7 +34390,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
|
|
|
33757
34390
|
if (isNodeOfType(parent, "ArrowFunctionExpression")) {
|
|
33758
34391
|
if (parent.body !== current) return false;
|
|
33759
34392
|
const grandparent = parent.parent;
|
|
33760
|
-
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$
|
|
34393
|
+
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
|
|
33761
34394
|
}
|
|
33762
34395
|
if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
|
|
33763
34396
|
current = parent;
|
|
@@ -34439,7 +35072,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34439
35072
|
if (!callExpr) continue;
|
|
34440
35073
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34441
35074
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
34442
|
-
const declarator = getUseStateDeclarator(ref);
|
|
35075
|
+
const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34443
35076
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
34444
35077
|
}
|
|
34445
35078
|
for (const ref of effectFnRefs) {
|
|
@@ -34448,7 +35081,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34448
35081
|
if (!callExpr) continue;
|
|
34449
35082
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34450
35083
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
34451
|
-
const setterDeclarator = getUseStateDeclarator(ref);
|
|
35084
|
+
const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34452
35085
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
34453
35086
|
const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
|
|
34454
35087
|
const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
|
|
@@ -36289,7 +36922,7 @@ const noDerivedState = defineRule({
|
|
|
36289
36922
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36290
36923
|
} }).visitors,
|
|
36291
36924
|
CallExpression(node) {
|
|
36292
|
-
if (!
|
|
36925
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
36293
36926
|
const analysis = getProgramAnalysis(node);
|
|
36294
36927
|
if (!analysis) return;
|
|
36295
36928
|
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
@@ -36309,7 +36942,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
36309
36942
|
tags: ["test-noise"],
|
|
36310
36943
|
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",
|
|
36311
36944
|
create: (context) => ({ CallExpression(node) {
|
|
36312
|
-
if (!
|
|
36945
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
36313
36946
|
const analysis = getProgramAnalysis(node);
|
|
36314
36947
|
if (!analysis) return;
|
|
36315
36948
|
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
@@ -36432,7 +37065,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
|
|
|
36432
37065
|
if (isFunctionLike$1(cursor)) {
|
|
36433
37066
|
const parent = cursor.parent ?? null;
|
|
36434
37067
|
if (parent && isNodeOfType(parent, "CallExpression")) {
|
|
36435
|
-
const calleeName = getCalleeName$
|
|
37068
|
+
const calleeName = getCalleeName$1(parent);
|
|
36436
37069
|
if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
|
|
36437
37070
|
}
|
|
36438
37071
|
}
|
|
@@ -36503,7 +37136,7 @@ const isNonHandlerHookCallback = (functionNode) => {
|
|
|
36503
37136
|
const parent = functionNode.parent ?? null;
|
|
36504
37137
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
36505
37138
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
36506
|
-
const calleeName = getCalleeName$
|
|
37139
|
+
const calleeName = getCalleeName$1(parent);
|
|
36507
37140
|
return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
|
|
36508
37141
|
};
|
|
36509
37142
|
const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
@@ -36585,7 +37218,7 @@ const noDerivedUseState = defineRule({
|
|
|
36585
37218
|
return {
|
|
36586
37219
|
...propStackTracker.visitors,
|
|
36587
37220
|
CallExpression(node) {
|
|
36588
|
-
if (!
|
|
37221
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
36589
37222
|
const seed = unwrapInitializerSeed(node.arguments[0]);
|
|
36590
37223
|
const reportStalePropCopy = (propName) => {
|
|
36591
37224
|
if (isIntentionalSnapshotState(node)) return;
|
|
@@ -37261,7 +37894,7 @@ const noDirectMutationState = defineRule({
|
|
|
37261
37894
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
37262
37895
|
//#endregion
|
|
37263
37896
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
37264
|
-
const collectUseStateBindings = (componentBody) => {
|
|
37897
|
+
const collectUseStateBindings = (componentBody, scopes) => {
|
|
37265
37898
|
const bindings = [];
|
|
37266
37899
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
37267
37900
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -37274,7 +37907,7 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
37274
37907
|
const setterElement = elements[1];
|
|
37275
37908
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37276
37909
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37277
|
-
if (!
|
|
37910
|
+
if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
|
|
37278
37911
|
bindings.push({
|
|
37279
37912
|
valueName: valueElement.name,
|
|
37280
37913
|
setterName: setterElement.name,
|
|
@@ -37441,7 +38074,7 @@ const noDirectStateMutation = defineRule({
|
|
|
37441
38074
|
create: (context) => {
|
|
37442
38075
|
const checkComponent = (componentBody) => {
|
|
37443
38076
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
37444
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
38077
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
37445
38078
|
if (bindings.length === 0) return;
|
|
37446
38079
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
37447
38080
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
@@ -38001,25 +38634,31 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
38001
38634
|
};
|
|
38002
38635
|
//#endregion
|
|
38003
38636
|
//#region src/plugin/rules/state-and-effects/no-effect-chain.ts
|
|
38004
|
-
const findTopLevelEffectCalls = (componentBody) => {
|
|
38637
|
+
const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
38005
38638
|
const effectCalls = [];
|
|
38006
38639
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
38007
38640
|
for (const statement of componentBody.body ?? []) {
|
|
38008
38641
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38009
38642
|
const expression = unwrapDiscardedExpression(statement);
|
|
38010
38643
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
38011
|
-
if (!
|
|
38644
|
+
if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
|
|
38012
38645
|
effectCalls.push(expression);
|
|
38013
38646
|
}
|
|
38014
38647
|
return effectCalls;
|
|
38015
38648
|
};
|
|
38016
|
-
const
|
|
38017
|
-
const
|
|
38018
|
-
if (!isNodeOfType(effectNode, "CallExpression")) return
|
|
38649
|
+
const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
|
|
38650
|
+
const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
|
|
38651
|
+
if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
|
|
38019
38652
|
const depsNode = effectNode.arguments?.[1];
|
|
38020
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) return
|
|
38021
|
-
for (const element of depsNode.elements ?? [])
|
|
38022
|
-
|
|
38653
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyStateSymbolIds;
|
|
38654
|
+
for (const element of depsNode.elements ?? []) {
|
|
38655
|
+
if (!element || isNodeOfType(element, "SpreadElement")) continue;
|
|
38656
|
+
const rootIdentifier = getRootIdentifier$1(element);
|
|
38657
|
+
if (!isNodeOfType(rootIdentifier, "Identifier")) continue;
|
|
38658
|
+
const symbol = resolveConstIdentifierAlias(rootIdentifier, scopes, true);
|
|
38659
|
+
if (symbol && stateSymbolIds.has(symbol.id)) dependencyStateSymbolIds.add(symbol.id);
|
|
38660
|
+
}
|
|
38661
|
+
return dependencyStateSymbolIds;
|
|
38023
38662
|
};
|
|
38024
38663
|
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
38025
38664
|
const analysisFunctions = new Set([effectCallback]);
|
|
@@ -38125,12 +38764,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
|
|
|
38125
38764
|
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
38126
38765
|
return readStaticEffectValue(argument, scopes, null, null);
|
|
38127
38766
|
};
|
|
38128
|
-
const collectStateWritesInEffect = (analysisFunctions,
|
|
38767
|
+
const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
|
|
38129
38768
|
const stateWrites = /* @__PURE__ */ new Map();
|
|
38130
38769
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38131
38770
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38132
38771
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
38133
|
-
const
|
|
38772
|
+
const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
|
|
38773
|
+
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
38134
38774
|
if (!stateName) return;
|
|
38135
38775
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
38136
38776
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -38216,11 +38856,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
38216
38856
|
"keys",
|
|
38217
38857
|
"values"
|
|
38218
38858
|
]);
|
|
38219
|
-
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
38859
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
38220
38860
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
38221
38861
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
38222
38862
|
if (isNodeOfType(returnedValue.callee, "Identifier")) {
|
|
38223
|
-
|
|
38863
|
+
const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
|
|
38864
|
+
if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
38224
38865
|
if (isSetterIdentifier(returnedValue.callee.name)) return true;
|
|
38225
38866
|
}
|
|
38226
38867
|
return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
@@ -38244,7 +38885,7 @@ const collectStorageHookSetterNames = (componentBody) => {
|
|
|
38244
38885
|
for (const declarator of statement.declarations ?? []) {
|
|
38245
38886
|
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
38246
38887
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
38247
|
-
const calleeName = getCalleeName$
|
|
38888
|
+
const calleeName = getCalleeName$1(declarator.init);
|
|
38248
38889
|
if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
|
|
38249
38890
|
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
|
|
38250
38891
|
}
|
|
@@ -38387,11 +39028,11 @@ const isExternalSyncNode = (node) => {
|
|
|
38387
39028
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
38388
39029
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
38389
39030
|
};
|
|
38390
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
39031
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
38391
39032
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
38392
39033
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
38393
|
-
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
38394
|
-
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
39034
|
+
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, setterSymbolIdToStateName, scopes, false)) return true;
|
|
39035
|
+
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
|
|
38395
39036
|
let didFindExternalCall = false;
|
|
38396
39037
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38397
39038
|
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
@@ -38407,10 +39048,11 @@ const noEffectChain = defineRule({
|
|
|
38407
39048
|
create: (context) => {
|
|
38408
39049
|
const checkComponent = (componentBody) => {
|
|
38409
39050
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38410
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
39051
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
38411
39052
|
if (useStateBindings.length === 0) return;
|
|
38412
39053
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
38413
39054
|
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
39055
|
+
const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
|
|
38414
39056
|
for (const binding of useStateBindings) {
|
|
38415
39057
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
38416
39058
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
@@ -38419,21 +39061,27 @@ const noEffectChain = defineRule({
|
|
|
38419
39061
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
38420
39062
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
38421
39063
|
}
|
|
39064
|
+
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39065
|
+
if (isNodeOfType(setterIdentifier, "Identifier")) {
|
|
39066
|
+
const setterSymbol = context.scopes.symbolFor(setterIdentifier);
|
|
39067
|
+
if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
|
|
39068
|
+
}
|
|
38422
39069
|
}
|
|
38423
39070
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
39071
|
+
const stateSymbolIdSet = new Set(stateSymbolIds.values());
|
|
38424
39072
|
const effectInfos = [];
|
|
38425
|
-
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
39073
|
+
for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
|
|
38426
39074
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
38427
39075
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
38428
39076
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
38429
|
-
const stateWrites = collectStateWritesInEffect(analysisFunctions,
|
|
39077
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
|
|
38430
39078
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
38431
39079
|
effectInfos.push({
|
|
38432
39080
|
node: effectCall,
|
|
38433
|
-
|
|
39081
|
+
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
38434
39082
|
stateWrites,
|
|
38435
39083
|
analysisFunctions,
|
|
38436
|
-
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
39084
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
38437
39085
|
});
|
|
38438
39086
|
}
|
|
38439
39087
|
if (effectInfos.length < 2) return;
|
|
@@ -38444,10 +39092,11 @@ const noEffectChain = defineRule({
|
|
|
38444
39092
|
for (const readerEffect of effectInfos) {
|
|
38445
39093
|
if (readerEffect === writerEffect) continue;
|
|
38446
39094
|
if (readerEffect.isExternalSync) continue;
|
|
38447
|
-
if (readerEffect.
|
|
39095
|
+
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
38448
39096
|
let chainedStateName = null;
|
|
38449
39097
|
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
38450
|
-
|
|
39098
|
+
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39099
|
+
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
38451
39100
|
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
38452
39101
|
chainedStateName = writtenName;
|
|
38453
39102
|
break;
|
|
@@ -38724,7 +39373,7 @@ const noEffectEventHandler = defineRule({
|
|
|
38724
39373
|
return {
|
|
38725
39374
|
...propStackTracker.visitors,
|
|
38726
39375
|
CallExpression(node) {
|
|
38727
|
-
if (!
|
|
39376
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
38728
39377
|
const callback = getEffectCallback(node);
|
|
38729
39378
|
if (!callback) return;
|
|
38730
39379
|
const analysis = getProgramAnalysis(node);
|
|
@@ -38844,14 +39493,14 @@ const noEffectEventInDeps = defineRule({
|
|
|
38844
39493
|
if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
|
|
38845
39494
|
const initializer = declaratorNode.init;
|
|
38846
39495
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
38847
|
-
if (!
|
|
39496
|
+
if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
|
|
38848
39497
|
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
38849
39498
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
38850
39499
|
} });
|
|
38851
39500
|
return {
|
|
38852
39501
|
...componentBindings.visitors,
|
|
38853
39502
|
CallExpression(node) {
|
|
38854
|
-
if (!
|
|
39503
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
38855
39504
|
if (!componentBindings.isInsideComponent()) return;
|
|
38856
39505
|
const depsNode = node.arguments[1];
|
|
38857
39506
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -38908,7 +39557,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
38908
39557
|
node: finding.reportNode,
|
|
38909
39558
|
message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
|
|
38910
39559
|
});
|
|
38911
|
-
if (!
|
|
39560
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
38912
39561
|
const args = node.arguments ?? [];
|
|
38913
39562
|
if (args.length < 2) return;
|
|
38914
39563
|
const depsNode = args[1];
|
|
@@ -39169,7 +39818,7 @@ const noEventHandler = defineRule({
|
|
|
39169
39818
|
severity: "warn",
|
|
39170
39819
|
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",
|
|
39171
39820
|
create: (context) => ({ CallExpression(node) {
|
|
39172
|
-
if (!
|
|
39821
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
39173
39822
|
const analysis = getProgramAnalysis(node);
|
|
39174
39823
|
if (!analysis || hasCleanup(analysis, node)) return;
|
|
39175
39824
|
const frames = collectBoundedEffectExecutionFrames(analysis, node);
|
|
@@ -39631,25 +40280,25 @@ const addDeclarationBindings = (statement, scope) => {
|
|
|
39631
40280
|
}
|
|
39632
40281
|
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
|
|
39633
40282
|
};
|
|
39634
|
-
const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40283
|
+
const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39635
40284
|
let hasReturn = false;
|
|
39636
|
-
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
|
|
40285
|
+
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, scopes, eventHandlerReferenceNames)) hasReturn = true;
|
|
39637
40286
|
else addDeclarationBindings(statement, scope);
|
|
39638
40287
|
return hasReturn;
|
|
39639
40288
|
};
|
|
39640
|
-
const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
|
|
40289
|
+
const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
|
|
39641
40290
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
39642
40291
|
if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
|
|
39643
40292
|
return true;
|
|
39644
40293
|
}
|
|
39645
|
-
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) && !
|
|
40294
|
+
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)) {
|
|
39646
40295
|
for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
|
|
39647
40296
|
return false;
|
|
39648
40297
|
}
|
|
39649
|
-
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
|
|
40298
|
+
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), scopes, eventHandlerReferenceNames);
|
|
39650
40299
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39651
|
-
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
|
|
39652
|
-
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
|
|
40300
|
+
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, scopes, eventHandlerReferenceNames);
|
|
40301
|
+
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
39653
40302
|
if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39654
40303
|
return consequentHasReturn || alternateHasReturn;
|
|
39655
40304
|
}
|
|
@@ -39657,7 +40306,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39657
40306
|
let hasReturn = false;
|
|
39658
40307
|
for (const switchCase of statement.cases ?? []) {
|
|
39659
40308
|
const caseScope = createBlockBindingScope(scope);
|
|
39660
|
-
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
|
|
40309
|
+
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
|
|
39661
40310
|
hasReturn = true;
|
|
39662
40311
|
if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
|
|
39663
40312
|
}
|
|
@@ -39665,25 +40314,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39665
40314
|
return hasReturn;
|
|
39666
40315
|
}
|
|
39667
40316
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39668
|
-
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
|
|
39669
|
-
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
|
|
39670
|
-
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
|
|
40317
|
+
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, scopes, eventHandlerReferenceNames);
|
|
40318
|
+
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
40319
|
+
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
39671
40320
|
return blockHasReturn || handlerHasReturn || finalizerHasReturn;
|
|
39672
40321
|
}
|
|
39673
40322
|
if (isNodeOfType(statement, "CatchClause")) {
|
|
39674
40323
|
const catchScope = createBlockBindingScope(scope);
|
|
39675
40324
|
addPatternBindings(statement.param, catchScope);
|
|
39676
|
-
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
|
|
40325
|
+
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
|
|
39677
40326
|
}
|
|
39678
40327
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39679
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40328
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39680
40329
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39681
40330
|
return bodyHasReturn;
|
|
39682
40331
|
}
|
|
39683
40332
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39684
40333
|
const loopScope = createBlockBindingScope(scope);
|
|
39685
40334
|
if (statement.init) addDeclarationBindings(statement.init, loopScope);
|
|
39686
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40335
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39687
40336
|
if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
|
|
39688
40337
|
if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
|
|
39689
40338
|
if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
|
|
@@ -39693,22 +40342,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39693
40342
|
const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
|
|
39694
40343
|
const loopScope = createBlockBindingScope(scope);
|
|
39695
40344
|
if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
|
|
39696
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40345
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39697
40346
|
addNames(names, rightNames);
|
|
39698
40347
|
return true;
|
|
39699
40348
|
}
|
|
39700
|
-
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40349
|
+
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39701
40350
|
if (isNodeOfType(statement, "WithStatement")) {
|
|
39702
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40351
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39703
40352
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
|
|
39704
40353
|
return bodyHasReturn;
|
|
39705
40354
|
}
|
|
39706
40355
|
return false;
|
|
39707
40356
|
};
|
|
39708
|
-
const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40357
|
+
const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39709
40358
|
const names = /* @__PURE__ */ new Set();
|
|
39710
40359
|
if (!isNodeOfType(componentBody, "BlockStatement")) return names;
|
|
39711
|
-
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
|
|
40360
|
+
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
|
|
39712
40361
|
return names;
|
|
39713
40362
|
};
|
|
39714
40363
|
//#endregion
|
|
@@ -39731,42 +40380,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
|
|
|
39731
40380
|
};
|
|
39732
40381
|
//#endregion
|
|
39733
40382
|
//#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
|
|
39734
|
-
const
|
|
39735
|
-
|
|
39736
|
-
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
39737
|
-
if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
|
|
39738
|
-
return null;
|
|
39739
|
-
};
|
|
39740
|
-
const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
|
|
39741
|
-
if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
|
|
40383
|
+
const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
|
|
40384
|
+
if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
|
|
39742
40385
|
if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
|
|
39743
40386
|
const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
|
|
39744
40387
|
return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
|
|
39745
40388
|
};
|
|
39746
|
-
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
|
|
40389
|
+
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
|
|
39747
40390
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
39748
40391
|
for (const property of node.properties ?? []) {
|
|
39749
40392
|
if (!isNodeOfType(property, "Property")) continue;
|
|
39750
40393
|
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
39751
40394
|
if (!propertyName) continue;
|
|
39752
|
-
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
40395
|
+
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
|
|
39753
40396
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
39754
40397
|
}
|
|
39755
40398
|
};
|
|
39756
|
-
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40399
|
+
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39757
40400
|
if (!isNodeOfType(statement, "VariableDeclaration")) return;
|
|
39758
40401
|
const declarationScope = getVariableDeclarationScope(statement, scope);
|
|
39759
40402
|
for (const declarator of statement.declarations ?? []) {
|
|
39760
40403
|
const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
|
|
39761
40404
|
if (!declarator.init) continue;
|
|
39762
|
-
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
|
|
40405
|
+
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39763
40406
|
for (const declaredBindingName of declaredBindingNames) {
|
|
39764
40407
|
if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
|
|
39765
|
-
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
|
|
40408
|
+
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39766
40409
|
}
|
|
39767
40410
|
}
|
|
39768
40411
|
};
|
|
39769
|
-
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40412
|
+
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39770
40413
|
if (isNodeOfType(statement, "FunctionDeclaration")) {
|
|
39771
40414
|
if (statement.id) {
|
|
39772
40415
|
const declaredBindingNames = addPatternBindings(statement.id, scope);
|
|
@@ -39775,61 +40418,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
|
|
|
39775
40418
|
return;
|
|
39776
40419
|
}
|
|
39777
40420
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
39778
|
-
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40421
|
+
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
39779
40422
|
return;
|
|
39780
40423
|
}
|
|
39781
40424
|
if (isNodeOfType(statement, "BlockStatement")) {
|
|
39782
|
-
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40425
|
+
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
39783
40426
|
return;
|
|
39784
40427
|
}
|
|
39785
40428
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39786
|
-
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
|
|
39787
|
-
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
|
|
40429
|
+
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope, scopes);
|
|
40430
|
+
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope, scopes);
|
|
39788
40431
|
return;
|
|
39789
40432
|
}
|
|
39790
40433
|
if (isNodeOfType(statement, "SwitchStatement")) {
|
|
39791
|
-
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40434
|
+
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
39792
40435
|
return;
|
|
39793
40436
|
}
|
|
39794
40437
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39795
|
-
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
|
|
40438
|
+
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
|
|
39796
40439
|
if (statement.handler) {
|
|
39797
40440
|
const catchScope = createBlockBindingScope(scope);
|
|
39798
40441
|
addPatternBindings(statement.handler.param, catchScope);
|
|
39799
|
-
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
|
|
40442
|
+
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
|
|
39800
40443
|
}
|
|
39801
|
-
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
|
|
40444
|
+
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
|
|
39802
40445
|
return;
|
|
39803
40446
|
}
|
|
39804
40447
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39805
40448
|
const loopScope = createBlockBindingScope(scope);
|
|
39806
|
-
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
|
|
39807
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40449
|
+
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope, scopes);
|
|
40450
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
39808
40451
|
return;
|
|
39809
40452
|
}
|
|
39810
40453
|
if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
|
|
39811
40454
|
const loopScope = createBlockBindingScope(scope);
|
|
39812
|
-
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
|
|
40455
|
+
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
|
|
39813
40456
|
else addPatternBindings(statement.left, loopScope);
|
|
39814
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40457
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
39815
40458
|
return;
|
|
39816
40459
|
}
|
|
39817
40460
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39818
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40461
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39819
40462
|
return;
|
|
39820
40463
|
}
|
|
39821
|
-
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40464
|
+
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39822
40465
|
};
|
|
39823
|
-
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
|
|
39824
|
-
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40466
|
+
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope, scopes) => {
|
|
40467
|
+
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
39825
40468
|
};
|
|
39826
|
-
const collectFunctionLikeLocalNames = (componentBody) => {
|
|
40469
|
+
const collectFunctionLikeLocalNames = (componentBody, scopes) => {
|
|
39827
40470
|
const functionLikeLocalNames = /* @__PURE__ */ new Set();
|
|
39828
40471
|
if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
|
|
39829
40472
|
let previousSize = -1;
|
|
39830
40473
|
while (previousSize !== functionLikeLocalNames.size) {
|
|
39831
40474
|
previousSize = functionLikeLocalNames.size;
|
|
39832
|
-
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
|
|
40475
|
+
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
|
|
39833
40476
|
}
|
|
39834
40477
|
return functionLikeLocalNames;
|
|
39835
40478
|
};
|
|
@@ -39869,17 +40512,17 @@ const noEventTriggerState = defineRule({
|
|
|
39869
40512
|
create: (context) => {
|
|
39870
40513
|
const checkComponent = (componentBody) => {
|
|
39871
40514
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
39872
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
40515
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
39873
40516
|
if (useStateBindings.length === 0) return;
|
|
39874
40517
|
const analysis = getProgramAnalysis(componentBody);
|
|
39875
40518
|
if (!analysis) return;
|
|
39876
40519
|
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
39877
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
40520
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
39878
40521
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
39879
|
-
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
40522
|
+
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
|
|
39880
40523
|
walkAst(componentBody, (effectCall) => {
|
|
39881
40524
|
if (!isNodeOfType(effectCall, "CallExpression")) return;
|
|
39882
|
-
if (!
|
|
40525
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
39883
40526
|
if ((effectCall.arguments?.length ?? 0) < 2) return;
|
|
39884
40527
|
const depsNode = effectCall.arguments[1];
|
|
39885
40528
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -40263,6 +40906,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
40263
40906
|
"scrollHeight"
|
|
40264
40907
|
]);
|
|
40265
40908
|
const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
|
|
40909
|
+
const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
|
|
40910
|
+
"blur",
|
|
40911
|
+
"focus",
|
|
40912
|
+
"restoreSelection",
|
|
40913
|
+
"scroll",
|
|
40914
|
+
"scrollBy",
|
|
40915
|
+
"scrollIntoView",
|
|
40916
|
+
"scrollTo",
|
|
40917
|
+
"setRangeText",
|
|
40918
|
+
"setSelectionRange"
|
|
40919
|
+
]);
|
|
40266
40920
|
const subtreeReadsDomMeasurement = (root) => {
|
|
40267
40921
|
if (!root) return false;
|
|
40268
40922
|
let found = false;
|
|
@@ -40281,29 +40935,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
40281
40935
|
});
|
|
40282
40936
|
return found;
|
|
40283
40937
|
};
|
|
40284
|
-
const
|
|
40938
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
40285
40939
|
const names = /* @__PURE__ */ new Set();
|
|
40286
40940
|
walkAst(program, (child) => {
|
|
40287
40941
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
40288
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40942
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
40289
40943
|
return;
|
|
40290
40944
|
}
|
|
40291
40945
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
40292
40946
|
let functionValue = child.init;
|
|
40293
40947
|
if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
|
|
40294
|
-
if (functionValue && isFunctionLike$1(functionValue) &&
|
|
40948
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40295
40949
|
});
|
|
40296
40950
|
return names;
|
|
40297
40951
|
};
|
|
40298
|
-
const
|
|
40299
|
-
|
|
40952
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40953
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40954
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
40955
|
+
let found = false;
|
|
40956
|
+
walkAst(root, (child) => {
|
|
40957
|
+
if (found) return false;
|
|
40958
|
+
if (child !== root && isFunctionLike$1(child)) return false;
|
|
40959
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40960
|
+
const callee = stripParenExpression(child.callee);
|
|
40961
|
+
const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
40962
|
+
if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
|
|
40963
|
+
found = true;
|
|
40964
|
+
return false;
|
|
40965
|
+
}
|
|
40966
|
+
});
|
|
40967
|
+
return found;
|
|
40968
|
+
};
|
|
40969
|
+
const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
|
|
40970
|
+
const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
|
|
40971
|
+
if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
|
|
40300
40972
|
let found = false;
|
|
40301
40973
|
walkAst(root, (child) => {
|
|
40302
40974
|
if (found) return false;
|
|
40975
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
40303
40976
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
40304
40977
|
});
|
|
40305
40978
|
return found;
|
|
40306
40979
|
};
|
|
40980
|
+
const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
|
|
40981
|
+
let statement = call;
|
|
40982
|
+
let parent = statement.parent;
|
|
40983
|
+
while (parent) {
|
|
40984
|
+
const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
|
|
40985
|
+
if (statements) {
|
|
40986
|
+
const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
|
|
40987
|
+
if (statementIndex >= 0) {
|
|
40988
|
+
const nextStatement = statements[statementIndex + 1];
|
|
40989
|
+
return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
|
|
40990
|
+
}
|
|
40991
|
+
}
|
|
40992
|
+
if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
40993
|
+
statement = parent;
|
|
40994
|
+
parent = parent.parent;
|
|
40995
|
+
}
|
|
40996
|
+
return false;
|
|
40997
|
+
};
|
|
40307
40998
|
const isInsideStartViewTransition = (node) => {
|
|
40308
40999
|
let cursor = node.parent;
|
|
40309
41000
|
while (cursor) {
|
|
@@ -40344,11 +41035,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
40344
41035
|
};
|
|
40345
41036
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
40346
41037
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
41038
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
40347
41039
|
let exempt = false;
|
|
40348
41040
|
walkAst(program, (child) => {
|
|
40349
41041
|
if (exempt) return false;
|
|
40350
41042
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
40351
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
41043
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
40352
41044
|
exempt = true;
|
|
40353
41045
|
return false;
|
|
40354
41046
|
}
|
|
@@ -40913,41 +41605,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40913
41605
|
if (leftResult === false && rightResult === false) return false;
|
|
40914
41606
|
return null;
|
|
40915
41607
|
};
|
|
40916
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41608
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40917
41609
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40918
41610
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40919
41611
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40920
41612
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40921
41613
|
if (staticResult !== null) return staticResult;
|
|
41614
|
+
const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
|
|
41615
|
+
const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
|
|
41616
|
+
if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41617
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41618
|
+
const result = readHydrationConditionResult(parameterValue, context, runtime, state);
|
|
41619
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41620
|
+
return result;
|
|
41621
|
+
}
|
|
41622
|
+
if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41623
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41624
|
+
const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
|
|
41625
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41626
|
+
return result;
|
|
41627
|
+
}
|
|
41628
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41629
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41630
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41631
|
+
allowGlobalReactNamespace: true,
|
|
41632
|
+
resolveNamedAliases: true
|
|
41633
|
+
})) {
|
|
41634
|
+
const callbackArgument = callArguments[0];
|
|
41635
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41636
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41637
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
|
|
41638
|
+
}
|
|
41639
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41640
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
|
|
41641
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41642
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41643
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41644
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41645
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41646
|
+
const argument = callArguments[parameterIndex];
|
|
41647
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41648
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41649
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41650
|
+
}
|
|
41651
|
+
return readHydrationFunctionResult(helperFunction, context, runtime, {
|
|
41652
|
+
...state,
|
|
41653
|
+
parameterValuesBySymbolId
|
|
41654
|
+
});
|
|
41655
|
+
}
|
|
40922
41656
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40923
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41657
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40924
41658
|
return argumentResult === null ? null : !argumentResult;
|
|
40925
41659
|
}
|
|
40926
41660
|
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
40927
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
41661
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
|
|
41662
|
+
};
|
|
41663
|
+
const readHydrationStatementResult = (statement, context, runtime, state) => {
|
|
41664
|
+
if (isNodeOfType(statement, "ReturnStatement")) return {
|
|
41665
|
+
didReturn: true,
|
|
41666
|
+
value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
|
|
41667
|
+
};
|
|
41668
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
41669
|
+
for (const childStatement of statement.body) {
|
|
41670
|
+
const result = readHydrationStatementResult(childStatement, context, runtime, state);
|
|
41671
|
+
if (result.didReturn) return result;
|
|
41672
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41673
|
+
}
|
|
41674
|
+
return {
|
|
41675
|
+
didReturn: false,
|
|
41676
|
+
value: null
|
|
41677
|
+
};
|
|
41678
|
+
}
|
|
41679
|
+
if (!isNodeOfType(statement, "IfStatement")) return {
|
|
41680
|
+
didReturn: false,
|
|
41681
|
+
value: null
|
|
41682
|
+
};
|
|
41683
|
+
const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
|
|
41684
|
+
if (conditionResult !== null) {
|
|
41685
|
+
const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
|
|
41686
|
+
return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
|
|
41687
|
+
didReturn: false,
|
|
41688
|
+
value: null
|
|
41689
|
+
};
|
|
41690
|
+
}
|
|
41691
|
+
const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
|
|
41692
|
+
const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
|
|
41693
|
+
didReturn: false,
|
|
41694
|
+
value: null
|
|
41695
|
+
};
|
|
41696
|
+
return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
|
|
41697
|
+
didReturn: consequentResult.didReturn || alternateResult.didReturn,
|
|
41698
|
+
value: null
|
|
41699
|
+
};
|
|
41700
|
+
};
|
|
41701
|
+
const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
|
|
41702
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41703
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41704
|
+
const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
|
|
41705
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41706
|
+
return result;
|
|
41707
|
+
};
|
|
41708
|
+
const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
|
|
41709
|
+
const left = stripParenExpression(leftExpression);
|
|
41710
|
+
const right = stripParenExpression(rightExpression);
|
|
41711
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
41712
|
+
const leftSymbol = scopes.symbolFor(left);
|
|
41713
|
+
const rightSymbol = scopes.symbolFor(right);
|
|
41714
|
+
return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
|
|
41715
|
+
}
|
|
41716
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
|
|
41717
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
41718
|
+
const rightArguments = right.arguments ?? [];
|
|
41719
|
+
return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
|
|
41720
|
+
const rightArgument = rightArguments[index];
|
|
41721
|
+
return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
|
|
41722
|
+
});
|
|
41723
|
+
}
|
|
41724
|
+
return true;
|
|
41725
|
+
};
|
|
41726
|
+
const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
|
|
41727
|
+
if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
|
|
41728
|
+
const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
|
|
41729
|
+
const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
|
|
41730
|
+
return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
|
|
40928
41731
|
};
|
|
40929
|
-
const
|
|
41732
|
+
const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
|
|
41733
|
+
const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
|
|
41734
|
+
return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
|
|
41735
|
+
};
|
|
41736
|
+
const matchHydrationConditionInternal = (expression, context, state) => {
|
|
40930
41737
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40931
41738
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40932
41739
|
if (predicateMatch) return {
|
|
40933
41740
|
predicateMatch,
|
|
40934
41741
|
predicateNode: unwrappedExpression
|
|
40935
41742
|
};
|
|
40936
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40937
|
-
|
|
40938
|
-
|
|
40939
|
-
|
|
40940
|
-
|
|
40941
|
-
|
|
40942
|
-
|
|
40943
|
-
|
|
41743
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
41744
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
41745
|
+
const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
|
|
41746
|
+
if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
|
|
41747
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41748
|
+
const match = matchHydrationConditionInternal(parameterValue, context, state);
|
|
41749
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41750
|
+
return match;
|
|
41751
|
+
}
|
|
41752
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
|
|
41753
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41754
|
+
const match = matchHydrationConditionInternal(symbol.initializer, context, state);
|
|
41755
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41756
|
+
return match;
|
|
41757
|
+
}
|
|
41758
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41759
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41760
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41761
|
+
allowGlobalReactNamespace: true,
|
|
41762
|
+
resolveNamedAliases: true
|
|
41763
|
+
})) {
|
|
41764
|
+
const callbackArgument = callArguments[0];
|
|
41765
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41766
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41767
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
|
|
41768
|
+
}
|
|
41769
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41770
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
|
|
41771
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41772
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41773
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41774
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41775
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41776
|
+
const argument = callArguments[parameterIndex];
|
|
41777
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41778
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41779
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41780
|
+
}
|
|
41781
|
+
return matchHydrationFunctionResult(helperFunction, context, {
|
|
41782
|
+
...state,
|
|
41783
|
+
parameterValuesBySymbolId
|
|
41784
|
+
});
|
|
40944
41785
|
}
|
|
41786
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
|
|
41787
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
41788
|
+
const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
|
|
41789
|
+
const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
|
|
40945
41790
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40946
41791
|
if (!nestedMatch) return null;
|
|
40947
|
-
const
|
|
40948
|
-
|
|
40949
|
-
return nestedMatch;
|
|
41792
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
|
|
41793
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
|
|
41794
|
+
return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
|
|
40950
41795
|
};
|
|
41796
|
+
const matchHydrationReturningStatement = (statement, context, state) => {
|
|
41797
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
|
|
41798
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
41799
|
+
const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
|
|
41800
|
+
const consequentValues = getReturnedValues(statement.consequent);
|
|
41801
|
+
const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
|
|
41802
|
+
if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
|
|
41803
|
+
return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
|
|
41804
|
+
}
|
|
41805
|
+
if (!isNodeOfType(statement, "BlockStatement")) return null;
|
|
41806
|
+
for (const childStatement of statement.body) {
|
|
41807
|
+
const match = matchHydrationReturningStatement(childStatement, context, state);
|
|
41808
|
+
if (match) return match;
|
|
41809
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41810
|
+
}
|
|
41811
|
+
return null;
|
|
41812
|
+
};
|
|
41813
|
+
const matchHydrationFunctionResult = (functionNode, context, state) => {
|
|
41814
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41815
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41816
|
+
const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
|
|
41817
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41818
|
+
return match;
|
|
41819
|
+
};
|
|
41820
|
+
const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
|
|
41821
|
+
parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
|
|
41822
|
+
visitedFunctionNodes: /* @__PURE__ */ new Set(),
|
|
41823
|
+
visitedSymbolIds: /* @__PURE__ */ new Set()
|
|
41824
|
+
});
|
|
40951
41825
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40952
41826
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40953
41827
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -41090,17 +41964,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
41090
41964
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
41091
41965
|
if (reportedNodes.has(predicateNode)) return;
|
|
41092
41966
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
41093
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41967
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
41094
41968
|
if (!componentOrHookNode) return;
|
|
41095
41969
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
41096
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41970
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41097
41971
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
41098
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41972
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
41099
41973
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
41100
41974
|
}
|
|
41101
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
41102
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
41103
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41975
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41976
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41977
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
41104
41978
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
41105
41979
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
41106
41980
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -41477,7 +42351,7 @@ const noInitializeState = defineRule({
|
|
|
41477
42351
|
tags: ["test-noise"],
|
|
41478
42352
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
41479
42353
|
create: (context) => ({ CallExpression(node) {
|
|
41480
|
-
if (!
|
|
42354
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
41481
42355
|
const dependencies = node.arguments?.[1];
|
|
41482
42356
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41483
42357
|
const analysis = getProgramAnalysis(node);
|
|
@@ -41840,7 +42714,8 @@ const noJsxElementType = defineRule({
|
|
|
41840
42714
|
create: (context) => {
|
|
41841
42715
|
let isJsxImported = false;
|
|
41842
42716
|
const flaggedAnnotations = [];
|
|
41843
|
-
const
|
|
42717
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42718
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
41844
42719
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
41845
42720
|
if (!typeAnnotation) return;
|
|
41846
42721
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -41850,19 +42725,16 @@ const noJsxElementType = defineRule({
|
|
|
41850
42725
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
41851
42726
|
},
|
|
41852
42727
|
FunctionDeclaration(node) {
|
|
41853
|
-
|
|
42728
|
+
collectComponentReturnType(node, node.returnType);
|
|
41854
42729
|
},
|
|
41855
42730
|
ArrowFunctionExpression(node) {
|
|
41856
|
-
|
|
42731
|
+
collectComponentReturnType(node, node.returnType);
|
|
41857
42732
|
},
|
|
41858
42733
|
FunctionExpression(node) {
|
|
41859
|
-
|
|
42734
|
+
collectComponentReturnType(node, node.returnType);
|
|
41860
42735
|
},
|
|
41861
42736
|
TSDeclareFunction(node) {
|
|
41862
|
-
|
|
41863
|
-
},
|
|
41864
|
-
TSMethodSignature(node) {
|
|
41865
|
-
checkReturnType(node.returnType);
|
|
42737
|
+
collectComponentReturnType(node, node.returnType);
|
|
41866
42738
|
},
|
|
41867
42739
|
"Program:exit"() {
|
|
41868
42740
|
if (isJsxImported) return;
|
|
@@ -43091,7 +43963,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43091
43963
|
const setterElement = elements[1];
|
|
43092
43964
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43093
43965
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43094
|
-
if (!
|
|
43966
|
+
if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
|
|
43095
43967
|
const initializer = declarator.init.arguments?.[0];
|
|
43096
43968
|
if (!initializer) continue;
|
|
43097
43969
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43109,7 +43981,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43109
43981
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43110
43982
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43111
43983
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43112
|
-
if (!
|
|
43984
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
43113
43985
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43114
43986
|
const depsNode = effectCall.arguments[1];
|
|
43115
43987
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -43518,7 +44390,7 @@ const noMultiComp = defineRule({
|
|
|
43518
44390
|
});
|
|
43519
44391
|
//#endregion
|
|
43520
44392
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
43521
|
-
const collectUseRefBindingNames = (componentBody) => {
|
|
44393
|
+
const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
43522
44394
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
43523
44395
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
43524
44396
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -43526,7 +44398,7 @@ const collectUseRefBindingNames = (componentBody) => {
|
|
|
43526
44398
|
for (const declarator of statement.declarations ?? []) {
|
|
43527
44399
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
43528
44400
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43529
|
-
if (!
|
|
44401
|
+
if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
|
|
43530
44402
|
useRefBindings.add(declarator.id.name);
|
|
43531
44403
|
}
|
|
43532
44404
|
}
|
|
@@ -43561,12 +44433,12 @@ const noMutableInDeps = defineRule({
|
|
|
43561
44433
|
create: (context) => {
|
|
43562
44434
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
43563
44435
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
43564
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44436
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
|
|
43565
44437
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
43566
44438
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
43567
44439
|
walkAst(componentBody, (child) => {
|
|
43568
44440
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
43569
|
-
if (!
|
|
44441
|
+
if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
43570
44442
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
43571
44443
|
const depsNode = child.arguments[1];
|
|
43572
44444
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -45396,6 +46268,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
45396
46268
|
"useMatchMedia",
|
|
45397
46269
|
"useMediaJobProgress",
|
|
45398
46270
|
"useMediaQuery",
|
|
46271
|
+
"useMediaQueryState",
|
|
45399
46272
|
"useResizeObserver",
|
|
45400
46273
|
"useVisibility",
|
|
45401
46274
|
"useWindowSize"
|
|
@@ -45432,9 +46305,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
45432
46305
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
45433
46306
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
45434
46307
|
};
|
|
45435
|
-
const
|
|
46308
|
+
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
46309
|
+
let hookFunction = resolveToFunction(ref);
|
|
46310
|
+
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46311
|
+
const definitionNode = definition.node;
|
|
46312
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
46313
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
46314
|
+
if (!isNodeOfType(initializer, "CallExpression")) continue;
|
|
46315
|
+
const callee = stripParenExpression(initializer.callee);
|
|
46316
|
+
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
46317
|
+
const calleeReference = getRef(analysis, callee);
|
|
46318
|
+
if (!calleeReference) continue;
|
|
46319
|
+
hookFunction = resolveToFunction(calleeReference);
|
|
46320
|
+
if (hookFunction) break;
|
|
46321
|
+
}
|
|
46322
|
+
if (!hookFunction) return null;
|
|
46323
|
+
const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46324
|
+
if (returnedReferences.length === 0) return null;
|
|
46325
|
+
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46326
|
+
};
|
|
46327
|
+
const isExternalSubscriptionHookRef = (analysis, ref) => {
|
|
45436
46328
|
const identifier = ref.identifier;
|
|
45437
46329
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46330
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46331
|
+
if (localHookProof !== null) return localHookProof;
|
|
45438
46332
|
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45439
46333
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
45440
46334
|
const node = def.node;
|
|
@@ -45457,16 +46351,10 @@ const noPassDataToParent = defineRule({
|
|
|
45457
46351
|
tags: ["test-noise"],
|
|
45458
46352
|
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",
|
|
45459
46353
|
create: (context) => {
|
|
45460
|
-
const isReactUseRefCall = (node) =>
|
|
45461
|
-
|
|
45462
|
-
allowUnboundBareCalls: true
|
|
45463
|
-
});
|
|
45464
|
-
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
45465
|
-
allowGlobalReactNamespace: true,
|
|
45466
|
-
allowUnboundBareCalls: true
|
|
45467
|
-
});
|
|
46354
|
+
const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
|
|
46355
|
+
const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
|
|
45468
46356
|
return { CallExpression(node) {
|
|
45469
|
-
if (!
|
|
46357
|
+
if (!isReactUseEffectCall(node)) return;
|
|
45470
46358
|
const analysis = getProgramAnalysis(node);
|
|
45471
46359
|
if (!analysis) return;
|
|
45472
46360
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -45518,11 +46406,11 @@ const noPassDataToParent = defineRule({
|
|
|
45518
46406
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
45519
46407
|
}
|
|
45520
46408
|
return getDownstreamRefs(analysis, argument);
|
|
45521
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46409
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
45522
46410
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45523
46411
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
45524
46412
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
45525
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46413
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
45526
46414
|
if (isProp(analysis, argRef)) return false;
|
|
45527
46415
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
45528
46416
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -45753,7 +46641,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
45753
46641
|
tags: ["test-noise"],
|
|
45754
46642
|
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",
|
|
45755
46643
|
create: (context) => ({ CallExpression(node) {
|
|
45756
|
-
if (!
|
|
46644
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
45757
46645
|
const analysis = getProgramAnalysis(node);
|
|
45758
46646
|
if (!analysis) return;
|
|
45759
46647
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -46168,7 +47056,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
46168
47056
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
46169
47057
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
46170
47058
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
46171
|
-
const calleeName = getCalleeName$
|
|
47059
|
+
const calleeName = getCalleeName$1(binding.initializer);
|
|
46172
47060
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
46173
47061
|
}
|
|
46174
47062
|
return false;
|
|
@@ -46190,7 +47078,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
46190
47078
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
46191
47079
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
46192
47080
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
46193
|
-
if (getCalleeName$
|
|
47081
|
+
if (getCalleeName$1(binding.initializer) !== "useRef") return null;
|
|
46194
47082
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
46195
47083
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
46196
47084
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -46222,7 +47110,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
46222
47110
|
return {
|
|
46223
47111
|
...propStackTracker.visitors,
|
|
46224
47112
|
CallExpression(node) {
|
|
46225
|
-
if (!
|
|
47113
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
46226
47114
|
const callback = getEffectCallback(node);
|
|
46227
47115
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
46228
47116
|
const depsNode = node.arguments[1];
|
|
@@ -47086,6 +47974,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
47086
47974
|
}
|
|
47087
47975
|
});
|
|
47088
47976
|
//#endregion
|
|
47977
|
+
//#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
|
|
47978
|
+
const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47979
|
+
const expression = stripParenExpression(rawExpression);
|
|
47980
|
+
if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
|
|
47981
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
47982
|
+
if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
|
|
47983
|
+
return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
|
|
47984
|
+
}
|
|
47985
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
47986
|
+
if (isNodeOfType(expression.left, "Literal")) {
|
|
47987
|
+
const isLeftTruthy = Boolean(expression.left.value);
|
|
47988
|
+
if (expression.operator === "&&" && !isLeftTruthy) return [];
|
|
47989
|
+
if (expression.operator === "||" && isLeftTruthy) return [];
|
|
47990
|
+
if (expression.operator === "??" && expression.left.value !== null) return [];
|
|
47991
|
+
}
|
|
47992
|
+
if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
|
|
47993
|
+
return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
|
|
47994
|
+
}
|
|
47995
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
47996
|
+
const finalExpression = expression.expressions.at(-1);
|
|
47997
|
+
return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
|
|
47998
|
+
}
|
|
47999
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
48000
|
+
if (!isReactApiCall(expression, "useCallback", scopes)) return [];
|
|
48001
|
+
const callback = expression.arguments[0];
|
|
48002
|
+
return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
|
|
48003
|
+
}
|
|
48004
|
+
if (!isNodeOfType(expression, "Identifier")) return [];
|
|
48005
|
+
const symbol = scopes.symbolFor(expression);
|
|
48006
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
|
|
48007
|
+
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
48008
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
48009
|
+
if (!initializer) return [];
|
|
48010
|
+
return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
48011
|
+
};
|
|
48012
|
+
const functionReturnsCleanupFunction = (functionExpression, scopes) => {
|
|
48013
|
+
if (!isFunctionLike$1(functionExpression)) return false;
|
|
48014
|
+
if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
|
|
48015
|
+
return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
|
|
48016
|
+
};
|
|
48017
|
+
const callbackReturnsCleanupFunction = (callback, scopes) => {
|
|
48018
|
+
return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
|
|
48019
|
+
};
|
|
48020
|
+
const noRefCallbackCleanupBeforeReact19 = defineRule({
|
|
48021
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
48022
|
+
title: "Ref cleanup requires React 19",
|
|
48023
|
+
requires: ["react:18"],
|
|
48024
|
+
disabledWhen: ["react:19"],
|
|
48025
|
+
severity: "warn",
|
|
48026
|
+
recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
|
|
48027
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
48028
|
+
if (getJsxAttributeName(node.name) !== "ref") return;
|
|
48029
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
48030
|
+
const callback = node.value.expression;
|
|
48031
|
+
if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
|
|
48032
|
+
if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
|
|
48033
|
+
context.report({
|
|
48034
|
+
node,
|
|
48035
|
+
message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
|
|
48036
|
+
});
|
|
48037
|
+
} })
|
|
48038
|
+
});
|
|
48039
|
+
//#endregion
|
|
47089
48040
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
47090
48041
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
47091
48042
|
"DoWhileStatement",
|
|
@@ -48052,7 +49003,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
48052
49003
|
tags: ["test-noise"],
|
|
48053
49004
|
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",
|
|
48054
49005
|
create: (context) => ({ CallExpression(node) {
|
|
48055
|
-
if (!
|
|
49006
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
48056
49007
|
const analysis = getProgramAnalysis(node);
|
|
48057
49008
|
if (!analysis) return;
|
|
48058
49009
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -48321,7 +49272,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
48321
49272
|
if (node.callee.property.name !== "handler") return false;
|
|
48322
49273
|
let currentNode = node.callee.object;
|
|
48323
49274
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
48324
|
-
const calleeName = getCalleeName$
|
|
49275
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
48325
49276
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
48326
49277
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
48327
49278
|
currentNode = currentNode.callee.object;
|
|
@@ -48822,7 +49773,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
48822
49773
|
create: (context) => {
|
|
48823
49774
|
const checkFunctionScope = (functionBody) => {
|
|
48824
49775
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
48825
|
-
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49776
|
+
const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
|
|
48826
49777
|
if (useStateBindings.length === 0) return;
|
|
48827
49778
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
48828
49779
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -48831,7 +49782,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
48831
49782
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
48832
49783
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
48833
49784
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
48834
|
-
if (!
|
|
49785
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
48835
49786
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
48836
49787
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
48837
49788
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -48917,7 +49868,7 @@ const noSetStateInRender = defineRule({
|
|
|
48917
49868
|
create: (context) => {
|
|
48918
49869
|
const checkComponent = (componentBody) => {
|
|
48919
49870
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
48920
|
-
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49871
|
+
const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
|
|
48921
49872
|
if (setterNames.size === 0) return;
|
|
48922
49873
|
for (const statement of componentBody.body ?? []) {
|
|
48923
49874
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -49166,10 +50117,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
49166
50117
|
});
|
|
49167
50118
|
return facts;
|
|
49168
50119
|
};
|
|
49169
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
50120
|
+
const isEffectCallbackFunction = (functionNode, scopes) => {
|
|
49170
50121
|
const parent = functionNode.parent;
|
|
49171
50122
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
49172
|
-
return
|
|
50123
|
+
return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
|
|
49173
50124
|
};
|
|
49174
50125
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
49175
50126
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -49188,24 +50139,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
49188
50139
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49189
50140
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
49190
50141
|
};
|
|
49191
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50142
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
|
|
49192
50143
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49193
50144
|
if (cleanupBindingName === null) return false;
|
|
49194
50145
|
let isReturnedFromEffect = false;
|
|
49195
50146
|
walkAst(ownerScope, (child) => {
|
|
49196
50147
|
if (isReturnedFromEffect) return false;
|
|
49197
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50148
|
+
if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
49198
50149
|
const effectCallback = getEffectCallback(child);
|
|
49199
50150
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
49200
50151
|
});
|
|
49201
50152
|
return isReturnedFromEffect;
|
|
49202
50153
|
};
|
|
49203
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50154
|
+
const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
|
|
49204
50155
|
let functionNode = findEnclosingFunction$1(node);
|
|
49205
50156
|
while (functionNode) {
|
|
49206
50157
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
49207
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
49208
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
50158
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50159
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
|
|
49209
50160
|
functionNode = outerFunction;
|
|
49210
50161
|
}
|
|
49211
50162
|
return false;
|
|
@@ -49241,10 +50192,10 @@ const noStaleTimerRef = defineRule({
|
|
|
49241
50192
|
if (isShadowedTimerGlobal(node)) return;
|
|
49242
50193
|
const { clearCalleeName, refName } = clearCall;
|
|
49243
50194
|
const refBinding = findVariableInitializer(node, refName);
|
|
49244
|
-
if (!refBinding?.initializer || !
|
|
50195
|
+
if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
|
|
49245
50196
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
49246
50197
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
49247
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50198
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
|
|
49248
50199
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
49249
50200
|
context.report({
|
|
49250
50201
|
node,
|
|
@@ -55048,7 +55999,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
55048
55999
|
allReadsAreInSubHandlers = false;
|
|
55049
56000
|
return;
|
|
55050
56001
|
}
|
|
55051
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
56002
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
|
|
55052
56003
|
});
|
|
55053
56004
|
return {
|
|
55054
56005
|
hasAnyRead,
|
|
@@ -55071,7 +56022,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
55071
56022
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
55072
56023
|
const effectCall = statement.expression;
|
|
55073
56024
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55074
|
-
if (!
|
|
56025
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
55075
56026
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55076
56027
|
const depsNode = effectCall.arguments[1];
|
|
55077
56028
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -55103,11 +56054,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
55103
56054
|
});
|
|
55104
56055
|
//#endregion
|
|
55105
56056
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
55106
|
-
const findUseEffectsInComponent = (componentBody) => {
|
|
56057
|
+
const findUseEffectsInComponent = (componentBody, scopes) => {
|
|
55107
56058
|
const effectCalls = [];
|
|
55108
56059
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
55109
56060
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
55110
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56061
|
+
if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
|
|
55111
56062
|
});
|
|
55112
56063
|
return effectCalls;
|
|
55113
56064
|
};
|
|
@@ -55310,7 +56261,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55310
56261
|
};
|
|
55311
56262
|
const checkComponent = (componentBody) => {
|
|
55312
56263
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
55313
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56264
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
55314
56265
|
if (useStateBindings.length === 0) return;
|
|
55315
56266
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
55316
56267
|
for (const binding of useStateBindings) {
|
|
@@ -55323,7 +56274,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55323
56274
|
}
|
|
55324
56275
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
55325
56276
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
55326
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56277
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55327
56278
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55328
56279
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55329
56280
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55365,7 +56316,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55365
56316
|
})).filter((candidate) => candidate.storeName !== null);
|
|
55366
56317
|
if (snapshotBindings.length === 0) return;
|
|
55367
56318
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
55368
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56319
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55369
56320
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55370
56321
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55371
56322
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55462,7 +56413,7 @@ const preferUseReducer = defineRule({
|
|
|
55462
56413
|
create: (context) => {
|
|
55463
56414
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
55464
56415
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
55465
|
-
const bindings = collectUseStateBindings(body);
|
|
56416
|
+
const bindings = collectUseStateBindings(body, context.scopes);
|
|
55466
56417
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
55467
56418
|
if (setterNames.size < 5) return;
|
|
55468
56419
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -55678,7 +56629,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
55678
56629
|
]);
|
|
55679
56630
|
const isQueryCacheSourceCall = (initializer) => {
|
|
55680
56631
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
55681
|
-
const hookName = getCalleeName$
|
|
56632
|
+
const hookName = getCalleeName$1(initializer);
|
|
55682
56633
|
if (!hookName) return false;
|
|
55683
56634
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
55684
56635
|
};
|
|
@@ -55810,7 +56761,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
55810
56761
|
},
|
|
55811
56762
|
CallExpression(node) {
|
|
55812
56763
|
if (!hasQueryReadUsage) {
|
|
55813
|
-
const callName = getCalleeName$
|
|
56764
|
+
const callName = getCalleeName$1(node);
|
|
55814
56765
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
55815
56766
|
}
|
|
55816
56767
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -57473,6 +58424,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
57473
58424
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
57474
58425
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
57475
58426
|
const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
|
|
58427
|
+
const FILE_READER_READ_METHOD_NAMES = new Set([
|
|
58428
|
+
"readAsArrayBuffer",
|
|
58429
|
+
"readAsBinaryString",
|
|
58430
|
+
"readAsDataURL",
|
|
58431
|
+
"readAsText"
|
|
58432
|
+
]);
|
|
58433
|
+
const isGlobalFileReaderConstruction = (expression, context) => {
|
|
58434
|
+
if (!expression) return false;
|
|
58435
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58436
|
+
if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
58437
|
+
return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
|
|
58438
|
+
};
|
|
58439
|
+
const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
|
|
58440
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58441
|
+
let latestValue = null;
|
|
58442
|
+
let latestStart = null;
|
|
58443
|
+
if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
|
|
58444
|
+
latestValue = readerSymbol.initializer;
|
|
58445
|
+
latestStart = readerSymbol.declarationNode.range[0];
|
|
58446
|
+
}
|
|
58447
|
+
for (const reference of readerSymbol.references) {
|
|
58448
|
+
if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
|
|
58449
|
+
const assignment = reference.identifier.parent;
|
|
58450
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
|
|
58451
|
+
latestValue = assignment.right;
|
|
58452
|
+
latestStart = reference.identifier.range[0];
|
|
58453
|
+
}
|
|
58454
|
+
return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
|
|
58455
|
+
};
|
|
58456
|
+
const resolveLoadingCompletionFunction = (expression, context) => {
|
|
58457
|
+
const directFunction = resolveExactLocalFunction(expression, context.scopes);
|
|
58458
|
+
if (directFunction) return directFunction;
|
|
58459
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58460
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
58461
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
58462
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
58463
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
|
|
58464
|
+
const callback = initializer.arguments?.[0];
|
|
58465
|
+
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
58466
|
+
};
|
|
58467
|
+
const isSetterBooleanCall = (node, setterSymbol, value, context) => {
|
|
58468
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
58469
|
+
const callee = stripParenExpression(node.callee);
|
|
58470
|
+
const argument = node.arguments?.[0];
|
|
58471
|
+
const unwrappedArgument = argument ? stripParenExpression(argument) : null;
|
|
58472
|
+
return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
|
|
58473
|
+
};
|
|
58474
|
+
const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
|
|
58475
|
+
if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
|
|
58476
|
+
visitedFunctions.add(functionNode);
|
|
58477
|
+
let didClearLoadingState = false;
|
|
58478
|
+
walkAst(functionNode.body, (child) => {
|
|
58479
|
+
if (didClearLoadingState) return false;
|
|
58480
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
58481
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58482
|
+
if (isSetterBooleanCall(child, setterSymbol, false, context)) {
|
|
58483
|
+
didClearLoadingState = true;
|
|
58484
|
+
return false;
|
|
58485
|
+
}
|
|
58486
|
+
const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
|
|
58487
|
+
if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
|
|
58488
|
+
didClearLoadingState = true;
|
|
58489
|
+
return false;
|
|
58490
|
+
}
|
|
58491
|
+
});
|
|
58492
|
+
return didClearLoadingState;
|
|
58493
|
+
};
|
|
58494
|
+
const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
|
|
58495
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58496
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return null;
|
|
58497
|
+
let callback = null;
|
|
58498
|
+
let callbackStart = originStart;
|
|
58499
|
+
walkAst(readFunction.body, (child) => {
|
|
58500
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58501
|
+
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;
|
|
58502
|
+
const receiver = stripParenExpression(child.left.object);
|
|
58503
|
+
if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
|
|
58504
|
+
callback = child.right;
|
|
58505
|
+
callbackStart = child.range[0];
|
|
58506
|
+
});
|
|
58507
|
+
return callback;
|
|
58508
|
+
};
|
|
58509
|
+
const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
|
|
58510
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58511
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return false;
|
|
58512
|
+
let didStartLoading = false;
|
|
58513
|
+
walkAst(readFunction.body, (child) => {
|
|
58514
|
+
if (didStartLoading) return false;
|
|
58515
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58516
|
+
if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
|
|
58517
|
+
if (isSetterBooleanCall(child, setterSymbol, true, context)) {
|
|
58518
|
+
didStartLoading = true;
|
|
58519
|
+
return false;
|
|
58520
|
+
}
|
|
58521
|
+
});
|
|
58522
|
+
return didStartLoading;
|
|
58523
|
+
};
|
|
58524
|
+
const setterTracksFileReader = (functionBody, setterSymbol, context) => {
|
|
58525
|
+
let didFindFileReaderLifecycle = false;
|
|
58526
|
+
walkAst(functionBody, (child) => {
|
|
58527
|
+
if (didFindFileReaderLifecycle) return false;
|
|
58528
|
+
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
|
|
58529
|
+
const receiver = stripParenExpression(child.callee.object);
|
|
58530
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
58531
|
+
const readerSymbol = context.scopes.symbolFor(receiver);
|
|
58532
|
+
if (!readerSymbol) return;
|
|
58533
|
+
const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
|
|
58534
|
+
if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
|
|
58535
|
+
const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
|
|
58536
|
+
const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
|
|
58537
|
+
const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
|
|
58538
|
+
const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
|
|
58539
|
+
if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
|
|
58540
|
+
didFindFileReaderLifecycle = true;
|
|
58541
|
+
return false;
|
|
58542
|
+
}
|
|
58543
|
+
});
|
|
58544
|
+
return didFindFileReaderLifecycle;
|
|
58545
|
+
};
|
|
57476
58546
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
57477
58547
|
let found = false;
|
|
57478
58548
|
walkAst(fnBody, (child) => {
|
|
@@ -57646,6 +58716,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
57646
58716
|
const fnBody = enclosingFunctionBody(node);
|
|
57647
58717
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
57648
58718
|
if (fnBody && setterName) {
|
|
58719
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58720
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
57649
58721
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
57650
58722
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
57651
58723
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -57977,7 +59049,7 @@ const rerenderDependencies = defineRule({
|
|
|
57977
59049
|
severity: "error",
|
|
57978
59050
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
57979
59051
|
create: (context) => ({ CallExpression(node) {
|
|
57980
|
-
if (!
|
|
59052
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
57981
59053
|
const depsNode = node.arguments[1];
|
|
57982
59054
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
57983
59055
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -58232,7 +59304,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
58232
59304
|
category: "Performance",
|
|
58233
59305
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
58234
59306
|
create: (context) => ({ CallExpression(node) {
|
|
58235
|
-
if (!
|
|
59307
|
+
if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
|
|
58236
59308
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
58237
59309
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
58238
59310
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -58296,7 +59368,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
58296
59368
|
category: "Performance",
|
|
58297
59369
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
58298
59370
|
create: (context) => ({ CallExpression(node) {
|
|
58299
|
-
if (!
|
|
59371
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
58300
59372
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
58301
59373
|
if (!initializer) return;
|
|
58302
59374
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59059,11 +60131,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
59059
60131
|
}
|
|
59060
60132
|
return false;
|
|
59061
60133
|
};
|
|
59062
|
-
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60134
|
+
const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
59063
60135
|
const effectInfos = [];
|
|
59064
60136
|
walkAst(componentBody, (child) => {
|
|
59065
60137
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59066
|
-
if (!
|
|
60138
|
+
if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
59067
60139
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
59068
60140
|
for (const argument of child.arguments ?? []) {
|
|
59069
60141
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -59119,13 +60191,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
|
59119
60191
|
});
|
|
59120
60192
|
return effectInfos;
|
|
59121
60193
|
};
|
|
59122
|
-
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60194
|
+
const collectCustomHookArgumentNames = (componentBody, scopes) => {
|
|
59123
60195
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
59124
60196
|
walkAst(componentBody, (child) => {
|
|
59125
60197
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59126
60198
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
59127
60199
|
const calleeName = child.callee.name;
|
|
59128
60200
|
if (!isReactHookName(calleeName)) return;
|
|
60201
|
+
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
59129
60202
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
59130
60203
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
59131
60204
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -59166,21 +60239,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
59166
60239
|
create: (context) => {
|
|
59167
60240
|
const checkComponent = (componentBody) => {
|
|
59168
60241
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
59169
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
60242
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
59170
60243
|
if (bindings.length === 0) return;
|
|
59171
60244
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
59172
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60245
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
59173
60246
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
59174
|
-
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60247
|
+
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
59175
60248
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
59176
60249
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
59177
60250
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
59178
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60251
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
|
|
59179
60252
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
59180
60253
|
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);
|
|
59181
60254
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
59182
60255
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
59183
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60256
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
59184
60257
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
59185
60258
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
59186
60259
|
walkAst(componentBody, (child) => {
|
|
@@ -67081,7 +68154,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
67081
68154
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
67082
68155
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
67083
68156
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
67084
|
-
const calleeName = getCalleeName$
|
|
68157
|
+
const calleeName = getCalleeName$1(argument);
|
|
67085
68158
|
if (!calleeName) continue;
|
|
67086
68159
|
if (isAuthGuardName(calleeName)) return true;
|
|
67087
68160
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -67721,7 +68794,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
67721
68794
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
67722
68795
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
67723
68796
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
67724
|
-
const calleeName = getCalleeName$
|
|
68797
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
67725
68798
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
67726
68799
|
result.isServerFnChain = true;
|
|
67727
68800
|
const optionsArgument = currentNode.arguments?.[0];
|
|
@@ -71853,6 +72926,17 @@ const reactDoctorRules = [
|
|
|
71853
72926
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
71854
72927
|
}
|
|
71855
72928
|
},
|
|
72929
|
+
{
|
|
72930
|
+
key: "react-doctor/no-ref-callback-cleanup-before-react-19",
|
|
72931
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
72932
|
+
source: "react-doctor",
|
|
72933
|
+
originallyExternal: false,
|
|
72934
|
+
rule: {
|
|
72935
|
+
...noRefCallbackCleanupBeforeReact19,
|
|
72936
|
+
framework: "global",
|
|
72937
|
+
category: "Bugs"
|
|
72938
|
+
}
|
|
72939
|
+
},
|
|
71856
72940
|
{
|
|
71857
72941
|
key: "react-doctor/no-ref-current-in-render",
|
|
71858
72942
|
id: "no-ref-current-in-render",
|