oxlint-plugin-react-doctor 0.7.9-dev.2ba83c3 → 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 +1788 -418
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -836,22 +836,131 @@ const getImportedName = (importSpecifier) => {
|
|
|
836
836
|
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
837
837
|
};
|
|
838
838
|
//#endregion
|
|
839
|
-
//#region src/plugin/utils/get-
|
|
840
|
-
const
|
|
841
|
-
if (!isNodeOfType(node, "
|
|
842
|
-
|
|
843
|
-
if (
|
|
839
|
+
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
840
|
+
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
841
|
+
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
842
|
+
const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
|
|
843
|
+
if (node.computed) {
|
|
844
|
+
if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
|
|
845
|
+
if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
if (isNodeOfType(key, "Identifier")) return key.name;
|
|
849
|
+
if (isNodeOfType(key, "Literal")) {
|
|
850
|
+
if (typeof key.value === "string") return key.value;
|
|
851
|
+
if (options.stringifyNonStringLiterals) return String(key.value);
|
|
852
|
+
}
|
|
844
853
|
return null;
|
|
845
854
|
};
|
|
846
855
|
//#endregion
|
|
847
|
-
//#region src/plugin/utils/
|
|
848
|
-
const
|
|
856
|
+
//#region src/plugin/utils/get-static-property-name.ts
|
|
857
|
+
const getStaticPropertyName = (memberExpression) => {
|
|
858
|
+
const property = memberExpression.property;
|
|
859
|
+
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
860
|
+
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
861
|
+
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
862
|
+
return null;
|
|
863
|
+
};
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
866
|
+
const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
|
|
867
|
+
"ParenthesizedExpression",
|
|
868
|
+
"TSAsExpression",
|
|
869
|
+
"TSSatisfiesExpression",
|
|
870
|
+
"TSTypeAssertion",
|
|
871
|
+
"TSNonNullExpression",
|
|
872
|
+
"TSInstantiationExpression",
|
|
873
|
+
"ChainExpression"
|
|
874
|
+
]);
|
|
875
|
+
const stripParenExpression = (node) => {
|
|
876
|
+
let current = node;
|
|
877
|
+
while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
|
|
878
|
+
return current;
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
882
|
+
const resolveConstIdentifierAlias = (identifier, scopes, allowPatternBinding = false) => {
|
|
883
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
884
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
885
|
+
let symbol = scopes.symbolFor(identifier);
|
|
886
|
+
while (symbol?.kind === "const") {
|
|
887
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return null;
|
|
888
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
|
|
889
|
+
visitedSymbolIds.add(symbol.id);
|
|
890
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
891
|
+
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
892
|
+
symbol = scopes.symbolFor(initializer);
|
|
893
|
+
}
|
|
894
|
+
return symbol;
|
|
895
|
+
};
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/plugin/utils/is-react-api-call.ts
|
|
898
|
+
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
899
|
+
const isImportedFromReact = (symbol) => {
|
|
900
|
+
if (symbol.kind !== "import") return false;
|
|
901
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
902
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
903
|
+
};
|
|
904
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
905
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
906
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
907
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
908
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
909
|
+
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
910
|
+
};
|
|
911
|
+
const isReactNamespaceImport = (identifier, scopes) => {
|
|
912
|
+
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
913
|
+
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
914
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
915
|
+
};
|
|
916
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
917
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
918
|
+
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
919
|
+
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
920
|
+
};
|
|
921
|
+
const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
|
|
922
|
+
const symbol = scopes.symbolFor(identifier);
|
|
923
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
924
|
+
const pattern = symbol.declarationNode.id;
|
|
925
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
926
|
+
for (const property of pattern.properties) {
|
|
927
|
+
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
928
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
929
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
};
|
|
933
|
+
const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
849
934
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
935
|
+
return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
|
|
936
|
+
};
|
|
937
|
+
const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
|
|
938
|
+
const callee = stripParenExpression(rawCallee);
|
|
939
|
+
if (options.resolveConditionalAliases && isNodeOfType(callee, "ConditionalExpression")) return isReactApiCallee(callee.consequent, apiNames, scopes, options, new Set(visitedSymbolIds)) && isReactApiCallee(callee.alternate, apiNames, scopes, options, new Set(visitedSymbolIds));
|
|
940
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
941
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
942
|
+
if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
|
|
943
|
+
if (options.resolveConditionalAliases) {
|
|
944
|
+
const symbol = scopes.symbolFor(callee);
|
|
945
|
+
if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
|
|
946
|
+
visitedSymbolIds.add(symbol.id);
|
|
947
|
+
return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
951
|
+
}
|
|
952
|
+
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
953
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
853
954
|
};
|
|
854
955
|
//#endregion
|
|
956
|
+
//#region src/plugin/utils/is-react-hook-call.ts
|
|
957
|
+
const isReactHookCall = (node, hookNames, scopes) => isReactApiCall(node, hookNames, scopes, {
|
|
958
|
+
allowGlobalReactNamespace: true,
|
|
959
|
+
allowUnboundBareCalls: true,
|
|
960
|
+
resolveConditionalAliases: true,
|
|
961
|
+
resolveNamedAliases: true
|
|
962
|
+
});
|
|
963
|
+
//#endregion
|
|
855
964
|
//#region src/plugin/utils/is-ast-node.ts
|
|
856
965
|
const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
857
966
|
//#endregion
|
|
@@ -921,12 +1030,12 @@ const collectChildComponentNames = (element, into) => {
|
|
|
921
1030
|
into.add(name);
|
|
922
1031
|
});
|
|
923
1032
|
};
|
|
924
|
-
const countEffectHookCalls = (body) => {
|
|
1033
|
+
const countEffectHookCalls = (body, scopes) => {
|
|
925
1034
|
if (!body) return 0;
|
|
926
1035
|
let count = 0;
|
|
927
1036
|
walkAst(body, (child) => {
|
|
928
1037
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
929
|
-
if (
|
|
1038
|
+
if (isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) count++;
|
|
930
1039
|
});
|
|
931
1040
|
return count;
|
|
932
1041
|
};
|
|
@@ -952,11 +1061,11 @@ const getComponentEffectIndex = (programRoot) => {
|
|
|
952
1061
|
componentEffectIndexCache.set(programRoot, index);
|
|
953
1062
|
return index;
|
|
954
1063
|
};
|
|
955
|
-
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
1064
|
+
const getSameFileComponentEffectCount = (programRoot, componentName, scopes) => {
|
|
956
1065
|
const index = getComponentEffectIndex(programRoot);
|
|
957
1066
|
const cachedCount = index.effectCountByName.get(componentName);
|
|
958
1067
|
if (cachedCount !== void 0) return cachedCount;
|
|
959
|
-
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
1068
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null, scopes);
|
|
960
1069
|
index.effectCountByName.set(componentName, count);
|
|
961
1070
|
return count;
|
|
962
1071
|
};
|
|
@@ -1016,7 +1125,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
1016
1125
|
let totalEffects = 0;
|
|
1017
1126
|
const effectfulChildren = [];
|
|
1018
1127
|
for (const componentName of childComponentNames) {
|
|
1019
|
-
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
1128
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName, context.scopes);
|
|
1020
1129
|
if (effectCount === 0) continue;
|
|
1021
1130
|
totalEffects += effectCount;
|
|
1022
1131
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1207,6 +1316,14 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
1207
1316
|
return best;
|
|
1208
1317
|
};
|
|
1209
1318
|
//#endregion
|
|
1319
|
+
//#region src/plugin/utils/get-callee-name.ts
|
|
1320
|
+
const getCalleeName$1 = (node) => {
|
|
1321
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
|
|
1322
|
+
if (isNodeOfType(node.callee, "Identifier")) return node.callee.name;
|
|
1323
|
+
if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier")) return node.callee.property.name;
|
|
1324
|
+
return null;
|
|
1325
|
+
};
|
|
1326
|
+
//#endregion
|
|
1210
1327
|
//#region src/plugin/utils/is-function-like.ts
|
|
1211
1328
|
/**
|
|
1212
1329
|
* Type-guard for the three "function-like" ESTree node shapes:
|
|
@@ -1220,37 +1337,6 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
1220
1337
|
*/
|
|
1221
1338
|
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
1222
1339
|
//#endregion
|
|
1223
|
-
//#region src/plugin/utils/strip-paren-expression.ts
|
|
1224
|
-
const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
|
|
1225
|
-
"ParenthesizedExpression",
|
|
1226
|
-
"TSAsExpression",
|
|
1227
|
-
"TSSatisfiesExpression",
|
|
1228
|
-
"TSTypeAssertion",
|
|
1229
|
-
"TSNonNullExpression",
|
|
1230
|
-
"TSInstantiationExpression",
|
|
1231
|
-
"ChainExpression"
|
|
1232
|
-
]);
|
|
1233
|
-
const stripParenExpression = (node) => {
|
|
1234
|
-
let current = node;
|
|
1235
|
-
while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
|
|
1236
|
-
return current;
|
|
1237
|
-
};
|
|
1238
|
-
//#endregion
|
|
1239
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
1240
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
1241
|
-
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
1242
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
1243
|
-
let symbol = scopes.symbolFor(identifier);
|
|
1244
|
-
while (symbol?.kind === "const") {
|
|
1245
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
1246
|
-
visitedSymbolIds.add(symbol.id);
|
|
1247
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
1248
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
1249
|
-
symbol = scopes.symbolFor(initializer);
|
|
1250
|
-
}
|
|
1251
|
-
return symbol;
|
|
1252
|
-
};
|
|
1253
|
-
//#endregion
|
|
1254
1340
|
//#region src/plugin/utils/resolve-exact-local-function.ts
|
|
1255
1341
|
const resolveExactLocalFunction = (expression, scopes) => {
|
|
1256
1342
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -1296,9 +1382,17 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1296
1382
|
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1297
1383
|
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1298
1384
|
//#endregion
|
|
1385
|
+
//#region src/plugin/utils/is-hook-call.ts
|
|
1386
|
+
const isHookCall$2 = (node, hookName) => {
|
|
1387
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
1388
|
+
const calleeName = getCalleeName$1(node);
|
|
1389
|
+
if (!calleeName) return false;
|
|
1390
|
+
return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
|
|
1391
|
+
};
|
|
1392
|
+
//#endregion
|
|
1299
1393
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1394
|
+
const REACT_STABLE_HANDLER_HOOK_NAMES = new Set(["useCallback", "useEffectEvent"]);
|
|
1395
|
+
const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1302
1396
|
"useEffectEvent",
|
|
1303
1397
|
"useEvent",
|
|
1304
1398
|
"useEventCallback",
|
|
@@ -1307,21 +1401,21 @@ const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
|
1307
1401
|
]);
|
|
1308
1402
|
const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
|
|
1309
1403
|
const isThrottledHandlerHookCall = (callNode) => {
|
|
1310
|
-
const calleeName = getCalleeName$
|
|
1404
|
+
const calleeName = getCalleeName$1(callNode);
|
|
1311
1405
|
return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
|
|
1312
1406
|
};
|
|
1313
|
-
const isEmptyDepsUseMemoCall = (callNode) => {
|
|
1314
|
-
if (!
|
|
1407
|
+
const isEmptyDepsUseMemoCall = (callNode, scopes) => {
|
|
1408
|
+
if (!isReactHookCall(callNode, "useMemo", scopes)) return false;
|
|
1315
1409
|
const memoDepsNode = callNode.arguments?.[1];
|
|
1316
1410
|
return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
|
|
1317
1411
|
};
|
|
1318
|
-
const isStableHandlerInitializer = (initializer) => {
|
|
1319
|
-
if (isNodeOfType(initializer, "CallExpression")) return isHookCall$2(initializer,
|
|
1412
|
+
const isStableHandlerInitializer = (initializer, scopes) => {
|
|
1413
|
+
if (isNodeOfType(initializer, "CallExpression")) return isReactHookCall(initializer, REACT_STABLE_HANDLER_HOOK_NAMES, scopes) || isHookCall$2(initializer, CUSTOM_STABLE_HANDLER_HOOK_NAMES) || isEmptyDepsUseMemoCall(initializer, scopes) || isThrottledHandlerHookCall(initializer);
|
|
1320
1414
|
return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
|
|
1321
1415
|
};
|
|
1322
|
-
const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
|
|
1416
|
+
const isStableRefReceiverDep = (referenceNode, receiverDepName, scopes) => {
|
|
1323
1417
|
const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
|
|
1324
|
-
return Boolean(receiverBinding?.initializer &&
|
|
1418
|
+
return Boolean(receiverBinding?.initializer && isReactHookCall(receiverBinding.initializer, "useRef", scopes));
|
|
1325
1419
|
};
|
|
1326
1420
|
const advancedEventHandlerRefs = defineRule({
|
|
1327
1421
|
id: "advanced-event-handler-refs",
|
|
@@ -1331,7 +1425,7 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1331
1425
|
category: "Performance",
|
|
1332
1426
|
recommendation: "Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",
|
|
1333
1427
|
create: (context) => ({ CallExpression(node) {
|
|
1334
|
-
if (!
|
|
1428
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
1335
1429
|
if ((node.arguments?.length ?? 0) < 2) return;
|
|
1336
1430
|
const callback = getEffectCallback(node);
|
|
1337
1431
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -1355,8 +1449,8 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1355
1449
|
});
|
|
1356
1450
|
if (!registeredHandlerName) return;
|
|
1357
1451
|
const handlerBinding = findVariableInitializer(node, registeredHandlerName);
|
|
1358
|
-
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
|
|
1359
|
-
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName))) return;
|
|
1452
|
+
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer, context.scopes)) return;
|
|
1453
|
+
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName, context.scopes))) return;
|
|
1360
1454
|
context.report({
|
|
1361
1455
|
node,
|
|
1362
1456
|
message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
|
|
@@ -1843,15 +1937,6 @@ const flattenJsxName$1 = (node) => {
|
|
|
1843
1937
|
return null;
|
|
1844
1938
|
};
|
|
1845
1939
|
//#endregion
|
|
1846
|
-
//#region src/plugin/utils/get-static-property-name.ts
|
|
1847
|
-
const getStaticPropertyName = (memberExpression) => {
|
|
1848
|
-
const property = memberExpression.property;
|
|
1849
|
-
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
1850
|
-
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
1851
|
-
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
1852
|
-
return null;
|
|
1853
|
-
};
|
|
1854
|
-
//#endregion
|
|
1855
1940
|
//#region src/plugin/utils/is-generated-image-renderer-call.ts
|
|
1856
1941
|
const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
1857
1942
|
"next/og",
|
|
@@ -4859,23 +4944,6 @@ const containsDirectAwait = (node) => {
|
|
|
4859
4944
|
return foundAwait;
|
|
4860
4945
|
};
|
|
4861
4946
|
//#endregion
|
|
4862
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
4863
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
4864
|
-
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
4865
|
-
const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
|
|
4866
|
-
if (node.computed) {
|
|
4867
|
-
if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
|
|
4868
|
-
if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
|
|
4869
|
-
return null;
|
|
4870
|
-
}
|
|
4871
|
-
if (isNodeOfType(key, "Identifier")) return key.name;
|
|
4872
|
-
if (isNodeOfType(key, "Literal")) {
|
|
4873
|
-
if (typeof key.value === "string") return key.value;
|
|
4874
|
-
if (options.stringifyNonStringLiterals) return String(key.value);
|
|
4875
|
-
}
|
|
4876
|
-
return null;
|
|
4877
|
-
};
|
|
4878
|
-
//#endregion
|
|
4879
4947
|
//#region src/plugin/utils/get-destructured-binding-property-name.ts
|
|
4880
4948
|
const getDestructuredBindingPropertyName = (bindingIdentifier) => {
|
|
4881
4949
|
let bindingNode = bindingIdentifier;
|
|
@@ -8940,65 +9008,6 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
|
|
|
8940
9008
|
}
|
|
8941
9009
|
};
|
|
8942
9010
|
//#endregion
|
|
8943
|
-
//#region src/plugin/utils/is-react-api-call.ts
|
|
8944
|
-
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
8945
|
-
const isImportedFromReact = (symbol) => {
|
|
8946
|
-
if (symbol.kind !== "import") return false;
|
|
8947
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
8948
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
8949
|
-
};
|
|
8950
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
8951
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
8952
|
-
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
8953
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8954
|
-
const importedName = getImportedName(symbol.declarationNode);
|
|
8955
|
-
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
8956
|
-
};
|
|
8957
|
-
const isReactNamespaceImport = (identifier, scopes) => {
|
|
8958
|
-
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
8959
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8960
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8961
|
-
};
|
|
8962
|
-
const isReactNamespaceReceiver = (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(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(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;
|
|
@@ -30212,6 +30781,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
30212
30781
|
const importDeclaration = declarationNode.parent;
|
|
30213
30782
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
30214
30783
|
}));
|
|
30784
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30785
|
+
const receiver = stripParenExpression(node);
|
|
30786
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30787
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30788
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30789
|
+
};
|
|
30215
30790
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
30216
30791
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
30217
30792
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -30220,34 +30795,22 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
30220
30795
|
if (!reference?.resolved) return callee.name === hookName;
|
|
30221
30796
|
return isReactNamedImportReference(reference, hookName);
|
|
30222
30797
|
}
|
|
30223
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
30224
|
-
|
|
30225
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
30226
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30798
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30799
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
30227
30800
|
};
|
|
30228
30801
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
30229
30802
|
if (!node) return false;
|
|
30230
30803
|
if (isNodeOfType(node, "Identifier")) {
|
|
30231
30804
|
if (node.name === hookName) return true;
|
|
30232
30805
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
30233
|
-
const
|
|
30234
|
-
|
|
30806
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30807
|
+
const parent = receiverRoot.parent;
|
|
30808
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
30235
30809
|
return false;
|
|
30236
30810
|
}
|
|
30237
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
30238
|
-
const receiver = stripParenExpression(node.object);
|
|
30239
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30240
|
-
}
|
|
30811
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30241
30812
|
return false;
|
|
30242
30813
|
};
|
|
30243
|
-
const isUseEffect = (node) => {
|
|
30244
|
-
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
30245
|
-
const callee = node.callee;
|
|
30246
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
30247
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
30248
|
-
const receiver = stripParenExpression(callee.object);
|
|
30249
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
30250
|
-
};
|
|
30251
30814
|
const getEffectFn = (analysis, node) => {
|
|
30252
30815
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30253
30816
|
const fn = node.arguments?.[0];
|
|
@@ -30345,7 +30908,27 @@ const isRefCurrent = (ref) => {
|
|
|
30345
30908
|
if (!isNodeOfType(parent.property, "Identifier")) return false;
|
|
30346
30909
|
return parent.property.name === "current";
|
|
30347
30910
|
};
|
|
30348
|
-
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);
|
|
30349
30932
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
30350
30933
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
30351
30934
|
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
@@ -30496,9 +31079,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
|
30496
31079
|
};
|
|
30497
31080
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
30498
31081
|
const getUseStateDecl = (analysis, ref) => {
|
|
30499
|
-
|
|
30500
|
-
|
|
30501
|
-
|
|
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;
|
|
30502
31087
|
};
|
|
30503
31088
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
30504
31089
|
if (isFunctionLike$1(node)) return true;
|
|
@@ -30661,7 +31246,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30661
31246
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30662
31247
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30663
31248
|
};
|
|
30664
|
-
const
|
|
31249
|
+
const isSynchronousFunction = (functionNode) => {
|
|
31250
|
+
const functionMetadata = functionNode;
|
|
31251
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
31252
|
+
};
|
|
31253
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
31254
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
31255
|
+
return null;
|
|
31256
|
+
};
|
|
31257
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
31258
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
31259
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
31260
|
+
if (!bindingIdentifier) return null;
|
|
31261
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
31262
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
31263
|
+
const definition = variable.defs[0];
|
|
31264
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
31265
|
+
if (definition.type !== "Variable") return null;
|
|
31266
|
+
const declarator = definition.node;
|
|
31267
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
31268
|
+
if (declarator.init === functionNode) return variable;
|
|
31269
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
31270
|
+
return null;
|
|
31271
|
+
};
|
|
31272
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
31273
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31274
|
+
const expressionContainer = expression.parent;
|
|
31275
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
31276
|
+
const attribute = expressionContainer.parent;
|
|
31277
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
31278
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
31279
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
31280
|
+
};
|
|
31281
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
31282
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
31283
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
31284
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
31285
|
+
};
|
|
31286
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
31287
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31288
|
+
const dependencyArray = expression.parent;
|
|
31289
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
31290
|
+
const hookCall = dependencyArray.parent;
|
|
31291
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
31292
|
+
const callee = hookCall.callee;
|
|
31293
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
31294
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
31295
|
+
};
|
|
31296
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
31297
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
31298
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
31299
|
+
const callExpressions = [];
|
|
31300
|
+
let hasDirectJsxEventReference = false;
|
|
31301
|
+
for (const reference of functionVariable.references) {
|
|
31302
|
+
if (reference.init) continue;
|
|
31303
|
+
const identifier = reference.identifier;
|
|
31304
|
+
if (reference.isWrite()) return false;
|
|
31305
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
31306
|
+
if (jsxEventValueAttribute) {
|
|
31307
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
31308
|
+
continue;
|
|
31309
|
+
}
|
|
31310
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
31311
|
+
const callExpression = getCallExpr(reference);
|
|
31312
|
+
if (!callExpression) return false;
|
|
31313
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
31314
|
+
if (jsxEventCallbackAttribute) {
|
|
31315
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
31316
|
+
continue;
|
|
31317
|
+
}
|
|
31318
|
+
callExpressions.push(callExpression);
|
|
31319
|
+
}
|
|
31320
|
+
if (hasDirectJsxEventReference) return true;
|
|
31321
|
+
for (const callExpression of callExpressions) {
|
|
31322
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
31323
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
31324
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
31325
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
31326
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
31327
|
+
}
|
|
31328
|
+
return false;
|
|
31329
|
+
};
|
|
31330
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30665
31331
|
if (!setterRef.resolved) return false;
|
|
30666
31332
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30667
31333
|
if (!componentFunction) return false;
|
|
@@ -30670,6 +31336,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30670
31336
|
const identifier = reference.identifier;
|
|
30671
31337
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30672
31338
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
31339
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
31340
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
31341
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
31342
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
31343
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30673
31344
|
}
|
|
30674
31345
|
return false;
|
|
30675
31346
|
};
|
|
@@ -31559,7 +32230,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31559
32230
|
}
|
|
31560
32231
|
return false;
|
|
31561
32232
|
};
|
|
31562
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
32233
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31563
32234
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31564
32235
|
if (frames.length === 0) return [];
|
|
31565
32236
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31589,7 +32260,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31589
32260
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31590
32261
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31591
32262
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31592
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
32263
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31593
32264
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31594
32265
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31595
32266
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31601,6 +32272,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31601
32272
|
sourceReferences,
|
|
31602
32273
|
isDeferred: frame.isDeferred,
|
|
31603
32274
|
isRenderKnownCopy,
|
|
32275
|
+
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
31604
32276
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
31605
32277
|
resetsSourceState: false
|
|
31606
32278
|
});
|
|
@@ -31616,22 +32288,71 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31616
32288
|
});
|
|
31617
32289
|
};
|
|
31618
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
|
|
31619
32337
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
31620
32338
|
const noAdjustStateOnPropChange = defineRule({
|
|
31621
32339
|
id: "no-adjust-state-on-prop-change",
|
|
31622
|
-
title: "State
|
|
32340
|
+
title: "State adjusted after a prop changes",
|
|
31623
32341
|
severity: "warn",
|
|
31624
32342
|
tags: ["test-noise"],
|
|
31625
|
-
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",
|
|
31626
32344
|
create: (context) => ({ CallExpression(node) {
|
|
31627
|
-
if (!
|
|
32345
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
31628
32346
|
const analysis = getProgramAnalysis(node);
|
|
31629
32347
|
if (!analysis) return;
|
|
31630
32348
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31631
32349
|
if (!dependencyReferences) return;
|
|
31632
32350
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31633
|
-
|
|
31634
|
-
|
|
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;
|
|
31635
32356
|
context.report({
|
|
31636
32357
|
node: fact.callExpression,
|
|
31637
32358
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
@@ -33644,7 +34365,7 @@ const declarationBodyContainsHookCall = (symbol) => {
|
|
|
33644
34365
|
walkAst(componentFunction, (descendant) => {
|
|
33645
34366
|
if (didFindHookCall) return false;
|
|
33646
34367
|
if (!isNodeOfType(descendant, "CallExpression")) return;
|
|
33647
|
-
const calleeName = getCalleeName$
|
|
34368
|
+
const calleeName = getCalleeName$1(descendant);
|
|
33648
34369
|
if (calleeName && isReactHookName(calleeName)) {
|
|
33649
34370
|
didFindHookCall = true;
|
|
33650
34371
|
return false;
|
|
@@ -33669,7 +34390,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
|
|
|
33669
34390
|
if (isNodeOfType(parent, "ArrowFunctionExpression")) {
|
|
33670
34391
|
if (parent.body !== current) return false;
|
|
33671
34392
|
const grandparent = parent.parent;
|
|
33672
|
-
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$
|
|
34393
|
+
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
|
|
33673
34394
|
}
|
|
33674
34395
|
if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
|
|
33675
34396
|
current = parent;
|
|
@@ -34351,7 +35072,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34351
35072
|
if (!callExpr) continue;
|
|
34352
35073
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34353
35074
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
34354
|
-
const declarator = getUseStateDeclarator(ref);
|
|
35075
|
+
const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34355
35076
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
34356
35077
|
}
|
|
34357
35078
|
for (const ref of effectFnRefs) {
|
|
@@ -34360,7 +35081,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34360
35081
|
if (!callExpr) continue;
|
|
34361
35082
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34362
35083
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
34363
|
-
const setterDeclarator = getUseStateDeclarator(ref);
|
|
35084
|
+
const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34364
35085
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
34365
35086
|
const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
|
|
34366
35087
|
const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
|
|
@@ -36201,10 +36922,10 @@ const noDerivedState = defineRule({
|
|
|
36201
36922
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36202
36923
|
} }).visitors,
|
|
36203
36924
|
CallExpression(node) {
|
|
36204
|
-
if (!
|
|
36925
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
36205
36926
|
const analysis = getProgramAnalysis(node);
|
|
36206
36927
|
if (!analysis) return;
|
|
36207
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36928
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
36208
36929
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
36209
36930
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36210
36931
|
}
|
|
@@ -36221,10 +36942,10 @@ const noDerivedStateEffect = defineRule({
|
|
|
36221
36942
|
tags: ["test-noise"],
|
|
36222
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",
|
|
36223
36944
|
create: (context) => ({ CallExpression(node) {
|
|
36224
|
-
if (!
|
|
36945
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
36225
36946
|
const analysis = getProgramAnalysis(node);
|
|
36226
36947
|
if (!analysis) return;
|
|
36227
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36948
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36228
36949
|
context.report({
|
|
36229
36950
|
node,
|
|
36230
36951
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -36344,7 +37065,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
|
|
|
36344
37065
|
if (isFunctionLike$1(cursor)) {
|
|
36345
37066
|
const parent = cursor.parent ?? null;
|
|
36346
37067
|
if (parent && isNodeOfType(parent, "CallExpression")) {
|
|
36347
|
-
const calleeName = getCalleeName$
|
|
37068
|
+
const calleeName = getCalleeName$1(parent);
|
|
36348
37069
|
if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
|
|
36349
37070
|
}
|
|
36350
37071
|
}
|
|
@@ -36415,7 +37136,7 @@ const isNonHandlerHookCallback = (functionNode) => {
|
|
|
36415
37136
|
const parent = functionNode.parent ?? null;
|
|
36416
37137
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
36417
37138
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
36418
|
-
const calleeName = getCalleeName$
|
|
37139
|
+
const calleeName = getCalleeName$1(parent);
|
|
36419
37140
|
return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
|
|
36420
37141
|
};
|
|
36421
37142
|
const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
@@ -36497,7 +37218,7 @@ const noDerivedUseState = defineRule({
|
|
|
36497
37218
|
return {
|
|
36498
37219
|
...propStackTracker.visitors,
|
|
36499
37220
|
CallExpression(node) {
|
|
36500
|
-
if (!
|
|
37221
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
36501
37222
|
const seed = unwrapInitializerSeed(node.arguments[0]);
|
|
36502
37223
|
const reportStalePropCopy = (propName) => {
|
|
36503
37224
|
if (isIntentionalSnapshotState(node)) return;
|
|
@@ -37173,7 +37894,7 @@ const noDirectMutationState = defineRule({
|
|
|
37173
37894
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
37174
37895
|
//#endregion
|
|
37175
37896
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
37176
|
-
const collectUseStateBindings = (componentBody) => {
|
|
37897
|
+
const collectUseStateBindings = (componentBody, scopes) => {
|
|
37177
37898
|
const bindings = [];
|
|
37178
37899
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
37179
37900
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -37186,7 +37907,7 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
37186
37907
|
const setterElement = elements[1];
|
|
37187
37908
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37188
37909
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37189
|
-
if (!
|
|
37910
|
+
if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
|
|
37190
37911
|
bindings.push({
|
|
37191
37912
|
valueName: valueElement.name,
|
|
37192
37913
|
setterName: setterElement.name,
|
|
@@ -37353,7 +38074,7 @@ const noDirectStateMutation = defineRule({
|
|
|
37353
38074
|
create: (context) => {
|
|
37354
38075
|
const checkComponent = (componentBody) => {
|
|
37355
38076
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
37356
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
38077
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
37357
38078
|
if (bindings.length === 0) return;
|
|
37358
38079
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
37359
38080
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
@@ -37913,25 +38634,31 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
37913
38634
|
};
|
|
37914
38635
|
//#endregion
|
|
37915
38636
|
//#region src/plugin/rules/state-and-effects/no-effect-chain.ts
|
|
37916
|
-
const findTopLevelEffectCalls = (componentBody) => {
|
|
38637
|
+
const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
37917
38638
|
const effectCalls = [];
|
|
37918
38639
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
37919
38640
|
for (const statement of componentBody.body ?? []) {
|
|
37920
38641
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37921
38642
|
const expression = unwrapDiscardedExpression(statement);
|
|
37922
38643
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
37923
|
-
if (!
|
|
38644
|
+
if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
|
|
37924
38645
|
effectCalls.push(expression);
|
|
37925
38646
|
}
|
|
37926
38647
|
return effectCalls;
|
|
37927
38648
|
};
|
|
37928
|
-
const
|
|
37929
|
-
const
|
|
37930
|
-
if (!isNodeOfType(effectNode, "CallExpression")) return
|
|
38649
|
+
const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
|
|
38650
|
+
const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
|
|
38651
|
+
if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
|
|
37931
38652
|
const depsNode = effectNode.arguments?.[1];
|
|
37932
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) return
|
|
37933
|
-
for (const element of depsNode.elements ?? [])
|
|
37934
|
-
|
|
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;
|
|
37935
38662
|
};
|
|
37936
38663
|
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
37937
38664
|
const analysisFunctions = new Set([effectCallback]);
|
|
@@ -38037,12 +38764,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
|
|
|
38037
38764
|
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
38038
38765
|
return readStaticEffectValue(argument, scopes, null, null);
|
|
38039
38766
|
};
|
|
38040
|
-
const collectStateWritesInEffect = (analysisFunctions,
|
|
38767
|
+
const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
|
|
38041
38768
|
const stateWrites = /* @__PURE__ */ new Map();
|
|
38042
38769
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38043
38770
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38044
38771
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
38045
|
-
const
|
|
38772
|
+
const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
|
|
38773
|
+
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
38046
38774
|
if (!stateName) return;
|
|
38047
38775
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
38048
38776
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -38128,11 +38856,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
38128
38856
|
"keys",
|
|
38129
38857
|
"values"
|
|
38130
38858
|
]);
|
|
38131
|
-
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
38859
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
38132
38860
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
38133
38861
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
38134
38862
|
if (isNodeOfType(returnedValue.callee, "Identifier")) {
|
|
38135
|
-
|
|
38863
|
+
const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
|
|
38864
|
+
if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
38136
38865
|
if (isSetterIdentifier(returnedValue.callee.name)) return true;
|
|
38137
38866
|
}
|
|
38138
38867
|
return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
@@ -38156,7 +38885,7 @@ const collectStorageHookSetterNames = (componentBody) => {
|
|
|
38156
38885
|
for (const declarator of statement.declarations ?? []) {
|
|
38157
38886
|
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
38158
38887
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
38159
|
-
const calleeName = getCalleeName$
|
|
38888
|
+
const calleeName = getCalleeName$1(declarator.init);
|
|
38160
38889
|
if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
|
|
38161
38890
|
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
|
|
38162
38891
|
}
|
|
@@ -38299,11 +39028,11 @@ const isExternalSyncNode = (node) => {
|
|
|
38299
39028
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
38300
39029
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
38301
39030
|
};
|
|
38302
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
39031
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
38303
39032
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
38304
39033
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
38305
|
-
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
38306
|
-
} 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;
|
|
38307
39036
|
let didFindExternalCall = false;
|
|
38308
39037
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38309
39038
|
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
@@ -38319,10 +39048,11 @@ const noEffectChain = defineRule({
|
|
|
38319
39048
|
create: (context) => {
|
|
38320
39049
|
const checkComponent = (componentBody) => {
|
|
38321
39050
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38322
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
39051
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
38323
39052
|
if (useStateBindings.length === 0) return;
|
|
38324
39053
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
38325
39054
|
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
39055
|
+
const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
|
|
38326
39056
|
for (const binding of useStateBindings) {
|
|
38327
39057
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
38328
39058
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
@@ -38331,21 +39061,27 @@ const noEffectChain = defineRule({
|
|
|
38331
39061
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
38332
39062
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
38333
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
|
+
}
|
|
38334
39069
|
}
|
|
38335
39070
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
39071
|
+
const stateSymbolIdSet = new Set(stateSymbolIds.values());
|
|
38336
39072
|
const effectInfos = [];
|
|
38337
|
-
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
39073
|
+
for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
|
|
38338
39074
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
38339
39075
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
38340
39076
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
38341
|
-
const stateWrites = collectStateWritesInEffect(analysisFunctions,
|
|
39077
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
|
|
38342
39078
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
38343
39079
|
effectInfos.push({
|
|
38344
39080
|
node: effectCall,
|
|
38345
|
-
|
|
39081
|
+
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
38346
39082
|
stateWrites,
|
|
38347
39083
|
analysisFunctions,
|
|
38348
|
-
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)
|
|
38349
39085
|
});
|
|
38350
39086
|
}
|
|
38351
39087
|
if (effectInfos.length < 2) return;
|
|
@@ -38356,10 +39092,11 @@ const noEffectChain = defineRule({
|
|
|
38356
39092
|
for (const readerEffect of effectInfos) {
|
|
38357
39093
|
if (readerEffect === writerEffect) continue;
|
|
38358
39094
|
if (readerEffect.isExternalSync) continue;
|
|
38359
|
-
if (readerEffect.
|
|
39095
|
+
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
38360
39096
|
let chainedStateName = null;
|
|
38361
39097
|
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
38362
|
-
|
|
39098
|
+
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39099
|
+
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
38363
39100
|
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
38364
39101
|
chainedStateName = writtenName;
|
|
38365
39102
|
break;
|
|
@@ -38636,7 +39373,7 @@ const noEffectEventHandler = defineRule({
|
|
|
38636
39373
|
return {
|
|
38637
39374
|
...propStackTracker.visitors,
|
|
38638
39375
|
CallExpression(node) {
|
|
38639
|
-
if (!
|
|
39376
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
38640
39377
|
const callback = getEffectCallback(node);
|
|
38641
39378
|
if (!callback) return;
|
|
38642
39379
|
const analysis = getProgramAnalysis(node);
|
|
@@ -38756,14 +39493,14 @@ const noEffectEventInDeps = defineRule({
|
|
|
38756
39493
|
if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
|
|
38757
39494
|
const initializer = declaratorNode.init;
|
|
38758
39495
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
38759
|
-
if (!
|
|
39496
|
+
if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
|
|
38760
39497
|
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
38761
39498
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
38762
39499
|
} });
|
|
38763
39500
|
return {
|
|
38764
39501
|
...componentBindings.visitors,
|
|
38765
39502
|
CallExpression(node) {
|
|
38766
|
-
if (!
|
|
39503
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
38767
39504
|
if (!componentBindings.isInsideComponent()) return;
|
|
38768
39505
|
const depsNode = node.arguments[1];
|
|
38769
39506
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -38820,7 +39557,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
38820
39557
|
node: finding.reportNode,
|
|
38821
39558
|
message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
|
|
38822
39559
|
});
|
|
38823
|
-
if (!
|
|
39560
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
38824
39561
|
const args = node.arguments ?? [];
|
|
38825
39562
|
if (args.length < 2) return;
|
|
38826
39563
|
const depsNode = args[1];
|
|
@@ -39081,7 +39818,7 @@ const noEventHandler = defineRule({
|
|
|
39081
39818
|
severity: "warn",
|
|
39082
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",
|
|
39083
39820
|
create: (context) => ({ CallExpression(node) {
|
|
39084
|
-
if (!
|
|
39821
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
39085
39822
|
const analysis = getProgramAnalysis(node);
|
|
39086
39823
|
if (!analysis || hasCleanup(analysis, node)) return;
|
|
39087
39824
|
const frames = collectBoundedEffectExecutionFrames(analysis, node);
|
|
@@ -39543,25 +40280,25 @@ const addDeclarationBindings = (statement, scope) => {
|
|
|
39543
40280
|
}
|
|
39544
40281
|
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
|
|
39545
40282
|
};
|
|
39546
|
-
const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40283
|
+
const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39547
40284
|
let hasReturn = false;
|
|
39548
|
-
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;
|
|
39549
40286
|
else addDeclarationBindings(statement, scope);
|
|
39550
40287
|
return hasReturn;
|
|
39551
40288
|
};
|
|
39552
|
-
const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
|
|
40289
|
+
const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
|
|
39553
40290
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
39554
40291
|
if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
|
|
39555
40292
|
return true;
|
|
39556
40293
|
}
|
|
39557
|
-
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)) {
|
|
39558
40295
|
for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
|
|
39559
40296
|
return false;
|
|
39560
40297
|
}
|
|
39561
|
-
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);
|
|
39562
40299
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39563
|
-
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
|
|
39564
|
-
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;
|
|
39565
40302
|
if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39566
40303
|
return consequentHasReturn || alternateHasReturn;
|
|
39567
40304
|
}
|
|
@@ -39569,7 +40306,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39569
40306
|
let hasReturn = false;
|
|
39570
40307
|
for (const switchCase of statement.cases ?? []) {
|
|
39571
40308
|
const caseScope = createBlockBindingScope(scope);
|
|
39572
|
-
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
|
|
40309
|
+
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
|
|
39573
40310
|
hasReturn = true;
|
|
39574
40311
|
if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
|
|
39575
40312
|
}
|
|
@@ -39577,25 +40314,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39577
40314
|
return hasReturn;
|
|
39578
40315
|
}
|
|
39579
40316
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39580
|
-
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
|
|
39581
|
-
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
|
|
39582
|
-
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;
|
|
39583
40320
|
return blockHasReturn || handlerHasReturn || finalizerHasReturn;
|
|
39584
40321
|
}
|
|
39585
40322
|
if (isNodeOfType(statement, "CatchClause")) {
|
|
39586
40323
|
const catchScope = createBlockBindingScope(scope);
|
|
39587
40324
|
addPatternBindings(statement.param, catchScope);
|
|
39588
|
-
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
|
|
40325
|
+
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
|
|
39589
40326
|
}
|
|
39590
40327
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39591
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40328
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39592
40329
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
39593
40330
|
return bodyHasReturn;
|
|
39594
40331
|
}
|
|
39595
40332
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39596
40333
|
const loopScope = createBlockBindingScope(scope);
|
|
39597
40334
|
if (statement.init) addDeclarationBindings(statement.init, loopScope);
|
|
39598
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40335
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39599
40336
|
if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
|
|
39600
40337
|
if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
|
|
39601
40338
|
if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
|
|
@@ -39605,22 +40342,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
39605
40342
|
const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
|
|
39606
40343
|
const loopScope = createBlockBindingScope(scope);
|
|
39607
40344
|
if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
|
|
39608
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40345
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
39609
40346
|
addNames(names, rightNames);
|
|
39610
40347
|
return true;
|
|
39611
40348
|
}
|
|
39612
|
-
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40349
|
+
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39613
40350
|
if (isNodeOfType(statement, "WithStatement")) {
|
|
39614
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40351
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
39615
40352
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
|
|
39616
40353
|
return bodyHasReturn;
|
|
39617
40354
|
}
|
|
39618
40355
|
return false;
|
|
39619
40356
|
};
|
|
39620
|
-
const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40357
|
+
const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
39621
40358
|
const names = /* @__PURE__ */ new Set();
|
|
39622
40359
|
if (!isNodeOfType(componentBody, "BlockStatement")) return names;
|
|
39623
|
-
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
|
|
40360
|
+
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
|
|
39624
40361
|
return names;
|
|
39625
40362
|
};
|
|
39626
40363
|
//#endregion
|
|
@@ -39643,42 +40380,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
|
|
|
39643
40380
|
};
|
|
39644
40381
|
//#endregion
|
|
39645
40382
|
//#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
|
|
39646
|
-
const
|
|
39647
|
-
|
|
39648
|
-
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
39649
|
-
if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
|
|
39650
|
-
return null;
|
|
39651
|
-
};
|
|
39652
|
-
const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
|
|
39653
|
-
if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
|
|
40383
|
+
const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
|
|
40384
|
+
if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
|
|
39654
40385
|
if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
|
|
39655
40386
|
const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
|
|
39656
40387
|
return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
|
|
39657
40388
|
};
|
|
39658
|
-
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
|
|
40389
|
+
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
|
|
39659
40390
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
39660
40391
|
for (const property of node.properties ?? []) {
|
|
39661
40392
|
if (!isNodeOfType(property, "Property")) continue;
|
|
39662
40393
|
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
39663
40394
|
if (!propertyName) continue;
|
|
39664
|
-
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
40395
|
+
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
|
|
39665
40396
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
39666
40397
|
}
|
|
39667
40398
|
};
|
|
39668
|
-
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40399
|
+
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39669
40400
|
if (!isNodeOfType(statement, "VariableDeclaration")) return;
|
|
39670
40401
|
const declarationScope = getVariableDeclarationScope(statement, scope);
|
|
39671
40402
|
for (const declarator of statement.declarations ?? []) {
|
|
39672
40403
|
const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
|
|
39673
40404
|
if (!declarator.init) continue;
|
|
39674
|
-
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
|
|
40405
|
+
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39675
40406
|
for (const declaredBindingName of declaredBindingNames) {
|
|
39676
40407
|
if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
|
|
39677
|
-
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
|
|
40408
|
+
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
|
|
39678
40409
|
}
|
|
39679
40410
|
}
|
|
39680
40411
|
};
|
|
39681
|
-
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40412
|
+
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
39682
40413
|
if (isNodeOfType(statement, "FunctionDeclaration")) {
|
|
39683
40414
|
if (statement.id) {
|
|
39684
40415
|
const declaredBindingNames = addPatternBindings(statement.id, scope);
|
|
@@ -39687,61 +40418,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
|
|
|
39687
40418
|
return;
|
|
39688
40419
|
}
|
|
39689
40420
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
39690
|
-
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40421
|
+
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
39691
40422
|
return;
|
|
39692
40423
|
}
|
|
39693
40424
|
if (isNodeOfType(statement, "BlockStatement")) {
|
|
39694
|
-
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40425
|
+
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
39695
40426
|
return;
|
|
39696
40427
|
}
|
|
39697
40428
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
39698
|
-
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
|
|
39699
|
-
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);
|
|
39700
40431
|
return;
|
|
39701
40432
|
}
|
|
39702
40433
|
if (isNodeOfType(statement, "SwitchStatement")) {
|
|
39703
|
-
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);
|
|
39704
40435
|
return;
|
|
39705
40436
|
}
|
|
39706
40437
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
39707
|
-
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
|
|
40438
|
+
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
|
|
39708
40439
|
if (statement.handler) {
|
|
39709
40440
|
const catchScope = createBlockBindingScope(scope);
|
|
39710
40441
|
addPatternBindings(statement.handler.param, catchScope);
|
|
39711
|
-
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
|
|
40442
|
+
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
|
|
39712
40443
|
}
|
|
39713
|
-
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
|
|
40444
|
+
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
|
|
39714
40445
|
return;
|
|
39715
40446
|
}
|
|
39716
40447
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
39717
40448
|
const loopScope = createBlockBindingScope(scope);
|
|
39718
|
-
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
|
|
39719
|
-
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);
|
|
39720
40451
|
return;
|
|
39721
40452
|
}
|
|
39722
40453
|
if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
|
|
39723
40454
|
const loopScope = createBlockBindingScope(scope);
|
|
39724
|
-
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
|
|
40455
|
+
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
|
|
39725
40456
|
else addPatternBindings(statement.left, loopScope);
|
|
39726
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40457
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
39727
40458
|
return;
|
|
39728
40459
|
}
|
|
39729
40460
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
39730
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40461
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39731
40462
|
return;
|
|
39732
40463
|
}
|
|
39733
|
-
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40464
|
+
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
39734
40465
|
};
|
|
39735
|
-
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
|
|
39736
|
-
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);
|
|
39737
40468
|
};
|
|
39738
|
-
const collectFunctionLikeLocalNames = (componentBody) => {
|
|
40469
|
+
const collectFunctionLikeLocalNames = (componentBody, scopes) => {
|
|
39739
40470
|
const functionLikeLocalNames = /* @__PURE__ */ new Set();
|
|
39740
40471
|
if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
|
|
39741
40472
|
let previousSize = -1;
|
|
39742
40473
|
while (previousSize !== functionLikeLocalNames.size) {
|
|
39743
40474
|
previousSize = functionLikeLocalNames.size;
|
|
39744
|
-
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
|
|
40475
|
+
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
|
|
39745
40476
|
}
|
|
39746
40477
|
return functionLikeLocalNames;
|
|
39747
40478
|
};
|
|
@@ -39781,17 +40512,17 @@ const noEventTriggerState = defineRule({
|
|
|
39781
40512
|
create: (context) => {
|
|
39782
40513
|
const checkComponent = (componentBody) => {
|
|
39783
40514
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
39784
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
40515
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
39785
40516
|
if (useStateBindings.length === 0) return;
|
|
39786
40517
|
const analysis = getProgramAnalysis(componentBody);
|
|
39787
40518
|
if (!analysis) return;
|
|
39788
40519
|
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
39789
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
40520
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
39790
40521
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
39791
|
-
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
40522
|
+
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
|
|
39792
40523
|
walkAst(componentBody, (effectCall) => {
|
|
39793
40524
|
if (!isNodeOfType(effectCall, "CallExpression")) return;
|
|
39794
|
-
if (!
|
|
40525
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
39795
40526
|
if ((effectCall.arguments?.length ?? 0) < 2) return;
|
|
39796
40527
|
const depsNode = effectCall.arguments[1];
|
|
39797
40528
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -40175,6 +40906,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
40175
40906
|
"scrollHeight"
|
|
40176
40907
|
]);
|
|
40177
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
|
+
]);
|
|
40178
40920
|
const subtreeReadsDomMeasurement = (root) => {
|
|
40179
40921
|
if (!root) return false;
|
|
40180
40922
|
let found = false;
|
|
@@ -40193,29 +40935,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
40193
40935
|
});
|
|
40194
40936
|
return found;
|
|
40195
40937
|
};
|
|
40196
|
-
const
|
|
40938
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
40197
40939
|
const names = /* @__PURE__ */ new Set();
|
|
40198
40940
|
walkAst(program, (child) => {
|
|
40199
40941
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
40200
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40942
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
40201
40943
|
return;
|
|
40202
40944
|
}
|
|
40203
40945
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
40204
40946
|
let functionValue = child.init;
|
|
40205
40947
|
if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
|
|
40206
|
-
if (functionValue && isFunctionLike$1(functionValue) &&
|
|
40948
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40207
40949
|
});
|
|
40208
40950
|
return names;
|
|
40209
40951
|
};
|
|
40210
|
-
const
|
|
40211
|
-
|
|
40952
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40953
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40954
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
40212
40955
|
let found = false;
|
|
40213
40956
|
walkAst(root, (child) => {
|
|
40214
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;
|
|
40972
|
+
let found = false;
|
|
40973
|
+
walkAst(root, (child) => {
|
|
40974
|
+
if (found) return false;
|
|
40975
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
40215
40976
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
40216
40977
|
});
|
|
40217
40978
|
return found;
|
|
40218
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
|
+
};
|
|
40219
40998
|
const isInsideStartViewTransition = (node) => {
|
|
40220
40999
|
let cursor = node.parent;
|
|
40221
41000
|
while (cursor) {
|
|
@@ -40256,11 +41035,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
40256
41035
|
};
|
|
40257
41036
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
40258
41037
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
41038
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
40259
41039
|
let exempt = false;
|
|
40260
41040
|
walkAst(program, (child) => {
|
|
40261
41041
|
if (exempt) return false;
|
|
40262
41042
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
40263
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
41043
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
40264
41044
|
exempt = true;
|
|
40265
41045
|
return false;
|
|
40266
41046
|
}
|
|
@@ -40825,41 +41605,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40825
41605
|
if (leftResult === false && rightResult === false) return false;
|
|
40826
41606
|
return null;
|
|
40827
41607
|
};
|
|
40828
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41608
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40829
41609
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40830
41610
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40831
41611
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40832
41612
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40833
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
|
+
}
|
|
40834
41656
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40835
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41657
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40836
41658
|
return argumentResult === null ? null : !argumentResult;
|
|
40837
41659
|
}
|
|
40838
41660
|
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
40839
|
-
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;
|
|
40840
41725
|
};
|
|
40841
|
-
const
|
|
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;
|
|
41731
|
+
};
|
|
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) => {
|
|
40842
41737
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40843
41738
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40844
41739
|
if (predicateMatch) return {
|
|
40845
41740
|
predicateMatch,
|
|
40846
41741
|
predicateNode: unwrappedExpression
|
|
40847
41742
|
};
|
|
40848
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40849
|
-
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
|
|
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;
|
|
40856
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
|
+
});
|
|
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);
|
|
40857
41790
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40858
41791
|
if (!nestedMatch) return null;
|
|
40859
|
-
const
|
|
40860
|
-
|
|
40861
|
-
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;
|
|
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;
|
|
40862
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
|
+
});
|
|
40863
41825
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40864
41826
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40865
41827
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -41002,17 +41964,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
41002
41964
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
41003
41965
|
if (reportedNodes.has(predicateNode)) return;
|
|
41004
41966
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
41005
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41967
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
41006
41968
|
if (!componentOrHookNode) return;
|
|
41007
41969
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
41008
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41970
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41009
41971
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
41010
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41972
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
41011
41973
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
41012
41974
|
}
|
|
41013
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
41014
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
41015
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41975
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41976
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41977
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
41016
41978
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
41017
41979
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
41018
41980
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -41389,12 +42351,12 @@ const noInitializeState = defineRule({
|
|
|
41389
42351
|
tags: ["test-noise"],
|
|
41390
42352
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
41391
42353
|
create: (context) => ({ CallExpression(node) {
|
|
41392
|
-
if (!
|
|
42354
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
41393
42355
|
const dependencies = node.arguments?.[1];
|
|
41394
42356
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41395
42357
|
const analysis = getProgramAnalysis(node);
|
|
41396
42358
|
if (!analysis) return;
|
|
41397
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
42359
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
41398
42360
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
41399
42361
|
const stateName = getStateName(fact.stateDeclarator);
|
|
41400
42362
|
context.report({
|
|
@@ -41752,7 +42714,8 @@ const noJsxElementType = defineRule({
|
|
|
41752
42714
|
create: (context) => {
|
|
41753
42715
|
let isJsxImported = false;
|
|
41754
42716
|
const flaggedAnnotations = [];
|
|
41755
|
-
const
|
|
42717
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42718
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
41756
42719
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
41757
42720
|
if (!typeAnnotation) return;
|
|
41758
42721
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -41762,19 +42725,16 @@ const noJsxElementType = defineRule({
|
|
|
41762
42725
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
41763
42726
|
},
|
|
41764
42727
|
FunctionDeclaration(node) {
|
|
41765
|
-
|
|
42728
|
+
collectComponentReturnType(node, node.returnType);
|
|
41766
42729
|
},
|
|
41767
42730
|
ArrowFunctionExpression(node) {
|
|
41768
|
-
|
|
42731
|
+
collectComponentReturnType(node, node.returnType);
|
|
41769
42732
|
},
|
|
41770
42733
|
FunctionExpression(node) {
|
|
41771
|
-
|
|
42734
|
+
collectComponentReturnType(node, node.returnType);
|
|
41772
42735
|
},
|
|
41773
42736
|
TSDeclareFunction(node) {
|
|
41774
|
-
|
|
41775
|
-
},
|
|
41776
|
-
TSMethodSignature(node) {
|
|
41777
|
-
checkReturnType(node.returnType);
|
|
42737
|
+
collectComponentReturnType(node, node.returnType);
|
|
41778
42738
|
},
|
|
41779
42739
|
"Program:exit"() {
|
|
41780
42740
|
if (isJsxImported) return;
|
|
@@ -43003,7 +43963,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43003
43963
|
const setterElement = elements[1];
|
|
43004
43964
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43005
43965
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43006
|
-
if (!
|
|
43966
|
+
if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
|
|
43007
43967
|
const initializer = declarator.init.arguments?.[0];
|
|
43008
43968
|
if (!initializer) continue;
|
|
43009
43969
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43021,7 +43981,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43021
43981
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43022
43982
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43023
43983
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43024
|
-
if (!
|
|
43984
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
43025
43985
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43026
43986
|
const depsNode = effectCall.arguments[1];
|
|
43027
43987
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -43430,7 +44390,7 @@ const noMultiComp = defineRule({
|
|
|
43430
44390
|
});
|
|
43431
44391
|
//#endregion
|
|
43432
44392
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
43433
|
-
const collectUseRefBindingNames = (componentBody) => {
|
|
44393
|
+
const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
43434
44394
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
43435
44395
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
43436
44396
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -43438,7 +44398,7 @@ const collectUseRefBindingNames = (componentBody) => {
|
|
|
43438
44398
|
for (const declarator of statement.declarations ?? []) {
|
|
43439
44399
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
43440
44400
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43441
|
-
if (!
|
|
44401
|
+
if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
|
|
43442
44402
|
useRefBindings.add(declarator.id.name);
|
|
43443
44403
|
}
|
|
43444
44404
|
}
|
|
@@ -43473,12 +44433,12 @@ const noMutableInDeps = defineRule({
|
|
|
43473
44433
|
create: (context) => {
|
|
43474
44434
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
43475
44435
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
43476
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44436
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
|
|
43477
44437
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
43478
44438
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
43479
44439
|
walkAst(componentBody, (child) => {
|
|
43480
44440
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
43481
|
-
if (!
|
|
44441
|
+
if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
43482
44442
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
43483
44443
|
const depsNode = child.arguments[1];
|
|
43484
44444
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -45308,6 +46268,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
45308
46268
|
"useMatchMedia",
|
|
45309
46269
|
"useMediaJobProgress",
|
|
45310
46270
|
"useMediaQuery",
|
|
46271
|
+
"useMediaQueryState",
|
|
45311
46272
|
"useResizeObserver",
|
|
45312
46273
|
"useVisibility",
|
|
45313
46274
|
"useWindowSize"
|
|
@@ -45344,9 +46305,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
45344
46305
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
45345
46306
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
45346
46307
|
};
|
|
45347
|
-
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) => {
|
|
45348
46328
|
const identifier = ref.identifier;
|
|
45349
46329
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46330
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46331
|
+
if (localHookProof !== null) return localHookProof;
|
|
45350
46332
|
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45351
46333
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
45352
46334
|
const node = def.node;
|
|
@@ -45369,16 +46351,10 @@ const noPassDataToParent = defineRule({
|
|
|
45369
46351
|
tags: ["test-noise"],
|
|
45370
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",
|
|
45371
46353
|
create: (context) => {
|
|
45372
|
-
const isReactUseRefCall = (node) =>
|
|
45373
|
-
|
|
45374
|
-
allowUnboundBareCalls: true
|
|
45375
|
-
});
|
|
45376
|
-
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
45377
|
-
allowGlobalReactNamespace: true,
|
|
45378
|
-
allowUnboundBareCalls: true
|
|
45379
|
-
});
|
|
46354
|
+
const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
|
|
46355
|
+
const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
|
|
45380
46356
|
return { CallExpression(node) {
|
|
45381
|
-
if (!
|
|
46357
|
+
if (!isReactUseEffectCall(node)) return;
|
|
45382
46358
|
const analysis = getProgramAnalysis(node);
|
|
45383
46359
|
if (!analysis) return;
|
|
45384
46360
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -45430,11 +46406,11 @@ const noPassDataToParent = defineRule({
|
|
|
45430
46406
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
45431
46407
|
}
|
|
45432
46408
|
return getDownstreamRefs(analysis, argument);
|
|
45433
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46409
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
45434
46410
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45435
46411
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
45436
46412
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
45437
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46413
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
45438
46414
|
if (isProp(analysis, argRef)) return false;
|
|
45439
46415
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
45440
46416
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -45665,7 +46641,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
45665
46641
|
tags: ["test-noise"],
|
|
45666
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",
|
|
45667
46643
|
create: (context) => ({ CallExpression(node) {
|
|
45668
|
-
if (!
|
|
46644
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
45669
46645
|
const analysis = getProgramAnalysis(node);
|
|
45670
46646
|
if (!analysis) return;
|
|
45671
46647
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -46080,7 +47056,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
46080
47056
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
46081
47057
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
46082
47058
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
46083
|
-
const calleeName = getCalleeName$
|
|
47059
|
+
const calleeName = getCalleeName$1(binding.initializer);
|
|
46084
47060
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
46085
47061
|
}
|
|
46086
47062
|
return false;
|
|
@@ -46102,7 +47078,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
46102
47078
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
46103
47079
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
46104
47080
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
46105
|
-
if (getCalleeName$
|
|
47081
|
+
if (getCalleeName$1(binding.initializer) !== "useRef") return null;
|
|
46106
47082
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
46107
47083
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
46108
47084
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -46134,7 +47110,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
46134
47110
|
return {
|
|
46135
47111
|
...propStackTracker.visitors,
|
|
46136
47112
|
CallExpression(node) {
|
|
46137
|
-
if (!
|
|
47113
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
46138
47114
|
const callback = getEffectCallback(node);
|
|
46139
47115
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
46140
47116
|
const depsNode = node.arguments[1];
|
|
@@ -46998,6 +47974,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
46998
47974
|
}
|
|
46999
47975
|
});
|
|
47000
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
|
|
47001
48040
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
47002
48041
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
47003
48042
|
"DoWhileStatement",
|
|
@@ -47964,7 +49003,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
47964
49003
|
tags: ["test-noise"],
|
|
47965
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",
|
|
47966
49005
|
create: (context) => ({ CallExpression(node) {
|
|
47967
|
-
if (!
|
|
49006
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
47968
49007
|
const analysis = getProgramAnalysis(node);
|
|
47969
49008
|
if (!analysis) return;
|
|
47970
49009
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -48233,7 +49272,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
48233
49272
|
if (node.callee.property.name !== "handler") return false;
|
|
48234
49273
|
let currentNode = node.callee.object;
|
|
48235
49274
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
48236
|
-
const calleeName = getCalleeName$
|
|
49275
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
48237
49276
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
48238
49277
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
48239
49278
|
currentNode = currentNode.callee.object;
|
|
@@ -48734,7 +49773,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
48734
49773
|
create: (context) => {
|
|
48735
49774
|
const checkFunctionScope = (functionBody) => {
|
|
48736
49775
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
48737
|
-
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49776
|
+
const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
|
|
48738
49777
|
if (useStateBindings.length === 0) return;
|
|
48739
49778
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
48740
49779
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -48743,7 +49782,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
48743
49782
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
48744
49783
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
48745
49784
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
48746
|
-
if (!
|
|
49785
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
48747
49786
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
48748
49787
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
48749
49788
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -48829,7 +49868,7 @@ const noSetStateInRender = defineRule({
|
|
|
48829
49868
|
create: (context) => {
|
|
48830
49869
|
const checkComponent = (componentBody) => {
|
|
48831
49870
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
48832
|
-
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49871
|
+
const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
|
|
48833
49872
|
if (setterNames.size === 0) return;
|
|
48834
49873
|
for (const statement of componentBody.body ?? []) {
|
|
48835
49874
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -49078,10 +50117,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
49078
50117
|
});
|
|
49079
50118
|
return facts;
|
|
49080
50119
|
};
|
|
49081
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
50120
|
+
const isEffectCallbackFunction = (functionNode, scopes) => {
|
|
49082
50121
|
const parent = functionNode.parent;
|
|
49083
50122
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
49084
|
-
return
|
|
50123
|
+
return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
|
|
49085
50124
|
};
|
|
49086
50125
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
49087
50126
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -49100,24 +50139,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
49100
50139
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49101
50140
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
49102
50141
|
};
|
|
49103
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50142
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
|
|
49104
50143
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
49105
50144
|
if (cleanupBindingName === null) return false;
|
|
49106
50145
|
let isReturnedFromEffect = false;
|
|
49107
50146
|
walkAst(ownerScope, (child) => {
|
|
49108
50147
|
if (isReturnedFromEffect) return false;
|
|
49109
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50148
|
+
if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
49110
50149
|
const effectCallback = getEffectCallback(child);
|
|
49111
50150
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
49112
50151
|
});
|
|
49113
50152
|
return isReturnedFromEffect;
|
|
49114
50153
|
};
|
|
49115
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50154
|
+
const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
|
|
49116
50155
|
let functionNode = findEnclosingFunction$1(node);
|
|
49117
50156
|
while (functionNode) {
|
|
49118
50157
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
49119
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
49120
|
-
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;
|
|
49121
50160
|
functionNode = outerFunction;
|
|
49122
50161
|
}
|
|
49123
50162
|
return false;
|
|
@@ -49153,10 +50192,10 @@ const noStaleTimerRef = defineRule({
|
|
|
49153
50192
|
if (isShadowedTimerGlobal(node)) return;
|
|
49154
50193
|
const { clearCalleeName, refName } = clearCall;
|
|
49155
50194
|
const refBinding = findVariableInitializer(node, refName);
|
|
49156
|
-
if (!refBinding?.initializer || !
|
|
50195
|
+
if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
|
|
49157
50196
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
49158
50197
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
49159
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50198
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
|
|
49160
50199
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
49161
50200
|
context.report({
|
|
49162
50201
|
node,
|
|
@@ -54960,7 +55999,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
54960
55999
|
allReadsAreInSubHandlers = false;
|
|
54961
56000
|
return;
|
|
54962
56001
|
}
|
|
54963
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
56002
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
|
|
54964
56003
|
});
|
|
54965
56004
|
return {
|
|
54966
56005
|
hasAnyRead,
|
|
@@ -54983,7 +56022,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
54983
56022
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
54984
56023
|
const effectCall = statement.expression;
|
|
54985
56024
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
54986
|
-
if (!
|
|
56025
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
54987
56026
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
54988
56027
|
const depsNode = effectCall.arguments[1];
|
|
54989
56028
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -55015,11 +56054,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
55015
56054
|
});
|
|
55016
56055
|
//#endregion
|
|
55017
56056
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
55018
|
-
const findUseEffectsInComponent = (componentBody) => {
|
|
56057
|
+
const findUseEffectsInComponent = (componentBody, scopes) => {
|
|
55019
56058
|
const effectCalls = [];
|
|
55020
56059
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
55021
56060
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
55022
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56061
|
+
if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
|
|
55023
56062
|
});
|
|
55024
56063
|
return effectCalls;
|
|
55025
56064
|
};
|
|
@@ -55222,7 +56261,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55222
56261
|
};
|
|
55223
56262
|
const checkComponent = (componentBody) => {
|
|
55224
56263
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
55225
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56264
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
55226
56265
|
if (useStateBindings.length === 0) return;
|
|
55227
56266
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
55228
56267
|
for (const binding of useStateBindings) {
|
|
@@ -55235,7 +56274,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55235
56274
|
}
|
|
55236
56275
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
55237
56276
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
55238
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56277
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55239
56278
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55240
56279
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55241
56280
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55277,7 +56316,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
55277
56316
|
})).filter((candidate) => candidate.storeName !== null);
|
|
55278
56317
|
if (snapshotBindings.length === 0) return;
|
|
55279
56318
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
55280
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56319
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
55281
56320
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55282
56321
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55283
56322
|
const depsNode = effectCall.arguments[1];
|
|
@@ -55374,7 +56413,7 @@ const preferUseReducer = defineRule({
|
|
|
55374
56413
|
create: (context) => {
|
|
55375
56414
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
55376
56415
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
55377
|
-
const bindings = collectUseStateBindings(body);
|
|
56416
|
+
const bindings = collectUseStateBindings(body, context.scopes);
|
|
55378
56417
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
55379
56418
|
if (setterNames.size < 5) return;
|
|
55380
56419
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -55590,7 +56629,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
55590
56629
|
]);
|
|
55591
56630
|
const isQueryCacheSourceCall = (initializer) => {
|
|
55592
56631
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
55593
|
-
const hookName = getCalleeName$
|
|
56632
|
+
const hookName = getCalleeName$1(initializer);
|
|
55594
56633
|
if (!hookName) return false;
|
|
55595
56634
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
55596
56635
|
};
|
|
@@ -55722,7 +56761,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
55722
56761
|
},
|
|
55723
56762
|
CallExpression(node) {
|
|
55724
56763
|
if (!hasQueryReadUsage) {
|
|
55725
|
-
const callName = getCalleeName$
|
|
56764
|
+
const callName = getCalleeName$1(node);
|
|
55726
56765
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
55727
56766
|
}
|
|
55728
56767
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -56959,8 +57998,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56959
57998
|
destructureIndex: 1
|
|
56960
57999
|
});
|
|
56961
58000
|
//#endregion
|
|
58001
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
58002
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
58003
|
+
//#endregion
|
|
56962
58004
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56963
58005
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
58006
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
58007
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
58008
|
+
const REACT_API_CALL_OPTIONS = {
|
|
58009
|
+
allowGlobalReactNamespace: true,
|
|
58010
|
+
allowUnboundBareCalls: true,
|
|
58011
|
+
resolveNamedAliases: true
|
|
58012
|
+
};
|
|
58013
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
58014
|
+
let readsDerivedSymbol = false;
|
|
58015
|
+
walkAst(expression, (node) => {
|
|
58016
|
+
if (readsDerivedSymbol) return false;
|
|
58017
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
58018
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
58019
|
+
});
|
|
58020
|
+
return readsDerivedSymbol;
|
|
58021
|
+
};
|
|
58022
|
+
const getStaticObjectPropertyName = (property) => {
|
|
58023
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
58024
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
58025
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
58026
|
+
return null;
|
|
58027
|
+
};
|
|
58028
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
58029
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
58030
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
58031
|
+
const parent = expressionRoot.parent;
|
|
58032
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
58033
|
+
};
|
|
56964
58034
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56965
58035
|
let readsCurrent = false;
|
|
56966
58036
|
walkAst(argument, (child) => {
|
|
@@ -57012,6 +58082,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
57012
58082
|
});
|
|
57013
58083
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
57014
58084
|
};
|
|
58085
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
58086
|
+
const member = stripParenExpression(node);
|
|
58087
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
58088
|
+
const receiver = stripParenExpression(member.object);
|
|
58089
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
58090
|
+
};
|
|
58091
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
58092
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58093
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
58094
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
58095
|
+
const argument = call.arguments[0];
|
|
58096
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
58097
|
+
};
|
|
58098
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
58099
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58100
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
58101
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
58102
|
+
const eventName = call.arguments[0];
|
|
58103
|
+
const handler = call.arguments[1];
|
|
58104
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
58105
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
58106
|
+
};
|
|
58107
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
58108
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
58109
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
58110
|
+
if (cleanupStatements.length !== 1) return null;
|
|
58111
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
58112
|
+
};
|
|
58113
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
58114
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
58115
|
+
const componentBody = componentFunction.body;
|
|
58116
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
58117
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
58118
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
58119
|
+
const declarator = setterSymbol.declarationNode;
|
|
58120
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
58121
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
58122
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
58123
|
+
if (!isNodeOfType(stateIdentifier, "Identifier") || !isNodeOfType(setterIdentifier, "Identifier") || setterIdentifier !== setterSymbol.bindingIdentifier || !isNodeOfType(declarator.init, "CallExpression") || !isReactApiCall(declarator.init, USE_STATE_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return null;
|
|
58124
|
+
const initializer = declarator.init.arguments?.[0];
|
|
58125
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
58126
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
58127
|
+
if (!stateSymbol) return null;
|
|
58128
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
58129
|
+
let didAddDerivedSymbol = true;
|
|
58130
|
+
while (didAddDerivedSymbol) {
|
|
58131
|
+
didAddDerivedSymbol = false;
|
|
58132
|
+
for (const statement of componentBody.body ?? []) {
|
|
58133
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
58134
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
58135
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
58136
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
58137
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
58138
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
58139
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
58140
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
58141
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
58142
|
+
didAddDerivedSymbol = true;
|
|
58143
|
+
}
|
|
58144
|
+
}
|
|
58145
|
+
}
|
|
58146
|
+
}
|
|
58147
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
58148
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58149
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58150
|
+
if (!symbol) return false;
|
|
58151
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
58152
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58153
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58154
|
+
let hasUnknownReference = false;
|
|
58155
|
+
walkAst(componentBody, (node) => {
|
|
58156
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
58157
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
58158
|
+
const parent = referenceRoot.parent;
|
|
58159
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
58160
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
58161
|
+
hasUnknownReference = true;
|
|
58162
|
+
return false;
|
|
58163
|
+
});
|
|
58164
|
+
return !hasUnknownReference;
|
|
58165
|
+
};
|
|
58166
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58167
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58168
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
58169
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
58170
|
+
if (cachedVisibility) return cachedVisibility;
|
|
58171
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
58172
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
58173
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
58174
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58175
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58176
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
58177
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
58178
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58179
|
+
return visibility;
|
|
58180
|
+
}
|
|
58181
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
58182
|
+
let visibility = "non-visible";
|
|
58183
|
+
for (const property of initializer.properties ?? []) {
|
|
58184
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
58185
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
58186
|
+
visibility = "unknown";
|
|
58187
|
+
break;
|
|
58188
|
+
}
|
|
58189
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
58190
|
+
}
|
|
58191
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58192
|
+
return visibility;
|
|
58193
|
+
};
|
|
58194
|
+
let hasNonAriaReference = false;
|
|
58195
|
+
walkAst(componentBody, (node) => {
|
|
58196
|
+
if (hasNonAriaReference) return false;
|
|
58197
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
58198
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
58199
|
+
const parent = node.parent;
|
|
58200
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
58201
|
+
let cursor = parent;
|
|
58202
|
+
while (cursor && cursor !== componentBody) {
|
|
58203
|
+
if (isFunctionLike$1(cursor)) return;
|
|
58204
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
58205
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
58206
|
+
return;
|
|
58207
|
+
}
|
|
58208
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
58209
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
58210
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
58211
|
+
return;
|
|
58212
|
+
}
|
|
58213
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
58214
|
+
hasNonAriaReference = true;
|
|
58215
|
+
return;
|
|
58216
|
+
}
|
|
58217
|
+
cursor = cursor.parent;
|
|
58218
|
+
}
|
|
58219
|
+
});
|
|
58220
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
58221
|
+
};
|
|
58222
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
58223
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
58224
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
58225
|
+
const statements = getCallbackStatements(callback);
|
|
58226
|
+
if (statements.length !== 4) return false;
|
|
58227
|
+
const handlerDeclaration = statements[0];
|
|
58228
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
58229
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
58230
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
58231
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
58232
|
+
if (handlerStatements.length !== 1) return false;
|
|
58233
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
58234
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
58235
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
58236
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
58237
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
58238
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
58239
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
58240
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
58241
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
58242
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
58243
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
58244
|
+
};
|
|
57015
58245
|
const renderingHydrationNoFlicker = defineRule({
|
|
57016
58246
|
id: "rendering-hydration-no-flicker",
|
|
57017
58247
|
title: "useEffect setState flashes on mount",
|
|
@@ -57024,7 +58254,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
57024
58254
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
57025
58255
|
const callback = getEffectCallback(node);
|
|
57026
58256
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
57027
|
-
|
|
58257
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
58258
|
+
context.report({
|
|
58259
|
+
node,
|
|
58260
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
58261
|
+
});
|
|
58262
|
+
return;
|
|
58263
|
+
}
|
|
58264
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
57028
58265
|
if (bodyStatements.length !== 1) return;
|
|
57029
58266
|
const soleStatement = bodyStatements[0];
|
|
57030
58267
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -57187,6 +58424,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
57187
58424
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
57188
58425
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
57189
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
|
+
};
|
|
57190
58546
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
57191
58547
|
let found = false;
|
|
57192
58548
|
walkAst(fnBody, (child) => {
|
|
@@ -57360,6 +58716,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
57360
58716
|
const fnBody = enclosingFunctionBody(node);
|
|
57361
58717
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
57362
58718
|
if (fnBody && setterName) {
|
|
58719
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58720
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
57363
58721
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
57364
58722
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
57365
58723
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -57691,7 +59049,7 @@ const rerenderDependencies = defineRule({
|
|
|
57691
59049
|
severity: "error",
|
|
57692
59050
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
57693
59051
|
create: (context) => ({ CallExpression(node) {
|
|
57694
|
-
if (!
|
|
59052
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
57695
59053
|
const depsNode = node.arguments[1];
|
|
57696
59054
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
57697
59055
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -57946,7 +59304,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
57946
59304
|
category: "Performance",
|
|
57947
59305
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
57948
59306
|
create: (context) => ({ CallExpression(node) {
|
|
57949
|
-
if (!
|
|
59307
|
+
if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
|
|
57950
59308
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
57951
59309
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
57952
59310
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -58010,7 +59368,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
58010
59368
|
category: "Performance",
|
|
58011
59369
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
58012
59370
|
create: (context) => ({ CallExpression(node) {
|
|
58013
|
-
if (!
|
|
59371
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
58014
59372
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
58015
59373
|
if (!initializer) return;
|
|
58016
59374
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -58773,11 +60131,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
58773
60131
|
}
|
|
58774
60132
|
return false;
|
|
58775
60133
|
};
|
|
58776
|
-
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60134
|
+
const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
58777
60135
|
const effectInfos = [];
|
|
58778
60136
|
walkAst(componentBody, (child) => {
|
|
58779
60137
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58780
|
-
if (!
|
|
60138
|
+
if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
58781
60139
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
58782
60140
|
for (const argument of child.arguments ?? []) {
|
|
58783
60141
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -58833,13 +60191,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
|
58833
60191
|
});
|
|
58834
60192
|
return effectInfos;
|
|
58835
60193
|
};
|
|
58836
|
-
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60194
|
+
const collectCustomHookArgumentNames = (componentBody, scopes) => {
|
|
58837
60195
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
58838
60196
|
walkAst(componentBody, (child) => {
|
|
58839
60197
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58840
60198
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
58841
60199
|
const calleeName = child.callee.name;
|
|
58842
60200
|
if (!isReactHookName(calleeName)) return;
|
|
60201
|
+
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
58843
60202
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
58844
60203
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
58845
60204
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -58880,21 +60239,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
58880
60239
|
create: (context) => {
|
|
58881
60240
|
const checkComponent = (componentBody) => {
|
|
58882
60241
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
58883
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
60242
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
58884
60243
|
if (bindings.length === 0) return;
|
|
58885
60244
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
58886
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60245
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
58887
60246
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
58888
|
-
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60247
|
+
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
58889
60248
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
58890
60249
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
58891
60250
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
58892
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60251
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
|
|
58893
60252
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
58894
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);
|
|
58895
60254
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
58896
60255
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
58897
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60256
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
58898
60257
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
58899
60258
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
58900
60259
|
walkAst(componentBody, (child) => {
|
|
@@ -66795,7 +68154,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
66795
68154
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
66796
68155
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
66797
68156
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
66798
|
-
const calleeName = getCalleeName$
|
|
68157
|
+
const calleeName = getCalleeName$1(argument);
|
|
66799
68158
|
if (!calleeName) continue;
|
|
66800
68159
|
if (isAuthGuardName(calleeName)) return true;
|
|
66801
68160
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -67435,7 +68794,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
67435
68794
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
67436
68795
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
67437
68796
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
67438
|
-
const calleeName = getCalleeName$
|
|
68797
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
67439
68798
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
67440
68799
|
result.isServerFnChain = true;
|
|
67441
68800
|
const optionsArgument = currentNode.arguments?.[0];
|
|
@@ -71567,6 +72926,17 @@ const reactDoctorRules = [
|
|
|
71567
72926
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
71568
72927
|
}
|
|
71569
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
|
+
},
|
|
71570
72940
|
{
|
|
71571
72941
|
key: "react-doctor/no-ref-current-in-render",
|
|
71572
72942
|
id: "no-ref-current-in-render",
|