oxlint-plugin-react-doctor 0.7.9-dev.4f3848b → 0.7.9-dev.51ecd7b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +510 -338
- 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,38 +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, allowPatternBinding = false) => {
|
|
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")) return null;
|
|
1246
|
-
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
|
|
1247
|
-
visitedSymbolIds.add(symbol.id);
|
|
1248
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
1249
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
1250
|
-
symbol = scopes.symbolFor(initializer);
|
|
1251
|
-
}
|
|
1252
|
-
return symbol;
|
|
1253
|
-
};
|
|
1254
|
-
//#endregion
|
|
1255
1340
|
//#region src/plugin/utils/resolve-exact-local-function.ts
|
|
1256
1341
|
const resolveExactLocalFunction = (expression, scopes) => {
|
|
1257
1342
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -1297,9 +1382,17 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1297
1382
|
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1298
1383
|
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1299
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
|
|
1300
1393
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1394
|
+
const REACT_STABLE_HANDLER_HOOK_NAMES = new Set(["useCallback", "useEffectEvent"]);
|
|
1395
|
+
const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1303
1396
|
"useEffectEvent",
|
|
1304
1397
|
"useEvent",
|
|
1305
1398
|
"useEventCallback",
|
|
@@ -1308,21 +1401,21 @@ const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
|
1308
1401
|
]);
|
|
1309
1402
|
const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
|
|
1310
1403
|
const isThrottledHandlerHookCall = (callNode) => {
|
|
1311
|
-
const calleeName = getCalleeName$
|
|
1404
|
+
const calleeName = getCalleeName$1(callNode);
|
|
1312
1405
|
return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
|
|
1313
1406
|
};
|
|
1314
|
-
const isEmptyDepsUseMemoCall = (callNode) => {
|
|
1315
|
-
if (!
|
|
1407
|
+
const isEmptyDepsUseMemoCall = (callNode, scopes) => {
|
|
1408
|
+
if (!isReactHookCall(callNode, "useMemo", scopes)) return false;
|
|
1316
1409
|
const memoDepsNode = callNode.arguments?.[1];
|
|
1317
1410
|
return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
|
|
1318
1411
|
};
|
|
1319
|
-
const isStableHandlerInitializer = (initializer) => {
|
|
1320
|
-
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);
|
|
1321
1414
|
return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
|
|
1322
1415
|
};
|
|
1323
|
-
const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
|
|
1416
|
+
const isStableRefReceiverDep = (referenceNode, receiverDepName, scopes) => {
|
|
1324
1417
|
const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
|
|
1325
|
-
return Boolean(receiverBinding?.initializer &&
|
|
1418
|
+
return Boolean(receiverBinding?.initializer && isReactHookCall(receiverBinding.initializer, "useRef", scopes));
|
|
1326
1419
|
};
|
|
1327
1420
|
const advancedEventHandlerRefs = defineRule({
|
|
1328
1421
|
id: "advanced-event-handler-refs",
|
|
@@ -1332,7 +1425,7 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1332
1425
|
category: "Performance",
|
|
1333
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.",
|
|
1334
1427
|
create: (context) => ({ CallExpression(node) {
|
|
1335
|
-
if (!
|
|
1428
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
1336
1429
|
if ((node.arguments?.length ?? 0) < 2) return;
|
|
1337
1430
|
const callback = getEffectCallback(node);
|
|
1338
1431
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -1356,8 +1449,8 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1356
1449
|
});
|
|
1357
1450
|
if (!registeredHandlerName) return;
|
|
1358
1451
|
const handlerBinding = findVariableInitializer(node, registeredHandlerName);
|
|
1359
|
-
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
|
|
1360
|
-
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;
|
|
1361
1454
|
context.report({
|
|
1362
1455
|
node,
|
|
1363
1456
|
message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
|
|
@@ -1844,15 +1937,6 @@ const flattenJsxName$1 = (node) => {
|
|
|
1844
1937
|
return null;
|
|
1845
1938
|
};
|
|
1846
1939
|
//#endregion
|
|
1847
|
-
//#region src/plugin/utils/get-static-property-name.ts
|
|
1848
|
-
const getStaticPropertyName = (memberExpression) => {
|
|
1849
|
-
const property = memberExpression.property;
|
|
1850
|
-
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
1851
|
-
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
1852
|
-
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
1853
|
-
return null;
|
|
1854
|
-
};
|
|
1855
|
-
//#endregion
|
|
1856
1940
|
//#region src/plugin/utils/is-generated-image-renderer-call.ts
|
|
1857
1941
|
const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
1858
1942
|
"next/og",
|
|
@@ -4860,23 +4944,6 @@ const containsDirectAwait = (node) => {
|
|
|
4860
4944
|
return foundAwait;
|
|
4861
4945
|
};
|
|
4862
4946
|
//#endregion
|
|
4863
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
4864
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
4865
|
-
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
4866
|
-
const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
|
|
4867
|
-
if (node.computed) {
|
|
4868
|
-
if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
|
|
4869
|
-
if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
|
|
4870
|
-
return null;
|
|
4871
|
-
}
|
|
4872
|
-
if (isNodeOfType(key, "Identifier")) return key.name;
|
|
4873
|
-
if (isNodeOfType(key, "Literal")) {
|
|
4874
|
-
if (typeof key.value === "string") return key.value;
|
|
4875
|
-
if (options.stringifyNonStringLiterals) return String(key.value);
|
|
4876
|
-
}
|
|
4877
|
-
return null;
|
|
4878
|
-
};
|
|
4879
|
-
//#endregion
|
|
4880
4947
|
//#region src/plugin/utils/get-destructured-binding-property-name.ts
|
|
4881
4948
|
const getDestructuredBindingPropertyName = (bindingIdentifier) => {
|
|
4882
4949
|
let bindingNode = bindingIdentifier;
|
|
@@ -8941,65 +9008,6 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
|
|
|
8941
9008
|
}
|
|
8942
9009
|
};
|
|
8943
9010
|
//#endregion
|
|
8944
|
-
//#region src/plugin/utils/is-react-api-call.ts
|
|
8945
|
-
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
8946
|
-
const isImportedFromReact = (symbol) => {
|
|
8947
|
-
if (symbol.kind !== "import") return false;
|
|
8948
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
8949
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
8950
|
-
};
|
|
8951
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
8952
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
8953
|
-
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
8954
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8955
|
-
const importedName = getImportedName(symbol.declarationNode);
|
|
8956
|
-
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
8957
|
-
};
|
|
8958
|
-
const isReactNamespaceImport = (identifier, scopes) => {
|
|
8959
|
-
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
8960
|
-
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8961
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8962
|
-
};
|
|
8963
|
-
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8964
|
-
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8965
|
-
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8966
|
-
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
8967
|
-
};
|
|
8968
|
-
const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
|
|
8969
|
-
const symbol = scopes.symbolFor(identifier);
|
|
8970
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
8971
|
-
const pattern = symbol.declarationNode.id;
|
|
8972
|
-
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
8973
|
-
for (const property of pattern.properties) {
|
|
8974
|
-
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8975
|
-
const propertyName = getStaticPropertyKeyName(property);
|
|
8976
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8977
|
-
}
|
|
8978
|
-
return false;
|
|
8979
|
-
};
|
|
8980
|
-
const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
8981
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
8982
|
-
return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
|
|
8983
|
-
};
|
|
8984
|
-
const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
|
|
8985
|
-
const callee = stripParenExpression(rawCallee);
|
|
8986
|
-
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));
|
|
8987
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8988
|
-
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
8989
|
-
if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
|
|
8990
|
-
if (options.resolveConditionalAliases) {
|
|
8991
|
-
const symbol = scopes.symbolFor(callee);
|
|
8992
|
-
if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
|
|
8993
|
-
visitedSymbolIds.add(symbol.id);
|
|
8994
|
-
return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
|
|
8995
|
-
}
|
|
8996
|
-
}
|
|
8997
|
-
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8998
|
-
}
|
|
8999
|
-
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
9000
|
-
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
9001
|
-
};
|
|
9002
|
-
//#endregion
|
|
9003
9011
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
9004
9012
|
const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
|
|
9005
9013
|
"AbortSignal",
|
|
@@ -13883,6 +13891,7 @@ const walkInsideStatementBlocks = (node, visitor) => {
|
|
|
13883
13891
|
};
|
|
13884
13892
|
//#endregion
|
|
13885
13893
|
//#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
|
|
13894
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13886
13895
|
const getSubscribeLikeMethodName = (node) => {
|
|
13887
13896
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
13888
13897
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
@@ -13893,6 +13902,11 @@ const isSubscribeLikeCallExpression = (node) => {
|
|
|
13893
13902
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13894
13903
|
return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
|
|
13895
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;
|
|
13896
13910
|
const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
13897
13911
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13898
13912
|
if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
|
|
@@ -13945,7 +13959,6 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13945
13959
|
};
|
|
13946
13960
|
//#endregion
|
|
13947
13961
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13948
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13949
13962
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13950
13963
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13951
13964
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -13955,10 +13968,6 @@ const RESOURCE_NOUN_BY_KIND = {
|
|
|
13955
13968
|
socket: "connection"
|
|
13956
13969
|
};
|
|
13957
13970
|
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
13958
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
13959
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
13960
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
13961
|
-
};
|
|
13962
13971
|
const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13963
13972
|
if (!expression) return null;
|
|
13964
13973
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -14060,12 +14069,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
14060
14069
|
});
|
|
14061
14070
|
return;
|
|
14062
14071
|
}
|
|
14063
|
-
|
|
14072
|
+
const subscribeOrObserveMethodName = getSubscribeOrObserveMethodName(child);
|
|
14073
|
+
if (subscribeOrObserveMethodName !== null) {
|
|
14064
14074
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
14065
14075
|
usages.push({
|
|
14066
14076
|
kind: "subscribe",
|
|
14067
14077
|
node: child,
|
|
14068
|
-
resourceName:
|
|
14078
|
+
resourceName: subscribeOrObserveMethodName,
|
|
14069
14079
|
handleKey: findAssignedResourceKey(child, context),
|
|
14070
14080
|
...registrationDetails
|
|
14071
14081
|
});
|
|
@@ -14596,7 +14606,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
14596
14606
|
walkAst(componentFunction.body, (child) => {
|
|
14597
14607
|
if (didFindUnmountCleanup) return false;
|
|
14598
14608
|
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction) return;
|
|
14599
|
-
if (!
|
|
14609
|
+
if (!isReactHookCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
14600
14610
|
const dependencyList = child.arguments?.[1];
|
|
14601
14611
|
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
14602
14612
|
const cleanupCallback = getEffectCallback(child);
|
|
@@ -15027,7 +15037,7 @@ const getReleaseVerbName = (node) => {
|
|
|
15027
15037
|
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
15028
15038
|
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
15029
15039
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15030
|
-
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
15040
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction, context) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
15031
15041
|
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
15032
15042
|
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
15033
15043
|
if (controllerKey === null || refCurrentKey === null) return false;
|
|
@@ -15174,7 +15184,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15174
15184
|
return true;
|
|
15175
15185
|
};
|
|
15176
15186
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
15177
|
-
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
15187
|
+
const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
15178
15188
|
let currentNode = functionNode;
|
|
15179
15189
|
let parentNode = currentNode.parent;
|
|
15180
15190
|
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
@@ -15183,10 +15193,10 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
15183
15193
|
}
|
|
15184
15194
|
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
15185
15195
|
const effectCall = effectCallback?.parent;
|
|
15186
|
-
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") &&
|
|
15196
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes));
|
|
15187
15197
|
};
|
|
15188
15198
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
15189
|
-
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
15199
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
|
|
15190
15200
|
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15191
15201
|
if (!bindingIdentifier) return false;
|
|
15192
15202
|
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
@@ -15683,7 +15693,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
|
15683
15693
|
return false;
|
|
15684
15694
|
}
|
|
15685
15695
|
}
|
|
15686
|
-
if (
|
|
15696
|
+
if (isSubscribeOrObserveCallExpression(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
15687
15697
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15688
15698
|
const subscriptionUsage = {
|
|
15689
15699
|
kind: "subscribe",
|
|
@@ -15891,7 +15901,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
|
15891
15901
|
if (!isFunctionLike$1(functionNode)) return false;
|
|
15892
15902
|
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15893
15903
|
const callbackCall = functionRoot.parent;
|
|
15894
|
-
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;
|
|
15895
15905
|
const parentNode = functionNode.parent;
|
|
15896
15906
|
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
15897
15907
|
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
@@ -15927,12 +15937,12 @@ const effectNeedsCleanup = defineRule({
|
|
|
15927
15937
|
};
|
|
15928
15938
|
return {
|
|
15929
15939
|
CallExpression(node) {
|
|
15930
|
-
if (
|
|
15940
|
+
if (isReactHookCall(node, "useCallback", context.scopes)) {
|
|
15931
15941
|
const retainedCallback = getEffectCallback(node);
|
|
15932
15942
|
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
15933
15943
|
return;
|
|
15934
15944
|
}
|
|
15935
|
-
if (!
|
|
15945
|
+
if (!isReactHookCall(node, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
|
|
15936
15946
|
const callback = getEffectCallback(node);
|
|
15937
15947
|
if (!callback) return;
|
|
15938
15948
|
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
@@ -15940,7 +15950,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
15940
15950
|
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
15941
15951
|
if (!firstUsage) return;
|
|
15942
15952
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
15943
|
-
const hookName = getCalleeName$
|
|
15953
|
+
const hookName = getCalleeName$1(node) ?? "effect";
|
|
15944
15954
|
context.report({
|
|
15945
15955
|
node,
|
|
15946
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.`
|
|
@@ -17283,7 +17293,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
|
17283
17293
|
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
17284
17294
|
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
17285
17295
|
};
|
|
17286
|
-
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);
|
|
17287
17328
|
//#endregion
|
|
17288
17329
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
17289
17330
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -18466,7 +18507,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
18466
18507
|
if (!isUsed) continue;
|
|
18467
18508
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
18468
18509
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
18469
|
-
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18510
|
+
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18470
18511
|
context.report({
|
|
18471
18512
|
node: reportNode,
|
|
18472
18513
|
message: buildUnstableDepMessage(hookName, declaredKey)
|
|
@@ -18782,7 +18823,7 @@ const flattenCalleeName = (callee) => {
|
|
|
18782
18823
|
const PRAGMA = "React";
|
|
18783
18824
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
18784
18825
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
18785
|
-
if (getCalleeName$
|
|
18826
|
+
if (getCalleeName$1(node) !== expectedCall) return false;
|
|
18786
18827
|
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
18787
18828
|
const receiver = stripParenExpression(node.callee.object);
|
|
18788
18829
|
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
@@ -18980,10 +19021,14 @@ const hookUseState = defineRule({
|
|
|
18980
19021
|
create: (context) => {
|
|
18981
19022
|
const { allowDestructuredState } = resolveSettings$40(context.settings);
|
|
18982
19023
|
return { CallExpression(node) {
|
|
18983
|
-
|
|
19024
|
+
const callExpressionNode = node;
|
|
19025
|
+
if (!isReactFunctionCall(callExpressionNode, "useState")) return;
|
|
19026
|
+
const expressionRoot = findTransparentExpressionRoot(callExpressionNode);
|
|
19027
|
+
const expressionParent = expressionRoot.parent;
|
|
19028
|
+
if (isNodeOfType(expressionParent, "ReturnStatement")) return;
|
|
19029
|
+
if (isNodeOfType(expressionParent, "ArrowFunctionExpression") && expressionParent.body === expressionRoot) return;
|
|
18984
19030
|
const parent = node.parent;
|
|
18985
19031
|
if (!parent) return;
|
|
18986
|
-
if (isNodeOfType(parent, "ReturnStatement")) return;
|
|
18987
19032
|
if (!isNodeOfType(parent, "VariableDeclarator")) {
|
|
18988
19033
|
context.report({
|
|
18989
19034
|
node,
|
|
@@ -19091,8 +19136,8 @@ const hooksNoNanInDeps = defineRule({
|
|
|
19091
19136
|
severity: "warn",
|
|
19092
19137
|
recommendation: "Remove `NaN` (or `Number.NaN`) from the dependency array. If a value can be NaN at runtime, normalise it (`Number.isNaN(x) ? 0 : x`) before passing it.",
|
|
19093
19138
|
create: (context) => ({ CallExpression(node) {
|
|
19094
|
-
if (!
|
|
19095
|
-
const depsIndex =
|
|
19139
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEP_ARRAY, context.scopes)) return;
|
|
19140
|
+
const depsIndex = isReactHookCall(node, "useImperativeHandle", context.scopes) ? 2 : 1;
|
|
19096
19141
|
const depsArgument = node.arguments[depsIndex];
|
|
19097
19142
|
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
19098
19143
|
for (const element of depsArgument.elements) {
|
|
@@ -20263,7 +20308,7 @@ const isImportedSelectAtom = (callExpression) => {
|
|
|
20263
20308
|
const isDeferredCallbackPosition$1 = (functionNode) => {
|
|
20264
20309
|
const parent = functionNode.parent;
|
|
20265
20310
|
if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
|
|
20266
|
-
const hookName = getCalleeName$
|
|
20311
|
+
const hookName = getCalleeName$1(parent);
|
|
20267
20312
|
if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
|
|
20268
20313
|
if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
|
|
20269
20314
|
}
|
|
@@ -30334,7 +30379,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
|
|
|
30334
30379
|
"on",
|
|
30335
30380
|
"once"
|
|
30336
30381
|
]);
|
|
30337
|
-
const getCalleeName
|
|
30382
|
+
const getCalleeName = (callee) => {
|
|
30338
30383
|
if (!callee) return null;
|
|
30339
30384
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
30340
30385
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
@@ -30346,11 +30391,11 @@ const isDeferredCallbackPosition = (expression) => {
|
|
|
30346
30391
|
const parent = parentOf(expression);
|
|
30347
30392
|
if (!parent) return false;
|
|
30348
30393
|
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
30349
|
-
const name = getCalleeName
|
|
30394
|
+
const name = getCalleeName(parent.callee);
|
|
30350
30395
|
if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
|
|
30351
30396
|
}
|
|
30352
30397
|
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
30353
|
-
const name = getCalleeName
|
|
30398
|
+
const name = getCalleeName(parent.callee);
|
|
30354
30399
|
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
30355
30400
|
}
|
|
30356
30401
|
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
@@ -30770,14 +30815,6 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
30770
30815
|
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30771
30816
|
return false;
|
|
30772
30817
|
};
|
|
30773
|
-
const isUseEffect = (node) => {
|
|
30774
|
-
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
30775
|
-
const callee = node.callee;
|
|
30776
|
-
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
30777
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
30778
|
-
const receiver = stripParenExpression(callee.object);
|
|
30779
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
30780
|
-
};
|
|
30781
30818
|
const getEffectFn = (analysis, node) => {
|
|
30782
30819
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30783
30820
|
const fn = node.arguments?.[0];
|
|
@@ -32239,6 +32276,7 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32239
32276
|
sourceReferences,
|
|
32240
32277
|
isDeferred: frame.isDeferred,
|
|
32241
32278
|
isRenderKnownCopy,
|
|
32279
|
+
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
32242
32280
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
32243
32281
|
resetsSourceState: false
|
|
32244
32282
|
});
|
|
@@ -32254,27 +32292,71 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32254
32292
|
});
|
|
32255
32293
|
};
|
|
32256
32294
|
//#endregion
|
|
32295
|
+
//#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
|
|
32296
|
+
const DEFERRED_MEMBER_NAMES = new Set([
|
|
32297
|
+
"catch",
|
|
32298
|
+
"finally",
|
|
32299
|
+
"then"
|
|
32300
|
+
]);
|
|
32301
|
+
const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
|
|
32302
|
+
const effectFunction = getEffectFn(analysis, effectNode);
|
|
32303
|
+
if (!effectFunction) return false;
|
|
32304
|
+
if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
|
|
32305
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
|
|
32306
|
+
let didFindDeferredOrExternalWork = false;
|
|
32307
|
+
walkAst(effectFunction, (child) => {
|
|
32308
|
+
if (didFindDeferredOrExternalWork) return false;
|
|
32309
|
+
if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
32310
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
32311
|
+
const assignmentTarget = child.left;
|
|
32312
|
+
if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
|
|
32313
|
+
didFindDeferredOrExternalWork = true;
|
|
32314
|
+
return false;
|
|
32315
|
+
}
|
|
32316
|
+
}
|
|
32317
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32318
|
+
if (isSubscribeOrObserveCallExpression(child)) {
|
|
32319
|
+
didFindDeferredOrExternalWork = true;
|
|
32320
|
+
return false;
|
|
32321
|
+
}
|
|
32322
|
+
const localFunction = resolveExactLocalFunction(child.callee, scopes);
|
|
32323
|
+
if (isFunctionLike$1(localFunction) && localFunction.async) {
|
|
32324
|
+
didFindDeferredOrExternalWork = true;
|
|
32325
|
+
return false;
|
|
32326
|
+
}
|
|
32327
|
+
const callee = child.callee;
|
|
32328
|
+
if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
|
|
32329
|
+
didFindDeferredOrExternalWork = true;
|
|
32330
|
+
return false;
|
|
32331
|
+
}
|
|
32332
|
+
const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
32333
|
+
if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
|
|
32334
|
+
didFindDeferredOrExternalWork = true;
|
|
32335
|
+
return false;
|
|
32336
|
+
}
|
|
32337
|
+
});
|
|
32338
|
+
return didFindDeferredOrExternalWork;
|
|
32339
|
+
};
|
|
32340
|
+
//#endregion
|
|
32257
32341
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
32258
32342
|
const noAdjustStateOnPropChange = defineRule({
|
|
32259
32343
|
id: "no-adjust-state-on-prop-change",
|
|
32260
|
-
title: "State
|
|
32344
|
+
title: "State adjusted after a prop changes",
|
|
32261
32345
|
severity: "warn",
|
|
32262
32346
|
tags: ["test-noise"],
|
|
32263
|
-
recommendation: "
|
|
32347
|
+
recommendation: "Remove the adjustment effect by deriving values during render, resetting the component with a key, or updating related state in the event that changes the prop. Avoid tracking the previous prop in more state, which preserves the duplication. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
32264
32348
|
create: (context) => ({ CallExpression(node) {
|
|
32265
|
-
if (!
|
|
32266
|
-
allowGlobalReactNamespace: true,
|
|
32267
|
-
allowUnboundBareCalls: true,
|
|
32268
|
-
resolveConditionalAliases: true,
|
|
32269
|
-
resolveNamedAliases: true
|
|
32270
|
-
})) return;
|
|
32349
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
32271
32350
|
const analysis = getProgramAnalysis(node);
|
|
32272
32351
|
if (!analysis) return;
|
|
32273
32352
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
32274
32353
|
if (!dependencyReferences) return;
|
|
32275
32354
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
32276
|
-
|
|
32277
|
-
|
|
32355
|
+
const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
|
|
32356
|
+
if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
|
|
32357
|
+
for (const fact of facts) {
|
|
32358
|
+
if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
|
|
32359
|
+
if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
|
|
32278
32360
|
context.report({
|
|
32279
32361
|
node: fact.callExpression,
|
|
32280
32362
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
@@ -34287,7 +34369,7 @@ const declarationBodyContainsHookCall = (symbol) => {
|
|
|
34287
34369
|
walkAst(componentFunction, (descendant) => {
|
|
34288
34370
|
if (didFindHookCall) return false;
|
|
34289
34371
|
if (!isNodeOfType(descendant, "CallExpression")) return;
|
|
34290
|
-
const calleeName = getCalleeName$
|
|
34372
|
+
const calleeName = getCalleeName$1(descendant);
|
|
34291
34373
|
if (calleeName && isReactHookName(calleeName)) {
|
|
34292
34374
|
didFindHookCall = true;
|
|
34293
34375
|
return false;
|
|
@@ -34312,7 +34394,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
|
|
|
34312
34394
|
if (isNodeOfType(parent, "ArrowFunctionExpression")) {
|
|
34313
34395
|
if (parent.body !== current) return false;
|
|
34314
34396
|
const grandparent = parent.parent;
|
|
34315
|
-
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$
|
|
34397
|
+
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
|
|
34316
34398
|
}
|
|
34317
34399
|
if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
|
|
34318
34400
|
current = parent;
|
|
@@ -36844,12 +36926,7 @@ const noDerivedState = defineRule({
|
|
|
36844
36926
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36845
36927
|
} }).visitors,
|
|
36846
36928
|
CallExpression(node) {
|
|
36847
|
-
if (!
|
|
36848
|
-
allowGlobalReactNamespace: true,
|
|
36849
|
-
allowUnboundBareCalls: true,
|
|
36850
|
-
resolveConditionalAliases: true,
|
|
36851
|
-
resolveNamedAliases: true
|
|
36852
|
-
})) return;
|
|
36929
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
36853
36930
|
const analysis = getProgramAnalysis(node);
|
|
36854
36931
|
if (!analysis) return;
|
|
36855
36932
|
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
@@ -36869,12 +36946,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
36869
36946
|
tags: ["test-noise"],
|
|
36870
36947
|
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
36871
36948
|
create: (context) => ({ CallExpression(node) {
|
|
36872
|
-
if (!
|
|
36873
|
-
allowGlobalReactNamespace: true,
|
|
36874
|
-
allowUnboundBareCalls: true,
|
|
36875
|
-
resolveConditionalAliases: true,
|
|
36876
|
-
resolveNamedAliases: true
|
|
36877
|
-
})) return;
|
|
36949
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
36878
36950
|
const analysis = getProgramAnalysis(node);
|
|
36879
36951
|
if (!analysis) return;
|
|
36880
36952
|
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
@@ -36997,7 +37069,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
|
|
|
36997
37069
|
if (isFunctionLike$1(cursor)) {
|
|
36998
37070
|
const parent = cursor.parent ?? null;
|
|
36999
37071
|
if (parent && isNodeOfType(parent, "CallExpression")) {
|
|
37000
|
-
const calleeName = getCalleeName$
|
|
37072
|
+
const calleeName = getCalleeName$1(parent);
|
|
37001
37073
|
if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
|
|
37002
37074
|
}
|
|
37003
37075
|
}
|
|
@@ -37068,7 +37140,7 @@ const isNonHandlerHookCallback = (functionNode) => {
|
|
|
37068
37140
|
const parent = functionNode.parent ?? null;
|
|
37069
37141
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
37070
37142
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
37071
|
-
const calleeName = getCalleeName$
|
|
37143
|
+
const calleeName = getCalleeName$1(parent);
|
|
37072
37144
|
return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
|
|
37073
37145
|
};
|
|
37074
37146
|
const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
@@ -37150,7 +37222,7 @@ const noDerivedUseState = defineRule({
|
|
|
37150
37222
|
return {
|
|
37151
37223
|
...propStackTracker.visitors,
|
|
37152
37224
|
CallExpression(node) {
|
|
37153
|
-
if (!
|
|
37225
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
37154
37226
|
const seed = unwrapInitializerSeed(node.arguments[0]);
|
|
37155
37227
|
const reportStalePropCopy = (propName) => {
|
|
37156
37228
|
if (isIntentionalSnapshotState(node)) return;
|
|
@@ -37839,12 +37911,7 @@ const collectUseStateBindings = (componentBody, scopes) => {
|
|
|
37839
37911
|
const setterElement = elements[1];
|
|
37840
37912
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37841
37913
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37842
|
-
if (!(
|
|
37843
|
-
allowGlobalReactNamespace: true,
|
|
37844
|
-
allowUnboundBareCalls: true,
|
|
37845
|
-
resolveConditionalAliases: true,
|
|
37846
|
-
resolveNamedAliases: true
|
|
37847
|
-
}) : isHookCall$2(declarator.init, "useState"))) continue;
|
|
37914
|
+
if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
|
|
37848
37915
|
bindings.push({
|
|
37849
37916
|
valueName: valueElement.name,
|
|
37850
37917
|
setterName: setterElement.name,
|
|
@@ -38011,7 +38078,7 @@ const noDirectStateMutation = defineRule({
|
|
|
38011
38078
|
create: (context) => {
|
|
38012
38079
|
const checkComponent = (componentBody) => {
|
|
38013
38080
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38014
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
38081
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
38015
38082
|
if (bindings.length === 0) return;
|
|
38016
38083
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
38017
38084
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
@@ -38578,12 +38645,7 @@ const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
|
38578
38645
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38579
38646
|
const expression = unwrapDiscardedExpression(statement);
|
|
38580
38647
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
38581
|
-
if (!
|
|
38582
|
-
allowGlobalReactNamespace: true,
|
|
38583
|
-
allowUnboundBareCalls: true,
|
|
38584
|
-
resolveConditionalAliases: true,
|
|
38585
|
-
resolveNamedAliases: true
|
|
38586
|
-
})) continue;
|
|
38648
|
+
if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
|
|
38587
38649
|
effectCalls.push(expression);
|
|
38588
38650
|
}
|
|
38589
38651
|
return effectCalls;
|
|
@@ -38827,7 +38889,7 @@ const collectStorageHookSetterNames = (componentBody) => {
|
|
|
38827
38889
|
for (const declarator of statement.declarations ?? []) {
|
|
38828
38890
|
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
38829
38891
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
38830
|
-
const calleeName = getCalleeName$
|
|
38892
|
+
const calleeName = getCalleeName$1(declarator.init);
|
|
38831
38893
|
if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
|
|
38832
38894
|
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
|
|
38833
38895
|
}
|
|
@@ -39315,7 +39377,7 @@ const noEffectEventHandler = defineRule({
|
|
|
39315
39377
|
return {
|
|
39316
39378
|
...propStackTracker.visitors,
|
|
39317
39379
|
CallExpression(node) {
|
|
39318
|
-
if (!
|
|
39380
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
39319
39381
|
const callback = getEffectCallback(node);
|
|
39320
39382
|
if (!callback) return;
|
|
39321
39383
|
const analysis = getProgramAnalysis(node);
|
|
@@ -39435,14 +39497,14 @@ const noEffectEventInDeps = defineRule({
|
|
|
39435
39497
|
if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
|
|
39436
39498
|
const initializer = declaratorNode.init;
|
|
39437
39499
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
39438
|
-
if (!
|
|
39500
|
+
if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
|
|
39439
39501
|
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
39440
39502
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
39441
39503
|
} });
|
|
39442
39504
|
return {
|
|
39443
39505
|
...componentBindings.visitors,
|
|
39444
39506
|
CallExpression(node) {
|
|
39445
|
-
if (!
|
|
39507
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
39446
39508
|
if (!componentBindings.isInsideComponent()) return;
|
|
39447
39509
|
const depsNode = node.arguments[1];
|
|
39448
39510
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -39499,7 +39561,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
39499
39561
|
node: finding.reportNode,
|
|
39500
39562
|
message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
|
|
39501
39563
|
});
|
|
39502
|
-
if (!
|
|
39564
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
39503
39565
|
const args = node.arguments ?? [];
|
|
39504
39566
|
if (args.length < 2) return;
|
|
39505
39567
|
const depsNode = args[1];
|
|
@@ -39760,7 +39822,7 @@ const noEventHandler = defineRule({
|
|
|
39760
39822
|
severity: "warn",
|
|
39761
39823
|
recommendation: "Run the side effect in the event handler that triggers it, instead of watching its state from a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers",
|
|
39762
39824
|
create: (context) => ({ CallExpression(node) {
|
|
39763
|
-
if (!
|
|
39825
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
39764
39826
|
const analysis = getProgramAnalysis(node);
|
|
39765
39827
|
if (!analysis || hasCleanup(analysis, node)) return;
|
|
39766
39828
|
const frames = collectBoundedEffectExecutionFrames(analysis, node);
|
|
@@ -40222,25 +40284,25 @@ const addDeclarationBindings = (statement, scope) => {
|
|
|
40222
40284
|
}
|
|
40223
40285
|
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
|
|
40224
40286
|
};
|
|
40225
|
-
const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40287
|
+
const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40226
40288
|
let hasReturn = false;
|
|
40227
|
-
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
|
|
40289
|
+
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, scopes, eventHandlerReferenceNames)) hasReturn = true;
|
|
40228
40290
|
else addDeclarationBindings(statement, scope);
|
|
40229
40291
|
return hasReturn;
|
|
40230
40292
|
};
|
|
40231
|
-
const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
|
|
40293
|
+
const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
|
|
40232
40294
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
40233
40295
|
if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
|
|
40234
40296
|
return true;
|
|
40235
40297
|
}
|
|
40236
|
-
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) && !
|
|
40298
|
+
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && (isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) || isReactHookCall(statement.expression, BUILTIN_HOOK_NAMES, scopes)) && !isReactHookCall(statement.expression, EFFECT_HOOK_NAMES$1, scopes)) {
|
|
40237
40299
|
for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
|
|
40238
40300
|
return false;
|
|
40239
40301
|
}
|
|
40240
|
-
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
|
|
40302
|
+
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), scopes, eventHandlerReferenceNames);
|
|
40241
40303
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
40242
|
-
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
|
|
40243
|
-
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
|
|
40304
|
+
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, scopes, eventHandlerReferenceNames);
|
|
40305
|
+
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
40244
40306
|
if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
40245
40307
|
return consequentHasReturn || alternateHasReturn;
|
|
40246
40308
|
}
|
|
@@ -40248,7 +40310,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
40248
40310
|
let hasReturn = false;
|
|
40249
40311
|
for (const switchCase of statement.cases ?? []) {
|
|
40250
40312
|
const caseScope = createBlockBindingScope(scope);
|
|
40251
|
-
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
|
|
40313
|
+
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
|
|
40252
40314
|
hasReturn = true;
|
|
40253
40315
|
if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
|
|
40254
40316
|
}
|
|
@@ -40256,25 +40318,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
40256
40318
|
return hasReturn;
|
|
40257
40319
|
}
|
|
40258
40320
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
40259
|
-
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
|
|
40260
|
-
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
|
|
40261
|
-
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
|
|
40321
|
+
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, scopes, eventHandlerReferenceNames);
|
|
40322
|
+
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
40323
|
+
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, scopes, eventHandlerReferenceNames) : false;
|
|
40262
40324
|
return blockHasReturn || handlerHasReturn || finalizerHasReturn;
|
|
40263
40325
|
}
|
|
40264
40326
|
if (isNodeOfType(statement, "CatchClause")) {
|
|
40265
40327
|
const catchScope = createBlockBindingScope(scope);
|
|
40266
40328
|
addPatternBindings(statement.param, catchScope);
|
|
40267
|
-
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
|
|
40329
|
+
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
|
|
40268
40330
|
}
|
|
40269
40331
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
40270
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40332
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
40271
40333
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
40272
40334
|
return bodyHasReturn;
|
|
40273
40335
|
}
|
|
40274
40336
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
40275
40337
|
const loopScope = createBlockBindingScope(scope);
|
|
40276
40338
|
if (statement.init) addDeclarationBindings(statement.init, loopScope);
|
|
40277
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40339
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
40278
40340
|
if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
|
|
40279
40341
|
if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
|
|
40280
40342
|
if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
|
|
@@ -40284,22 +40346,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
|
|
|
40284
40346
|
const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
|
|
40285
40347
|
const loopScope = createBlockBindingScope(scope);
|
|
40286
40348
|
if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
|
|
40287
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40349
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
|
|
40288
40350
|
addNames(names, rightNames);
|
|
40289
40351
|
return true;
|
|
40290
40352
|
}
|
|
40291
|
-
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40353
|
+
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
40292
40354
|
if (isNodeOfType(statement, "WithStatement")) {
|
|
40293
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40355
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
|
|
40294
40356
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
|
|
40295
40357
|
return bodyHasReturn;
|
|
40296
40358
|
}
|
|
40297
40359
|
return false;
|
|
40298
40360
|
};
|
|
40299
|
-
const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40361
|
+
const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40300
40362
|
const names = /* @__PURE__ */ new Set();
|
|
40301
40363
|
if (!isNodeOfType(componentBody, "BlockStatement")) return names;
|
|
40302
|
-
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
|
|
40364
|
+
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
|
|
40303
40365
|
return names;
|
|
40304
40366
|
};
|
|
40305
40367
|
//#endregion
|
|
@@ -40322,42 +40384,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
|
|
|
40322
40384
|
};
|
|
40323
40385
|
//#endregion
|
|
40324
40386
|
//#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
|
|
40325
|
-
const
|
|
40326
|
-
|
|
40327
|
-
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
40328
|
-
if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
|
|
40329
|
-
return null;
|
|
40330
|
-
};
|
|
40331
|
-
const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
|
|
40332
|
-
if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
|
|
40387
|
+
const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
|
|
40388
|
+
if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
|
|
40333
40389
|
if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
|
|
40334
40390
|
const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
|
|
40335
40391
|
return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
|
|
40336
40392
|
};
|
|
40337
|
-
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
|
|
40393
|
+
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
|
|
40338
40394
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
40339
40395
|
for (const property of node.properties ?? []) {
|
|
40340
40396
|
if (!isNodeOfType(property, "Property")) continue;
|
|
40341
40397
|
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
40342
40398
|
if (!propertyName) continue;
|
|
40343
|
-
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
40399
|
+
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
|
|
40344
40400
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
40345
40401
|
}
|
|
40346
40402
|
};
|
|
40347
|
-
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40403
|
+
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
40348
40404
|
if (!isNodeOfType(statement, "VariableDeclaration")) return;
|
|
40349
40405
|
const declarationScope = getVariableDeclarationScope(statement, scope);
|
|
40350
40406
|
for (const declarator of statement.declarations ?? []) {
|
|
40351
40407
|
const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
|
|
40352
40408
|
if (!declarator.init) continue;
|
|
40353
|
-
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
|
|
40409
|
+
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
|
|
40354
40410
|
for (const declaredBindingName of declaredBindingNames) {
|
|
40355
40411
|
if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
|
|
40356
|
-
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
|
|
40412
|
+
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
|
|
40357
40413
|
}
|
|
40358
40414
|
}
|
|
40359
40415
|
};
|
|
40360
|
-
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40416
|
+
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
|
|
40361
40417
|
if (isNodeOfType(statement, "FunctionDeclaration")) {
|
|
40362
40418
|
if (statement.id) {
|
|
40363
40419
|
const declaredBindingNames = addPatternBindings(statement.id, scope);
|
|
@@ -40366,61 +40422,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
|
|
|
40366
40422
|
return;
|
|
40367
40423
|
}
|
|
40368
40424
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
40369
|
-
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40425
|
+
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
40370
40426
|
return;
|
|
40371
40427
|
}
|
|
40372
40428
|
if (isNodeOfType(statement, "BlockStatement")) {
|
|
40373
|
-
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40429
|
+
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
40374
40430
|
return;
|
|
40375
40431
|
}
|
|
40376
40432
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
40377
|
-
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
|
|
40378
|
-
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
|
|
40433
|
+
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope, scopes);
|
|
40434
|
+
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope, scopes);
|
|
40379
40435
|
return;
|
|
40380
40436
|
}
|
|
40381
40437
|
if (isNodeOfType(statement, "SwitchStatement")) {
|
|
40382
|
-
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40438
|
+
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
|
|
40383
40439
|
return;
|
|
40384
40440
|
}
|
|
40385
40441
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
40386
|
-
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
|
|
40442
|
+
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
|
|
40387
40443
|
if (statement.handler) {
|
|
40388
40444
|
const catchScope = createBlockBindingScope(scope);
|
|
40389
40445
|
addPatternBindings(statement.handler.param, catchScope);
|
|
40390
|
-
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
|
|
40446
|
+
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
|
|
40391
40447
|
}
|
|
40392
|
-
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
|
|
40448
|
+
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
|
|
40393
40449
|
return;
|
|
40394
40450
|
}
|
|
40395
40451
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
40396
40452
|
const loopScope = createBlockBindingScope(scope);
|
|
40397
|
-
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
|
|
40398
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40453
|
+
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope, scopes);
|
|
40454
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
40399
40455
|
return;
|
|
40400
40456
|
}
|
|
40401
40457
|
if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
|
|
40402
40458
|
const loopScope = createBlockBindingScope(scope);
|
|
40403
|
-
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
|
|
40459
|
+
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
|
|
40404
40460
|
else addPatternBindings(statement.left, loopScope);
|
|
40405
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40461
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
|
|
40406
40462
|
return;
|
|
40407
40463
|
}
|
|
40408
40464
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
40409
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40465
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
40410
40466
|
return;
|
|
40411
40467
|
}
|
|
40412
|
-
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40468
|
+
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
|
|
40413
40469
|
};
|
|
40414
|
-
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
|
|
40415
|
-
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40470
|
+
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope, scopes) => {
|
|
40471
|
+
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope, scopes);
|
|
40416
40472
|
};
|
|
40417
|
-
const collectFunctionLikeLocalNames = (componentBody) => {
|
|
40473
|
+
const collectFunctionLikeLocalNames = (componentBody, scopes) => {
|
|
40418
40474
|
const functionLikeLocalNames = /* @__PURE__ */ new Set();
|
|
40419
40475
|
if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
|
|
40420
40476
|
let previousSize = -1;
|
|
40421
40477
|
while (previousSize !== functionLikeLocalNames.size) {
|
|
40422
40478
|
previousSize = functionLikeLocalNames.size;
|
|
40423
|
-
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
|
|
40479
|
+
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
|
|
40424
40480
|
}
|
|
40425
40481
|
return functionLikeLocalNames;
|
|
40426
40482
|
};
|
|
@@ -40460,17 +40516,17 @@ const noEventTriggerState = defineRule({
|
|
|
40460
40516
|
create: (context) => {
|
|
40461
40517
|
const checkComponent = (componentBody) => {
|
|
40462
40518
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
40463
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
40519
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
40464
40520
|
if (useStateBindings.length === 0) return;
|
|
40465
40521
|
const analysis = getProgramAnalysis(componentBody);
|
|
40466
40522
|
if (!analysis) return;
|
|
40467
40523
|
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
40468
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
40524
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
40469
40525
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
40470
|
-
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
40526
|
+
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
|
|
40471
40527
|
walkAst(componentBody, (effectCall) => {
|
|
40472
40528
|
if (!isNodeOfType(effectCall, "CallExpression")) return;
|
|
40473
|
-
if (!
|
|
40529
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
|
|
40474
40530
|
if ((effectCall.arguments?.length ?? 0) < 2) return;
|
|
40475
40531
|
const depsNode = effectCall.arguments[1];
|
|
40476
40532
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -42299,7 +42355,7 @@ const noInitializeState = defineRule({
|
|
|
42299
42355
|
tags: ["test-noise"],
|
|
42300
42356
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
42301
42357
|
create: (context) => ({ CallExpression(node) {
|
|
42302
|
-
if (!
|
|
42358
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
42303
42359
|
const dependencies = node.arguments?.[1];
|
|
42304
42360
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
42305
42361
|
const analysis = getProgramAnalysis(node);
|
|
@@ -43911,7 +43967,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43911
43967
|
const setterElement = elements[1];
|
|
43912
43968
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43913
43969
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43914
|
-
if (!
|
|
43970
|
+
if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
|
|
43915
43971
|
const initializer = declarator.init.arguments?.[0];
|
|
43916
43972
|
if (!initializer) continue;
|
|
43917
43973
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43929,7 +43985,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43929
43985
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43930
43986
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43931
43987
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43932
|
-
if (!
|
|
43988
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
43933
43989
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43934
43990
|
const depsNode = effectCall.arguments[1];
|
|
43935
43991
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -44338,7 +44394,7 @@ const noMultiComp = defineRule({
|
|
|
44338
44394
|
});
|
|
44339
44395
|
//#endregion
|
|
44340
44396
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
44341
|
-
const collectUseRefBindingNames = (componentBody) => {
|
|
44397
|
+
const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
44342
44398
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
44343
44399
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
44344
44400
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -44346,7 +44402,7 @@ const collectUseRefBindingNames = (componentBody) => {
|
|
|
44346
44402
|
for (const declarator of statement.declarations ?? []) {
|
|
44347
44403
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
44348
44404
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
44349
|
-
if (!
|
|
44405
|
+
if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
|
|
44350
44406
|
useRefBindings.add(declarator.id.name);
|
|
44351
44407
|
}
|
|
44352
44408
|
}
|
|
@@ -44381,12 +44437,12 @@ const noMutableInDeps = defineRule({
|
|
|
44381
44437
|
create: (context) => {
|
|
44382
44438
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
44383
44439
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
44384
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44440
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
|
|
44385
44441
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
44386
44442
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
44387
44443
|
walkAst(componentBody, (child) => {
|
|
44388
44444
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
44389
|
-
if (!
|
|
44445
|
+
if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
|
|
44390
44446
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
44391
44447
|
const depsNode = child.arguments[1];
|
|
44392
44448
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -46299,16 +46355,10 @@ const noPassDataToParent = defineRule({
|
|
|
46299
46355
|
tags: ["test-noise"],
|
|
46300
46356
|
recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
|
|
46301
46357
|
create: (context) => {
|
|
46302
|
-
const isReactUseRefCall = (node) =>
|
|
46303
|
-
|
|
46304
|
-
allowUnboundBareCalls: true
|
|
46305
|
-
});
|
|
46306
|
-
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
46307
|
-
allowGlobalReactNamespace: true,
|
|
46308
|
-
allowUnboundBareCalls: true
|
|
46309
|
-
});
|
|
46358
|
+
const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
|
|
46359
|
+
const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
|
|
46310
46360
|
return { CallExpression(node) {
|
|
46311
|
-
if (!
|
|
46361
|
+
if (!isReactUseEffectCall(node)) return;
|
|
46312
46362
|
const analysis = getProgramAnalysis(node);
|
|
46313
46363
|
if (!analysis) return;
|
|
46314
46364
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -46595,7 +46645,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
46595
46645
|
tags: ["test-noise"],
|
|
46596
46646
|
recommendation: "Move the state up to the parent (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes",
|
|
46597
46647
|
create: (context) => ({ CallExpression(node) {
|
|
46598
|
-
if (!
|
|
46648
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
46599
46649
|
const analysis = getProgramAnalysis(node);
|
|
46600
46650
|
if (!analysis) return;
|
|
46601
46651
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -47010,7 +47060,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
47010
47060
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
47011
47061
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
47012
47062
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
47013
|
-
const calleeName = getCalleeName$
|
|
47063
|
+
const calleeName = getCalleeName$1(binding.initializer);
|
|
47014
47064
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
47015
47065
|
}
|
|
47016
47066
|
return false;
|
|
@@ -47032,7 +47082,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
47032
47082
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
47033
47083
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
47034
47084
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
47035
|
-
if (getCalleeName$
|
|
47085
|
+
if (getCalleeName$1(binding.initializer) !== "useRef") return null;
|
|
47036
47086
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
47037
47087
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
47038
47088
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -47064,7 +47114,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
47064
47114
|
return {
|
|
47065
47115
|
...propStackTracker.visitors,
|
|
47066
47116
|
CallExpression(node) {
|
|
47067
|
-
if (!
|
|
47117
|
+
if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
|
|
47068
47118
|
const callback = getEffectCallback(node);
|
|
47069
47119
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
47070
47120
|
const depsNode = node.arguments[1];
|
|
@@ -48957,7 +49007,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
48957
49007
|
tags: ["test-noise"],
|
|
48958
49008
|
recommendation: "Pass the prop as `key` so React resets the component for you when the prop changes, instead of clearing every state value by hand in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes",
|
|
48959
49009
|
create: (context) => ({ CallExpression(node) {
|
|
48960
|
-
if (!
|
|
49010
|
+
if (!isReactHookCall(node, "useEffect", context.scopes)) return;
|
|
48961
49011
|
const analysis = getProgramAnalysis(node);
|
|
48962
49012
|
if (!analysis) return;
|
|
48963
49013
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -49226,7 +49276,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
49226
49276
|
if (node.callee.property.name !== "handler") return false;
|
|
49227
49277
|
let currentNode = node.callee.object;
|
|
49228
49278
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49229
|
-
const calleeName = getCalleeName$
|
|
49279
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
49230
49280
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
49231
49281
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
49232
49282
|
currentNode = currentNode.callee.object;
|
|
@@ -49727,7 +49777,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49727
49777
|
create: (context) => {
|
|
49728
49778
|
const checkFunctionScope = (functionBody) => {
|
|
49729
49779
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
49730
|
-
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49780
|
+
const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
|
|
49731
49781
|
if (useStateBindings.length === 0) return;
|
|
49732
49782
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
49733
49783
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -49736,7 +49786,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49736
49786
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
49737
49787
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
49738
49788
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
49739
|
-
if (!
|
|
49789
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
49740
49790
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
49741
49791
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
49742
49792
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -49822,7 +49872,7 @@ const noSetStateInRender = defineRule({
|
|
|
49822
49872
|
create: (context) => {
|
|
49823
49873
|
const checkComponent = (componentBody) => {
|
|
49824
49874
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
49825
|
-
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49875
|
+
const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
|
|
49826
49876
|
if (setterNames.size === 0) return;
|
|
49827
49877
|
for (const statement of componentBody.body ?? []) {
|
|
49828
49878
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -50071,10 +50121,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
50071
50121
|
});
|
|
50072
50122
|
return facts;
|
|
50073
50123
|
};
|
|
50074
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
50124
|
+
const isEffectCallbackFunction = (functionNode, scopes) => {
|
|
50075
50125
|
const parent = functionNode.parent;
|
|
50076
50126
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
50077
|
-
return
|
|
50127
|
+
return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
|
|
50078
50128
|
};
|
|
50079
50129
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
50080
50130
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -50093,24 +50143,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
50093
50143
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
50094
50144
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
50095
50145
|
};
|
|
50096
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50146
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
|
|
50097
50147
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
50098
50148
|
if (cleanupBindingName === null) return false;
|
|
50099
50149
|
let isReturnedFromEffect = false;
|
|
50100
50150
|
walkAst(ownerScope, (child) => {
|
|
50101
50151
|
if (isReturnedFromEffect) return false;
|
|
50102
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50152
|
+
if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
50103
50153
|
const effectCallback = getEffectCallback(child);
|
|
50104
50154
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
50105
50155
|
});
|
|
50106
50156
|
return isReturnedFromEffect;
|
|
50107
50157
|
};
|
|
50108
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50158
|
+
const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
|
|
50109
50159
|
let functionNode = findEnclosingFunction$1(node);
|
|
50110
50160
|
while (functionNode) {
|
|
50111
50161
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
50112
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50113
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
50162
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50163
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
|
|
50114
50164
|
functionNode = outerFunction;
|
|
50115
50165
|
}
|
|
50116
50166
|
return false;
|
|
@@ -50146,10 +50196,10 @@ const noStaleTimerRef = defineRule({
|
|
|
50146
50196
|
if (isShadowedTimerGlobal(node)) return;
|
|
50147
50197
|
const { clearCalleeName, refName } = clearCall;
|
|
50148
50198
|
const refBinding = findVariableInitializer(node, refName);
|
|
50149
|
-
if (!refBinding?.initializer || !
|
|
50199
|
+
if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
|
|
50150
50200
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
50151
50201
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
50152
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50202
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
|
|
50153
50203
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
50154
50204
|
context.report({
|
|
50155
50205
|
node,
|
|
@@ -55953,7 +56003,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
55953
56003
|
allReadsAreInSubHandlers = false;
|
|
55954
56004
|
return;
|
|
55955
56005
|
}
|
|
55956
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
56006
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
|
|
55957
56007
|
});
|
|
55958
56008
|
return {
|
|
55959
56009
|
hasAnyRead,
|
|
@@ -55976,7 +56026,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
55976
56026
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
55977
56027
|
const effectCall = statement.expression;
|
|
55978
56028
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
55979
|
-
if (!
|
|
56029
|
+
if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
|
|
55980
56030
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
55981
56031
|
const depsNode = effectCall.arguments[1];
|
|
55982
56032
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -56008,11 +56058,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
56008
56058
|
});
|
|
56009
56059
|
//#endregion
|
|
56010
56060
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
56011
|
-
const findUseEffectsInComponent = (componentBody) => {
|
|
56061
|
+
const findUseEffectsInComponent = (componentBody, scopes) => {
|
|
56012
56062
|
const effectCalls = [];
|
|
56013
56063
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
56014
56064
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
56015
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56065
|
+
if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
|
|
56016
56066
|
});
|
|
56017
56067
|
return effectCalls;
|
|
56018
56068
|
};
|
|
@@ -56215,7 +56265,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56215
56265
|
};
|
|
56216
56266
|
const checkComponent = (componentBody) => {
|
|
56217
56267
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
56218
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56268
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
56219
56269
|
if (useStateBindings.length === 0) return;
|
|
56220
56270
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
56221
56271
|
for (const binding of useStateBindings) {
|
|
@@ -56228,7 +56278,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56228
56278
|
}
|
|
56229
56279
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
56230
56280
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
56231
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56281
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
56232
56282
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
56233
56283
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
56234
56284
|
const depsNode = effectCall.arguments[1];
|
|
@@ -56270,7 +56320,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56270
56320
|
})).filter((candidate) => candidate.storeName !== null);
|
|
56271
56321
|
if (snapshotBindings.length === 0) return;
|
|
56272
56322
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
56273
|
-
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56323
|
+
for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
|
|
56274
56324
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
56275
56325
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
56276
56326
|
const depsNode = effectCall.arguments[1];
|
|
@@ -56367,7 +56417,7 @@ const preferUseReducer = defineRule({
|
|
|
56367
56417
|
create: (context) => {
|
|
56368
56418
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
56369
56419
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
56370
|
-
const bindings = collectUseStateBindings(body);
|
|
56420
|
+
const bindings = collectUseStateBindings(body, context.scopes);
|
|
56371
56421
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
56372
56422
|
if (setterNames.size < 5) return;
|
|
56373
56423
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -56583,7 +56633,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
56583
56633
|
]);
|
|
56584
56634
|
const isQueryCacheSourceCall = (initializer) => {
|
|
56585
56635
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
56586
|
-
const hookName = getCalleeName$
|
|
56636
|
+
const hookName = getCalleeName$1(initializer);
|
|
56587
56637
|
if (!hookName) return false;
|
|
56588
56638
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
56589
56639
|
};
|
|
@@ -56715,7 +56765,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
56715
56765
|
},
|
|
56716
56766
|
CallExpression(node) {
|
|
56717
56767
|
if (!hasQueryReadUsage) {
|
|
56718
|
-
const callName = getCalleeName$
|
|
56768
|
+
const callName = getCalleeName$1(node);
|
|
56719
56769
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
56720
56770
|
}
|
|
56721
56771
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -58378,6 +58428,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
58378
58428
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
58379
58429
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
58380
58430
|
const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
|
|
58431
|
+
const FILE_READER_READ_METHOD_NAMES = new Set([
|
|
58432
|
+
"readAsArrayBuffer",
|
|
58433
|
+
"readAsBinaryString",
|
|
58434
|
+
"readAsDataURL",
|
|
58435
|
+
"readAsText"
|
|
58436
|
+
]);
|
|
58437
|
+
const isGlobalFileReaderConstruction = (expression, context) => {
|
|
58438
|
+
if (!expression) return false;
|
|
58439
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58440
|
+
if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
58441
|
+
return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
|
|
58442
|
+
};
|
|
58443
|
+
const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
|
|
58444
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58445
|
+
let latestValue = null;
|
|
58446
|
+
let latestStart = null;
|
|
58447
|
+
if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
|
|
58448
|
+
latestValue = readerSymbol.initializer;
|
|
58449
|
+
latestStart = readerSymbol.declarationNode.range[0];
|
|
58450
|
+
}
|
|
58451
|
+
for (const reference of readerSymbol.references) {
|
|
58452
|
+
if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
|
|
58453
|
+
const assignment = reference.identifier.parent;
|
|
58454
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
|
|
58455
|
+
latestValue = assignment.right;
|
|
58456
|
+
latestStart = reference.identifier.range[0];
|
|
58457
|
+
}
|
|
58458
|
+
return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
|
|
58459
|
+
};
|
|
58460
|
+
const resolveLoadingCompletionFunction = (expression, context) => {
|
|
58461
|
+
const directFunction = resolveExactLocalFunction(expression, context.scopes);
|
|
58462
|
+
if (directFunction) return directFunction;
|
|
58463
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58464
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
58465
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
58466
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
58467
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
|
|
58468
|
+
const callback = initializer.arguments?.[0];
|
|
58469
|
+
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
58470
|
+
};
|
|
58471
|
+
const isSetterBooleanCall = (node, setterSymbol, value, context) => {
|
|
58472
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
58473
|
+
const callee = stripParenExpression(node.callee);
|
|
58474
|
+
const argument = node.arguments?.[0];
|
|
58475
|
+
const unwrappedArgument = argument ? stripParenExpression(argument) : null;
|
|
58476
|
+
return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
|
|
58477
|
+
};
|
|
58478
|
+
const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
|
|
58479
|
+
if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
|
|
58480
|
+
visitedFunctions.add(functionNode);
|
|
58481
|
+
let didClearLoadingState = false;
|
|
58482
|
+
walkAst(functionNode.body, (child) => {
|
|
58483
|
+
if (didClearLoadingState) return false;
|
|
58484
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
58485
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58486
|
+
if (isSetterBooleanCall(child, setterSymbol, false, context)) {
|
|
58487
|
+
didClearLoadingState = true;
|
|
58488
|
+
return false;
|
|
58489
|
+
}
|
|
58490
|
+
const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
|
|
58491
|
+
if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
|
|
58492
|
+
didClearLoadingState = true;
|
|
58493
|
+
return false;
|
|
58494
|
+
}
|
|
58495
|
+
});
|
|
58496
|
+
return didClearLoadingState;
|
|
58497
|
+
};
|
|
58498
|
+
const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
|
|
58499
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58500
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return null;
|
|
58501
|
+
let callback = null;
|
|
58502
|
+
let callbackStart = originStart;
|
|
58503
|
+
walkAst(readFunction.body, (child) => {
|
|
58504
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58505
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=" || child.range[0] >= readCall.range[0] || child.range[0] <= callbackStart || !isNodeOfType(child.left, "MemberExpression") || getStaticPropertyName(child.left) !== propertyName) return;
|
|
58506
|
+
const receiver = stripParenExpression(child.left.object);
|
|
58507
|
+
if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
|
|
58508
|
+
callback = child.right;
|
|
58509
|
+
callbackStart = child.range[0];
|
|
58510
|
+
});
|
|
58511
|
+
return callback;
|
|
58512
|
+
};
|
|
58513
|
+
const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
|
|
58514
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58515
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return false;
|
|
58516
|
+
let didStartLoading = false;
|
|
58517
|
+
walkAst(readFunction.body, (child) => {
|
|
58518
|
+
if (didStartLoading) return false;
|
|
58519
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58520
|
+
if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
|
|
58521
|
+
if (isSetterBooleanCall(child, setterSymbol, true, context)) {
|
|
58522
|
+
didStartLoading = true;
|
|
58523
|
+
return false;
|
|
58524
|
+
}
|
|
58525
|
+
});
|
|
58526
|
+
return didStartLoading;
|
|
58527
|
+
};
|
|
58528
|
+
const setterTracksFileReader = (functionBody, setterSymbol, context) => {
|
|
58529
|
+
let didFindFileReaderLifecycle = false;
|
|
58530
|
+
walkAst(functionBody, (child) => {
|
|
58531
|
+
if (didFindFileReaderLifecycle) return false;
|
|
58532
|
+
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
|
|
58533
|
+
const receiver = stripParenExpression(child.callee.object);
|
|
58534
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
58535
|
+
const readerSymbol = context.scopes.symbolFor(receiver);
|
|
58536
|
+
if (!readerSymbol) return;
|
|
58537
|
+
const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
|
|
58538
|
+
if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
|
|
58539
|
+
const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
|
|
58540
|
+
const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
|
|
58541
|
+
const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
|
|
58542
|
+
const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
|
|
58543
|
+
if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
|
|
58544
|
+
didFindFileReaderLifecycle = true;
|
|
58545
|
+
return false;
|
|
58546
|
+
}
|
|
58547
|
+
});
|
|
58548
|
+
return didFindFileReaderLifecycle;
|
|
58549
|
+
};
|
|
58381
58550
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
58382
58551
|
let found = false;
|
|
58383
58552
|
walkAst(fnBody, (child) => {
|
|
@@ -58551,6 +58720,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58551
58720
|
const fnBody = enclosingFunctionBody(node);
|
|
58552
58721
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
58553
58722
|
if (fnBody && setterName) {
|
|
58723
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58724
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
58554
58725
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
58555
58726
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
58556
58727
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -58882,7 +59053,7 @@ const rerenderDependencies = defineRule({
|
|
|
58882
59053
|
severity: "error",
|
|
58883
59054
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
58884
59055
|
create: (context) => ({ CallExpression(node) {
|
|
58885
|
-
if (!
|
|
59056
|
+
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
58886
59057
|
const depsNode = node.arguments[1];
|
|
58887
59058
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
58888
59059
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -59137,7 +59308,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
59137
59308
|
category: "Performance",
|
|
59138
59309
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
59139
59310
|
create: (context) => ({ CallExpression(node) {
|
|
59140
|
-
if (!
|
|
59311
|
+
if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
|
|
59141
59312
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
59142
59313
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
59143
59314
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59201,7 +59372,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
59201
59372
|
category: "Performance",
|
|
59202
59373
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
59203
59374
|
create: (context) => ({ CallExpression(node) {
|
|
59204
|
-
if (!
|
|
59375
|
+
if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
|
|
59205
59376
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
59206
59377
|
if (!initializer) return;
|
|
59207
59378
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59964,11 +60135,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
59964
60135
|
}
|
|
59965
60136
|
return false;
|
|
59966
60137
|
};
|
|
59967
|
-
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60138
|
+
const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
59968
60139
|
const effectInfos = [];
|
|
59969
60140
|
walkAst(componentBody, (child) => {
|
|
59970
60141
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
59971
|
-
if (!
|
|
60142
|
+
if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
|
|
59972
60143
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
59973
60144
|
for (const argument of child.arguments ?? []) {
|
|
59974
60145
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -60024,13 +60195,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
|
60024
60195
|
});
|
|
60025
60196
|
return effectInfos;
|
|
60026
60197
|
};
|
|
60027
|
-
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60198
|
+
const collectCustomHookArgumentNames = (componentBody, scopes) => {
|
|
60028
60199
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
60029
60200
|
walkAst(componentBody, (child) => {
|
|
60030
60201
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
60031
60202
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
60032
60203
|
const calleeName = child.callee.name;
|
|
60033
60204
|
if (!isReactHookName(calleeName)) return;
|
|
60205
|
+
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
60034
60206
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
60035
60207
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
60036
60208
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -60071,21 +60243,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
60071
60243
|
create: (context) => {
|
|
60072
60244
|
const checkComponent = (componentBody) => {
|
|
60073
60245
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
60074
|
-
const bindings = collectUseStateBindings(componentBody);
|
|
60246
|
+
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
60075
60247
|
if (bindings.length === 0) return;
|
|
60076
60248
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
60077
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60249
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
60078
60250
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
60079
|
-
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60251
|
+
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
60080
60252
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
60081
60253
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
60082
60254
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
60083
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60255
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
|
|
60084
60256
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
60085
60257
|
for (const binding of bindings) if (effectInfos.some((effectInfo) => effectInfo.dependencyNames.has(binding.valueName) && effectInfo.synchronouslyCalledFunctionNames.has(binding.setterName) && !effectInfo.payloadReadNames.has(binding.valueName) && !effectInfo.nestedCallbackCalledFunctionNames.has(binding.setterName))) selfEchoValueNames.add(binding.valueName);
|
|
60086
60258
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
60087
60259
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
60088
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60260
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
60089
60261
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
60090
60262
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
60091
60263
|
walkAst(componentBody, (child) => {
|
|
@@ -67986,7 +68158,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
67986
68158
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
67987
68159
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
67988
68160
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
67989
|
-
const calleeName = getCalleeName$
|
|
68161
|
+
const calleeName = getCalleeName$1(argument);
|
|
67990
68162
|
if (!calleeName) continue;
|
|
67991
68163
|
if (isAuthGuardName(calleeName)) return true;
|
|
67992
68164
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -68626,7 +68798,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
68626
68798
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
68627
68799
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
68628
68800
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
68629
|
-
const calleeName = getCalleeName$
|
|
68801
|
+
const calleeName = getCalleeName$1(currentNode);
|
|
68630
68802
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
68631
68803
|
result.isServerFnChain = true;
|
|
68632
68804
|
const optionsArgument = currentNode.arguments?.[0];
|