oxlint-plugin-react-doctor 0.7.9-dev.7e6cdf9 → 0.7.9-dev.842a55f
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 +346 -377
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -836,131 +836,22 @@ 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 (node.
|
|
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
|
-
}
|
|
853
|
-
return null;
|
|
854
|
-
};
|
|
855
|
-
//#endregion
|
|
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;
|
|
839
|
+
//#region src/plugin/utils/get-callee-name.ts
|
|
840
|
+
const getCalleeName$2 = (node) => {
|
|
841
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
|
|
842
|
+
if (isNodeOfType(node.callee, "Identifier")) return node.callee.name;
|
|
843
|
+
if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier")) return node.callee.property.name;
|
|
862
844
|
return null;
|
|
863
845
|
};
|
|
864
846
|
//#endregion
|
|
865
|
-
//#region src/plugin/utils/
|
|
866
|
-
const
|
|
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 = {}) => {
|
|
847
|
+
//#region src/plugin/utils/is-hook-call.ts
|
|
848
|
+
const isHookCall$2 = (node, hookName) => {
|
|
934
849
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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);
|
|
850
|
+
const calleeName = getCalleeName$2(node);
|
|
851
|
+
if (!calleeName) return false;
|
|
852
|
+
return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
|
|
954
853
|
};
|
|
955
854
|
//#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
|
|
964
855
|
//#region src/plugin/utils/is-ast-node.ts
|
|
965
856
|
const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
966
857
|
//#endregion
|
|
@@ -1030,12 +921,12 @@ const collectChildComponentNames = (element, into) => {
|
|
|
1030
921
|
into.add(name);
|
|
1031
922
|
});
|
|
1032
923
|
};
|
|
1033
|
-
const countEffectHookCalls = (body
|
|
924
|
+
const countEffectHookCalls = (body) => {
|
|
1034
925
|
if (!body) return 0;
|
|
1035
926
|
let count = 0;
|
|
1036
927
|
walkAst(body, (child) => {
|
|
1037
928
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
1038
|
-
if (
|
|
929
|
+
if (isHookCall$2(child, EFFECT_HOOK_NAMES$1)) count++;
|
|
1039
930
|
});
|
|
1040
931
|
return count;
|
|
1041
932
|
};
|
|
@@ -1061,11 +952,11 @@ const getComponentEffectIndex = (programRoot) => {
|
|
|
1061
952
|
componentEffectIndexCache.set(programRoot, index);
|
|
1062
953
|
return index;
|
|
1063
954
|
};
|
|
1064
|
-
const getSameFileComponentEffectCount = (programRoot, componentName
|
|
955
|
+
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
1065
956
|
const index = getComponentEffectIndex(programRoot);
|
|
1066
957
|
const cachedCount = index.effectCountByName.get(componentName);
|
|
1067
958
|
if (cachedCount !== void 0) return cachedCount;
|
|
1068
|
-
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null
|
|
959
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
1069
960
|
index.effectCountByName.set(componentName, count);
|
|
1070
961
|
return count;
|
|
1071
962
|
};
|
|
@@ -1125,7 +1016,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
1125
1016
|
let totalEffects = 0;
|
|
1126
1017
|
const effectfulChildren = [];
|
|
1127
1018
|
for (const componentName of childComponentNames) {
|
|
1128
|
-
const effectCount = getSameFileComponentEffectCount(programRoot, componentName
|
|
1019
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
1129
1020
|
if (effectCount === 0) continue;
|
|
1130
1021
|
totalEffects += effectCount;
|
|
1131
1022
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1316,14 +1207,6 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
1316
1207
|
return best;
|
|
1317
1208
|
};
|
|
1318
1209
|
//#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
|
|
1327
1210
|
//#region src/plugin/utils/is-function-like.ts
|
|
1328
1211
|
/**
|
|
1329
1212
|
* Type-guard for the three "function-like" ESTree node shapes:
|
|
@@ -1337,6 +1220,38 @@ const getCalleeName$1 = (node) => {
|
|
|
1337
1220
|
*/
|
|
1338
1221
|
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
1339
1222
|
//#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
|
|
1340
1255
|
//#region src/plugin/utils/resolve-exact-local-function.ts
|
|
1341
1256
|
const resolveExactLocalFunction = (expression, scopes) => {
|
|
1342
1257
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -1382,17 +1297,9 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1382
1297
|
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1383
1298
|
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1384
1299
|
//#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
|
|
1393
1300
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1301
|
+
const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1302
|
+
"useCallback",
|
|
1396
1303
|
"useEffectEvent",
|
|
1397
1304
|
"useEvent",
|
|
1398
1305
|
"useEventCallback",
|
|
@@ -1401,21 +1308,21 @@ const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
|
1401
1308
|
]);
|
|
1402
1309
|
const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
|
|
1403
1310
|
const isThrottledHandlerHookCall = (callNode) => {
|
|
1404
|
-
const calleeName = getCalleeName$
|
|
1311
|
+
const calleeName = getCalleeName$2(callNode);
|
|
1405
1312
|
return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
|
|
1406
1313
|
};
|
|
1407
|
-
const isEmptyDepsUseMemoCall = (callNode
|
|
1408
|
-
if (!
|
|
1314
|
+
const isEmptyDepsUseMemoCall = (callNode) => {
|
|
1315
|
+
if (!isHookCall$2(callNode, "useMemo")) return false;
|
|
1409
1316
|
const memoDepsNode = callNode.arguments?.[1];
|
|
1410
1317
|
return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
|
|
1411
1318
|
};
|
|
1412
|
-
const isStableHandlerInitializer = (initializer
|
|
1413
|
-
if (isNodeOfType(initializer, "CallExpression")) return
|
|
1319
|
+
const isStableHandlerInitializer = (initializer) => {
|
|
1320
|
+
if (isNodeOfType(initializer, "CallExpression")) return isHookCall$2(initializer, STABLE_HANDLER_HOOK_NAMES) || isEmptyDepsUseMemoCall(initializer) || isThrottledHandlerHookCall(initializer);
|
|
1414
1321
|
return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
|
|
1415
1322
|
};
|
|
1416
|
-
const isStableRefReceiverDep = (referenceNode, receiverDepName
|
|
1323
|
+
const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
|
|
1417
1324
|
const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
|
|
1418
|
-
return Boolean(receiverBinding?.initializer &&
|
|
1325
|
+
return Boolean(receiverBinding?.initializer && isHookCall$2(receiverBinding.initializer, "useRef"));
|
|
1419
1326
|
};
|
|
1420
1327
|
const advancedEventHandlerRefs = defineRule({
|
|
1421
1328
|
id: "advanced-event-handler-refs",
|
|
@@ -1425,7 +1332,7 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1425
1332
|
category: "Performance",
|
|
1426
1333
|
recommendation: "Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",
|
|
1427
1334
|
create: (context) => ({ CallExpression(node) {
|
|
1428
|
-
if (!
|
|
1335
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
1429
1336
|
if ((node.arguments?.length ?? 0) < 2) return;
|
|
1430
1337
|
const callback = getEffectCallback(node);
|
|
1431
1338
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -1449,8 +1356,8 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
1449
1356
|
});
|
|
1450
1357
|
if (!registeredHandlerName) return;
|
|
1451
1358
|
const handlerBinding = findVariableInitializer(node, registeredHandlerName);
|
|
1452
|
-
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer
|
|
1453
|
-
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName
|
|
1359
|
+
if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
|
|
1360
|
+
if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName))) return;
|
|
1454
1361
|
context.report({
|
|
1455
1362
|
node,
|
|
1456
1363
|
message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
|
|
@@ -1937,6 +1844,15 @@ const flattenJsxName$1 = (node) => {
|
|
|
1937
1844
|
return null;
|
|
1938
1845
|
};
|
|
1939
1846
|
//#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
|
|
1940
1856
|
//#region src/plugin/utils/is-generated-image-renderer-call.ts
|
|
1941
1857
|
const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
1942
1858
|
"next/og",
|
|
@@ -4944,6 +4860,23 @@ const containsDirectAwait = (node) => {
|
|
|
4944
4860
|
return foundAwait;
|
|
4945
4861
|
};
|
|
4946
4862
|
//#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
|
|
4947
4880
|
//#region src/plugin/utils/get-destructured-binding-property-name.ts
|
|
4948
4881
|
const getDestructuredBindingPropertyName = (bindingIdentifier) => {
|
|
4949
4882
|
let bindingNode = bindingIdentifier;
|
|
@@ -9008,6 +8941,65 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
|
|
|
9008
8941
|
}
|
|
9009
8942
|
};
|
|
9010
8943
|
//#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
|
|
9011
9003
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
9012
9004
|
const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
|
|
9013
9005
|
"AbortSignal",
|
|
@@ -13891,7 +13883,6 @@ const walkInsideStatementBlocks = (node, visitor) => {
|
|
|
13891
13883
|
};
|
|
13892
13884
|
//#endregion
|
|
13893
13885
|
//#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
|
|
13894
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13895
13886
|
const getSubscribeLikeMethodName = (node) => {
|
|
13896
13887
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
13897
13888
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
@@ -13902,11 +13893,6 @@ const isSubscribeLikeCallExpression = (node) => {
|
|
|
13902
13893
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13903
13894
|
return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
|
|
13904
13895
|
};
|
|
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;
|
|
13910
13896
|
const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
13911
13897
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13912
13898
|
if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
|
|
@@ -13959,6 +13945,7 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13959
13945
|
};
|
|
13960
13946
|
//#endregion
|
|
13961
13947
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13948
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13962
13949
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13963
13950
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13964
13951
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -13968,6 +13955,10 @@ const RESOURCE_NOUN_BY_KIND = {
|
|
|
13968
13955
|
socket: "connection"
|
|
13969
13956
|
};
|
|
13970
13957
|
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
|
+
};
|
|
13971
13962
|
const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13972
13963
|
if (!expression) return null;
|
|
13973
13964
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -14069,13 +14060,12 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
14069
14060
|
});
|
|
14070
14061
|
return;
|
|
14071
14062
|
}
|
|
14072
|
-
|
|
14073
|
-
if (subscribeOrObserveMethodName !== null) {
|
|
14063
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) {
|
|
14074
14064
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
14075
14065
|
usages.push({
|
|
14076
14066
|
kind: "subscribe",
|
|
14077
14067
|
node: child,
|
|
14078
|
-
resourceName:
|
|
14068
|
+
resourceName: child.callee.property.name,
|
|
14079
14069
|
handleKey: findAssignedResourceKey(child, context),
|
|
14080
14070
|
...registrationDetails
|
|
14081
14071
|
});
|
|
@@ -14606,7 +14596,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
14606
14596
|
walkAst(componentFunction.body, (child) => {
|
|
14607
14597
|
if (didFindUnmountCleanup) return false;
|
|
14608
14598
|
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction) return;
|
|
14609
|
-
if (!
|
|
14599
|
+
if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
14610
14600
|
const dependencyList = child.arguments?.[1];
|
|
14611
14601
|
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
14612
14602
|
const cleanupCallback = getEffectCallback(child);
|
|
@@ -15037,7 +15027,7 @@ const getReleaseVerbName = (node) => {
|
|
|
15037
15027
|
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
15038
15028
|
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
15039
15029
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15040
|
-
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction
|
|
15030
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
15041
15031
|
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
15042
15032
|
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
15043
15033
|
if (controllerKey === null || refCurrentKey === null) return false;
|
|
@@ -15184,7 +15174,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15184
15174
|
return true;
|
|
15185
15175
|
};
|
|
15186
15176
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
15187
|
-
const isReturnedEffectCleanupFunction = (functionNode
|
|
15177
|
+
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
15188
15178
|
let currentNode = functionNode;
|
|
15189
15179
|
let parentNode = currentNode.parent;
|
|
15190
15180
|
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
@@ -15193,10 +15183,10 @@ const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
|
15193
15183
|
}
|
|
15194
15184
|
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
15195
15185
|
const effectCall = effectCallback?.parent;
|
|
15196
|
-
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") &&
|
|
15186
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
15197
15187
|
};
|
|
15198
15188
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
15199
|
-
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode
|
|
15189
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
15200
15190
|
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15201
15191
|
if (!bindingIdentifier) return false;
|
|
15202
15192
|
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
@@ -15693,7 +15683,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
|
15693
15683
|
return false;
|
|
15694
15684
|
}
|
|
15695
15685
|
}
|
|
15696
|
-
if (
|
|
15686
|
+
if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
15697
15687
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15698
15688
|
const subscriptionUsage = {
|
|
15699
15689
|
kind: "subscribe",
|
|
@@ -15901,7 +15891,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
|
15901
15891
|
if (!isFunctionLike$1(functionNode)) return false;
|
|
15902
15892
|
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15903
15893
|
const callbackCall = functionRoot.parent;
|
|
15904
|
-
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot &&
|
|
15894
|
+
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
|
|
15905
15895
|
const parentNode = functionNode.parent;
|
|
15906
15896
|
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
15907
15897
|
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
@@ -15937,12 +15927,12 @@ const effectNeedsCleanup = defineRule({
|
|
|
15937
15927
|
};
|
|
15938
15928
|
return {
|
|
15939
15929
|
CallExpression(node) {
|
|
15940
|
-
if (
|
|
15930
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
15941
15931
|
const retainedCallback = getEffectCallback(node);
|
|
15942
15932
|
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
15943
15933
|
return;
|
|
15944
15934
|
}
|
|
15945
|
-
if (!
|
|
15935
|
+
if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
15946
15936
|
const callback = getEffectCallback(node);
|
|
15947
15937
|
if (!callback) return;
|
|
15948
15938
|
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
@@ -15950,7 +15940,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
15950
15940
|
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
15951
15941
|
if (!firstUsage) return;
|
|
15952
15942
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
15953
|
-
const hookName = getCalleeName$
|
|
15943
|
+
const hookName = getCalleeName$2(node) ?? "effect";
|
|
15954
15944
|
context.report({
|
|
15955
15945
|
node,
|
|
15956
15946
|
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.`
|
|
@@ -18823,7 +18813,7 @@ const flattenCalleeName = (callee) => {
|
|
|
18823
18813
|
const PRAGMA = "React";
|
|
18824
18814
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
18825
18815
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
18826
|
-
if (getCalleeName$
|
|
18816
|
+
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
18827
18817
|
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
18828
18818
|
const receiver = stripParenExpression(node.callee.object);
|
|
18829
18819
|
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
@@ -19021,14 +19011,10 @@ const hookUseState = defineRule({
|
|
|
19021
19011
|
create: (context) => {
|
|
19022
19012
|
const { allowDestructuredState } = resolveSettings$40(context.settings);
|
|
19023
19013
|
return { CallExpression(node) {
|
|
19024
|
-
|
|
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;
|
|
19014
|
+
if (!isReactFunctionCall(node, "useState")) return;
|
|
19030
19015
|
const parent = node.parent;
|
|
19031
19016
|
if (!parent) return;
|
|
19017
|
+
if (isNodeOfType(parent, "ReturnStatement")) return;
|
|
19032
19018
|
if (!isNodeOfType(parent, "VariableDeclarator")) {
|
|
19033
19019
|
context.report({
|
|
19034
19020
|
node,
|
|
@@ -19136,8 +19122,8 @@ const hooksNoNanInDeps = defineRule({
|
|
|
19136
19122
|
severity: "warn",
|
|
19137
19123
|
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.",
|
|
19138
19124
|
create: (context) => ({ CallExpression(node) {
|
|
19139
|
-
if (!
|
|
19140
|
-
const depsIndex =
|
|
19125
|
+
if (!isHookCall$2(node, HOOKS_WITH_DEP_ARRAY)) return;
|
|
19126
|
+
const depsIndex = getCalleeName$2(node) === "useImperativeHandle" ? 2 : 1;
|
|
19141
19127
|
const depsArgument = node.arguments[depsIndex];
|
|
19142
19128
|
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
19143
19129
|
for (const element of depsArgument.elements) {
|
|
@@ -20308,7 +20294,7 @@ const isImportedSelectAtom = (callExpression) => {
|
|
|
20308
20294
|
const isDeferredCallbackPosition$1 = (functionNode) => {
|
|
20309
20295
|
const parent = functionNode.parent;
|
|
20310
20296
|
if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
|
|
20311
|
-
const hookName = getCalleeName$
|
|
20297
|
+
const hookName = getCalleeName$2(parent);
|
|
20312
20298
|
if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
|
|
20313
20299
|
if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
|
|
20314
20300
|
}
|
|
@@ -30379,7 +30365,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
|
|
|
30379
30365
|
"on",
|
|
30380
30366
|
"once"
|
|
30381
30367
|
]);
|
|
30382
|
-
const getCalleeName = (callee) => {
|
|
30368
|
+
const getCalleeName$1 = (callee) => {
|
|
30383
30369
|
if (!callee) return null;
|
|
30384
30370
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
30385
30371
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
@@ -30391,11 +30377,11 @@ const isDeferredCallbackPosition = (expression) => {
|
|
|
30391
30377
|
const parent = parentOf(expression);
|
|
30392
30378
|
if (!parent) return false;
|
|
30393
30379
|
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
30394
|
-
const name = getCalleeName(parent.callee);
|
|
30380
|
+
const name = getCalleeName$1(parent.callee);
|
|
30395
30381
|
if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
|
|
30396
30382
|
}
|
|
30397
30383
|
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
30398
|
-
const name = getCalleeName(parent.callee);
|
|
30384
|
+
const name = getCalleeName$1(parent.callee);
|
|
30399
30385
|
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
30400
30386
|
}
|
|
30401
30387
|
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
@@ -30815,6 +30801,14 @@ const isHookCallee$1 = (analysis, node, hookName) => {
|
|
|
30815
30801
|
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30816
30802
|
return false;
|
|
30817
30803
|
};
|
|
30804
|
+
const isUseEffect = (node) => {
|
|
30805
|
+
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
30806
|
+
const callee = node.callee;
|
|
30807
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
30808
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
30809
|
+
const receiver = stripParenExpression(callee.object);
|
|
30810
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
30811
|
+
};
|
|
30818
30812
|
const getEffectFn = (analysis, node) => {
|
|
30819
30813
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30820
30814
|
const fn = node.arguments?.[0];
|
|
@@ -32276,7 +32270,6 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32276
32270
|
sourceReferences,
|
|
32277
32271
|
isDeferred: frame.isDeferred,
|
|
32278
32272
|
isRenderKnownCopy,
|
|
32279
|
-
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
32280
32273
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
32281
32274
|
resetsSourceState: false
|
|
32282
32275
|
});
|
|
@@ -32292,71 +32285,27 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32292
32285
|
});
|
|
32293
32286
|
};
|
|
32294
32287
|
//#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
|
|
32341
32288
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
32342
32289
|
const noAdjustStateOnPropChange = defineRule({
|
|
32343
32290
|
id: "no-adjust-state-on-prop-change",
|
|
32344
|
-
title: "State
|
|
32291
|
+
title: "State synced to a prop inside an effect",
|
|
32345
32292
|
severity: "warn",
|
|
32346
32293
|
tags: ["test-noise"],
|
|
32347
|
-
recommendation: "
|
|
32294
|
+
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
32348
32295
|
create: (context) => ({ CallExpression(node) {
|
|
32349
|
-
if (!
|
|
32296
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
32297
|
+
allowGlobalReactNamespace: true,
|
|
32298
|
+
allowUnboundBareCalls: true,
|
|
32299
|
+
resolveConditionalAliases: true,
|
|
32300
|
+
resolveNamedAliases: true
|
|
32301
|
+
})) return;
|
|
32350
32302
|
const analysis = getProgramAnalysis(node);
|
|
32351
32303
|
if (!analysis) return;
|
|
32352
32304
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
32353
32305
|
if (!dependencyReferences) return;
|
|
32354
32306
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
32355
|
-
const
|
|
32356
|
-
|
|
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;
|
|
32307
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
32308
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
32360
32309
|
context.report({
|
|
32361
32310
|
node: fact.callExpression,
|
|
32362
32311
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
@@ -34369,7 +34318,7 @@ const declarationBodyContainsHookCall = (symbol) => {
|
|
|
34369
34318
|
walkAst(componentFunction, (descendant) => {
|
|
34370
34319
|
if (didFindHookCall) return false;
|
|
34371
34320
|
if (!isNodeOfType(descendant, "CallExpression")) return;
|
|
34372
|
-
const calleeName = getCalleeName$
|
|
34321
|
+
const calleeName = getCalleeName$2(descendant);
|
|
34373
34322
|
if (calleeName && isReactHookName(calleeName)) {
|
|
34374
34323
|
didFindHookCall = true;
|
|
34375
34324
|
return false;
|
|
@@ -34394,7 +34343,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
|
|
|
34394
34343
|
if (isNodeOfType(parent, "ArrowFunctionExpression")) {
|
|
34395
34344
|
if (parent.body !== current) return false;
|
|
34396
34345
|
const grandparent = parent.parent;
|
|
34397
|
-
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$
|
|
34346
|
+
return isNodeOfType(grandparent, "CallExpression") && getCalleeName$2(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
|
|
34398
34347
|
}
|
|
34399
34348
|
if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
|
|
34400
34349
|
current = parent;
|
|
@@ -36926,7 +36875,12 @@ const noDerivedState = defineRule({
|
|
|
36926
36875
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36927
36876
|
} }).visitors,
|
|
36928
36877
|
CallExpression(node) {
|
|
36929
|
-
if (!
|
|
36878
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
36879
|
+
allowGlobalReactNamespace: true,
|
|
36880
|
+
allowUnboundBareCalls: true,
|
|
36881
|
+
resolveConditionalAliases: true,
|
|
36882
|
+
resolveNamedAliases: true
|
|
36883
|
+
})) return;
|
|
36930
36884
|
const analysis = getProgramAnalysis(node);
|
|
36931
36885
|
if (!analysis) return;
|
|
36932
36886
|
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
@@ -36946,7 +36900,12 @@ const noDerivedStateEffect = defineRule({
|
|
|
36946
36900
|
tags: ["test-noise"],
|
|
36947
36901
|
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",
|
|
36948
36902
|
create: (context) => ({ CallExpression(node) {
|
|
36949
|
-
if (!
|
|
36903
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
36904
|
+
allowGlobalReactNamespace: true,
|
|
36905
|
+
allowUnboundBareCalls: true,
|
|
36906
|
+
resolveConditionalAliases: true,
|
|
36907
|
+
resolveNamedAliases: true
|
|
36908
|
+
})) return;
|
|
36950
36909
|
const analysis = getProgramAnalysis(node);
|
|
36951
36910
|
if (!analysis) return;
|
|
36952
36911
|
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
@@ -37069,7 +37028,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
|
|
|
37069
37028
|
if (isFunctionLike$1(cursor)) {
|
|
37070
37029
|
const parent = cursor.parent ?? null;
|
|
37071
37030
|
if (parent && isNodeOfType(parent, "CallExpression")) {
|
|
37072
|
-
const calleeName = getCalleeName$
|
|
37031
|
+
const calleeName = getCalleeName$2(parent);
|
|
37073
37032
|
if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
|
|
37074
37033
|
}
|
|
37075
37034
|
}
|
|
@@ -37140,7 +37099,7 @@ const isNonHandlerHookCallback = (functionNode) => {
|
|
|
37140
37099
|
const parent = functionNode.parent ?? null;
|
|
37141
37100
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
37142
37101
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
37143
|
-
const calleeName = getCalleeName$
|
|
37102
|
+
const calleeName = getCalleeName$2(parent);
|
|
37144
37103
|
return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
|
|
37145
37104
|
};
|
|
37146
37105
|
const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
@@ -37222,7 +37181,7 @@ const noDerivedUseState = defineRule({
|
|
|
37222
37181
|
return {
|
|
37223
37182
|
...propStackTracker.visitors,
|
|
37224
37183
|
CallExpression(node) {
|
|
37225
|
-
if (!
|
|
37184
|
+
if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
|
|
37226
37185
|
const seed = unwrapInitializerSeed(node.arguments[0]);
|
|
37227
37186
|
const reportStalePropCopy = (propName) => {
|
|
37228
37187
|
if (isIntentionalSnapshotState(node)) return;
|
|
@@ -37911,7 +37870,12 @@ const collectUseStateBindings = (componentBody, scopes) => {
|
|
|
37911
37870
|
const setterElement = elements[1];
|
|
37912
37871
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37913
37872
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37914
|
-
if (!
|
|
37873
|
+
if (!(scopes ? isReactApiCall(declarator.init, "useState", scopes, {
|
|
37874
|
+
allowGlobalReactNamespace: true,
|
|
37875
|
+
allowUnboundBareCalls: true,
|
|
37876
|
+
resolveConditionalAliases: true,
|
|
37877
|
+
resolveNamedAliases: true
|
|
37878
|
+
}) : isHookCall$2(declarator.init, "useState"))) continue;
|
|
37915
37879
|
bindings.push({
|
|
37916
37880
|
valueName: valueElement.name,
|
|
37917
37881
|
setterName: setterElement.name,
|
|
@@ -38078,7 +38042,7 @@ const noDirectStateMutation = defineRule({
|
|
|
38078
38042
|
create: (context) => {
|
|
38079
38043
|
const checkComponent = (componentBody) => {
|
|
38080
38044
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38081
|
-
const bindings = collectUseStateBindings(componentBody
|
|
38045
|
+
const bindings = collectUseStateBindings(componentBody);
|
|
38082
38046
|
if (bindings.length === 0) return;
|
|
38083
38047
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
38084
38048
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
@@ -38645,7 +38609,12 @@ const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
|
38645
38609
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38646
38610
|
const expression = unwrapDiscardedExpression(statement);
|
|
38647
38611
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
38648
|
-
if (!
|
|
38612
|
+
if (!isReactApiCall(expression, EFFECT_HOOK_NAMES$1, scopes, {
|
|
38613
|
+
allowGlobalReactNamespace: true,
|
|
38614
|
+
allowUnboundBareCalls: true,
|
|
38615
|
+
resolveConditionalAliases: true,
|
|
38616
|
+
resolveNamedAliases: true
|
|
38617
|
+
})) continue;
|
|
38649
38618
|
effectCalls.push(expression);
|
|
38650
38619
|
}
|
|
38651
38620
|
return effectCalls;
|
|
@@ -38889,7 +38858,7 @@ const collectStorageHookSetterNames = (componentBody) => {
|
|
|
38889
38858
|
for (const declarator of statement.declarations ?? []) {
|
|
38890
38859
|
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
38891
38860
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
38892
|
-
const calleeName = getCalleeName$
|
|
38861
|
+
const calleeName = getCalleeName$2(declarator.init);
|
|
38893
38862
|
if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
|
|
38894
38863
|
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
|
|
38895
38864
|
}
|
|
@@ -39377,7 +39346,7 @@ const noEffectEventHandler = defineRule({
|
|
|
39377
39346
|
return {
|
|
39378
39347
|
...propStackTracker.visitors,
|
|
39379
39348
|
CallExpression(node) {
|
|
39380
|
-
if (!
|
|
39349
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
|
|
39381
39350
|
const callback = getEffectCallback(node);
|
|
39382
39351
|
if (!callback) return;
|
|
39383
39352
|
const analysis = getProgramAnalysis(node);
|
|
@@ -39497,14 +39466,14 @@ const noEffectEventInDeps = defineRule({
|
|
|
39497
39466
|
if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
|
|
39498
39467
|
const initializer = declaratorNode.init;
|
|
39499
39468
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
39500
|
-
if (!
|
|
39469
|
+
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
39501
39470
|
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
39502
39471
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
39503
39472
|
} });
|
|
39504
39473
|
return {
|
|
39505
39474
|
...componentBindings.visitors,
|
|
39506
39475
|
CallExpression(node) {
|
|
39507
|
-
if (!
|
|
39476
|
+
if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
|
|
39508
39477
|
if (!componentBindings.isInsideComponent()) return;
|
|
39509
39478
|
const depsNode = node.arguments[1];
|
|
39510
39479
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -39561,7 +39530,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
39561
39530
|
node: finding.reportNode,
|
|
39562
39531
|
message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
|
|
39563
39532
|
});
|
|
39564
|
-
if (!
|
|
39533
|
+
if (!isHookCall$2(node, HOOKS_WITH_DEPS)) return;
|
|
39565
39534
|
const args = node.arguments ?? [];
|
|
39566
39535
|
if (args.length < 2) return;
|
|
39567
39536
|
const depsNode = args[1];
|
|
@@ -39822,7 +39791,7 @@ const noEventHandler = defineRule({
|
|
|
39822
39791
|
severity: "warn",
|
|
39823
39792
|
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",
|
|
39824
39793
|
create: (context) => ({ CallExpression(node) {
|
|
39825
|
-
if (!
|
|
39794
|
+
if (!isUseEffect(node)) return;
|
|
39826
39795
|
const analysis = getProgramAnalysis(node);
|
|
39827
39796
|
if (!analysis || hasCleanup(analysis, node)) return;
|
|
39828
39797
|
const frames = collectBoundedEffectExecutionFrames(analysis, node);
|
|
@@ -40284,25 +40253,25 @@ const addDeclarationBindings = (statement, scope) => {
|
|
|
40284
40253
|
}
|
|
40285
40254
|
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
|
|
40286
40255
|
};
|
|
40287
|
-
const collectRenderReachableNamesFromStatements = (statements, names, scope,
|
|
40256
|
+
const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40288
40257
|
let hasReturn = false;
|
|
40289
|
-
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope,
|
|
40258
|
+
for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
|
|
40290
40259
|
else addDeclarationBindings(statement, scope);
|
|
40291
40260
|
return hasReturn;
|
|
40292
40261
|
};
|
|
40293
|
-
const collectRenderReachableNamesFromStatement = (statement, names, scope,
|
|
40262
|
+
const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
|
|
40294
40263
|
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
40295
40264
|
if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
|
|
40296
40265
|
return true;
|
|
40297
40266
|
}
|
|
40298
|
-
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") &&
|
|
40267
|
+
if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) && !EFFECT_HOOK_NAMES$1.has(statement.expression.callee.name)) {
|
|
40299
40268
|
for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
|
|
40300
40269
|
return false;
|
|
40301
40270
|
}
|
|
40302
|
-
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope),
|
|
40271
|
+
if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
|
|
40303
40272
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
40304
|
-
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope,
|
|
40305
|
-
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope,
|
|
40273
|
+
const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
|
|
40274
|
+
const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
|
|
40306
40275
|
if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
40307
40276
|
return consequentHasReturn || alternateHasReturn;
|
|
40308
40277
|
}
|
|
@@ -40310,7 +40279,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, scope
|
|
|
40310
40279
|
let hasReturn = false;
|
|
40311
40280
|
for (const switchCase of statement.cases ?? []) {
|
|
40312
40281
|
const caseScope = createBlockBindingScope(scope);
|
|
40313
|
-
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope,
|
|
40282
|
+
if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
|
|
40314
40283
|
hasReturn = true;
|
|
40315
40284
|
if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
|
|
40316
40285
|
}
|
|
@@ -40318,25 +40287,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, scope
|
|
|
40318
40287
|
return hasReturn;
|
|
40319
40288
|
}
|
|
40320
40289
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
40321
|
-
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope,
|
|
40322
|
-
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope,
|
|
40323
|
-
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope,
|
|
40290
|
+
const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
|
|
40291
|
+
const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
|
|
40292
|
+
const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
|
|
40324
40293
|
return blockHasReturn || handlerHasReturn || finalizerHasReturn;
|
|
40325
40294
|
}
|
|
40326
40295
|
if (isNodeOfType(statement, "CatchClause")) {
|
|
40327
40296
|
const catchScope = createBlockBindingScope(scope);
|
|
40328
40297
|
addPatternBindings(statement.param, catchScope);
|
|
40329
|
-
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope,
|
|
40298
|
+
return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
|
|
40330
40299
|
}
|
|
40331
40300
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
40332
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope,
|
|
40301
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40333
40302
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
|
|
40334
40303
|
return bodyHasReturn;
|
|
40335
40304
|
}
|
|
40336
40305
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
40337
40306
|
const loopScope = createBlockBindingScope(scope);
|
|
40338
40307
|
if (statement.init) addDeclarationBindings(statement.init, loopScope);
|
|
40339
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope,
|
|
40308
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40340
40309
|
if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
|
|
40341
40310
|
if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
|
|
40342
40311
|
if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
|
|
@@ -40346,22 +40315,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, scope
|
|
|
40346
40315
|
const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
|
|
40347
40316
|
const loopScope = createBlockBindingScope(scope);
|
|
40348
40317
|
if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
|
|
40349
|
-
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope,
|
|
40318
|
+
if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
|
|
40350
40319
|
addNames(names, rightNames);
|
|
40351
40320
|
return true;
|
|
40352
40321
|
}
|
|
40353
|
-
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope,
|
|
40322
|
+
if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40354
40323
|
if (isNodeOfType(statement, "WithStatement")) {
|
|
40355
|
-
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope,
|
|
40324
|
+
const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
|
|
40356
40325
|
if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
|
|
40357
40326
|
return bodyHasReturn;
|
|
40358
40327
|
}
|
|
40359
40328
|
return false;
|
|
40360
40329
|
};
|
|
40361
|
-
const collectRenderReachableNames = (componentBody,
|
|
40330
|
+
const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
|
|
40362
40331
|
const names = /* @__PURE__ */ new Set();
|
|
40363
40332
|
if (!isNodeOfType(componentBody, "BlockStatement")) return names;
|
|
40364
|
-
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(),
|
|
40333
|
+
collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
|
|
40365
40334
|
return names;
|
|
40366
40335
|
};
|
|
40367
40336
|
//#endregion
|
|
@@ -40384,36 +40353,42 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
|
|
|
40384
40353
|
};
|
|
40385
40354
|
//#endregion
|
|
40386
40355
|
//#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
|
|
40387
|
-
const
|
|
40388
|
-
|
|
40356
|
+
const isUseCallbackCall = (node) => isNodeOfType(node, "CallExpression") && getCalleeName(node.callee) === "useCallback";
|
|
40357
|
+
const getCalleeName = (node) => {
|
|
40358
|
+
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
40359
|
+
if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
|
|
40360
|
+
return null;
|
|
40361
|
+
};
|
|
40362
|
+
const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
|
|
40363
|
+
if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
|
|
40389
40364
|
if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
|
|
40390
40365
|
const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
|
|
40391
40366
|
return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
|
|
40392
40367
|
};
|
|
40393
|
-
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope
|
|
40368
|
+
const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
|
|
40394
40369
|
if (!isNodeOfType(node, "ObjectExpression")) return;
|
|
40395
40370
|
for (const property of node.properties ?? []) {
|
|
40396
40371
|
if (!isNodeOfType(property, "Property")) continue;
|
|
40397
40372
|
const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
|
|
40398
40373
|
if (!propertyName) continue;
|
|
40399
|
-
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope
|
|
40374
|
+
if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
|
|
40400
40375
|
functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
|
|
40401
40376
|
}
|
|
40402
40377
|
};
|
|
40403
|
-
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope
|
|
40378
|
+
const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40404
40379
|
if (!isNodeOfType(statement, "VariableDeclaration")) return;
|
|
40405
40380
|
const declarationScope = getVariableDeclarationScope(statement, scope);
|
|
40406
40381
|
for (const declarator of statement.declarations ?? []) {
|
|
40407
40382
|
const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
|
|
40408
40383
|
if (!declarator.init) continue;
|
|
40409
|
-
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope
|
|
40384
|
+
const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
|
|
40410
40385
|
for (const declaredBindingName of declaredBindingNames) {
|
|
40411
40386
|
if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
|
|
40412
|
-
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope
|
|
40387
|
+
addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
|
|
40413
40388
|
}
|
|
40414
40389
|
}
|
|
40415
40390
|
};
|
|
40416
|
-
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope
|
|
40391
|
+
const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
|
|
40417
40392
|
if (isNodeOfType(statement, "FunctionDeclaration")) {
|
|
40418
40393
|
if (statement.id) {
|
|
40419
40394
|
const declaredBindingNames = addPatternBindings(statement.id, scope);
|
|
@@ -40422,61 +40397,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope,
|
|
|
40422
40397
|
return;
|
|
40423
40398
|
}
|
|
40424
40399
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
40425
|
-
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope
|
|
40400
|
+
addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40426
40401
|
return;
|
|
40427
40402
|
}
|
|
40428
40403
|
if (isNodeOfType(statement, "BlockStatement")) {
|
|
40429
|
-
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope)
|
|
40404
|
+
collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40430
40405
|
return;
|
|
40431
40406
|
}
|
|
40432
40407
|
if (isNodeOfType(statement, "IfStatement")) {
|
|
40433
|
-
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope
|
|
40434
|
-
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope
|
|
40408
|
+
collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
|
|
40409
|
+
if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
|
|
40435
40410
|
return;
|
|
40436
40411
|
}
|
|
40437
40412
|
if (isNodeOfType(statement, "SwitchStatement")) {
|
|
40438
|
-
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope)
|
|
40413
|
+
for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
|
|
40439
40414
|
return;
|
|
40440
40415
|
}
|
|
40441
40416
|
if (isNodeOfType(statement, "TryStatement")) {
|
|
40442
|
-
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope
|
|
40417
|
+
collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
|
|
40443
40418
|
if (statement.handler) {
|
|
40444
40419
|
const catchScope = createBlockBindingScope(scope);
|
|
40445
40420
|
addPatternBindings(statement.handler.param, catchScope);
|
|
40446
|
-
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope
|
|
40421
|
+
collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
|
|
40447
40422
|
}
|
|
40448
|
-
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope
|
|
40423
|
+
if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
|
|
40449
40424
|
return;
|
|
40450
40425
|
}
|
|
40451
40426
|
if (isNodeOfType(statement, "ForStatement")) {
|
|
40452
40427
|
const loopScope = createBlockBindingScope(scope);
|
|
40453
|
-
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope
|
|
40454
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope
|
|
40428
|
+
if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
|
|
40429
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40455
40430
|
return;
|
|
40456
40431
|
}
|
|
40457
40432
|
if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
|
|
40458
40433
|
const loopScope = createBlockBindingScope(scope);
|
|
40459
|
-
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope
|
|
40434
|
+
if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
|
|
40460
40435
|
else addPatternBindings(statement.left, loopScope);
|
|
40461
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope
|
|
40436
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
|
|
40462
40437
|
return;
|
|
40463
40438
|
}
|
|
40464
40439
|
if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
|
|
40465
|
-
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope
|
|
40440
|
+
collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40466
40441
|
return;
|
|
40467
40442
|
}
|
|
40468
|
-
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope
|
|
40443
|
+
if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
|
|
40469
40444
|
};
|
|
40470
|
-
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope
|
|
40471
|
-
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope
|
|
40445
|
+
const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
|
|
40446
|
+
for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
|
|
40472
40447
|
};
|
|
40473
|
-
const collectFunctionLikeLocalNames = (componentBody
|
|
40448
|
+
const collectFunctionLikeLocalNames = (componentBody) => {
|
|
40474
40449
|
const functionLikeLocalNames = /* @__PURE__ */ new Set();
|
|
40475
40450
|
if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
|
|
40476
40451
|
let previousSize = -1;
|
|
40477
40452
|
while (previousSize !== functionLikeLocalNames.size) {
|
|
40478
40453
|
previousSize = functionLikeLocalNames.size;
|
|
40479
|
-
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope()
|
|
40454
|
+
collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
|
|
40480
40455
|
}
|
|
40481
40456
|
return functionLikeLocalNames;
|
|
40482
40457
|
};
|
|
@@ -40516,17 +40491,17 @@ const noEventTriggerState = defineRule({
|
|
|
40516
40491
|
create: (context) => {
|
|
40517
40492
|
const checkComponent = (componentBody) => {
|
|
40518
40493
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
40519
|
-
const useStateBindings = collectUseStateBindings(componentBody
|
|
40494
|
+
const useStateBindings = collectUseStateBindings(componentBody);
|
|
40520
40495
|
if (useStateBindings.length === 0) return;
|
|
40521
40496
|
const analysis = getProgramAnalysis(componentBody);
|
|
40522
40497
|
if (!analysis) return;
|
|
40523
40498
|
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
40524
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody
|
|
40499
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
40525
40500
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
40526
|
-
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody,
|
|
40501
|
+
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
40527
40502
|
walkAst(componentBody, (effectCall) => {
|
|
40528
40503
|
if (!isNodeOfType(effectCall, "CallExpression")) return;
|
|
40529
|
-
if (!
|
|
40504
|
+
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) return;
|
|
40530
40505
|
if ((effectCall.arguments?.length ?? 0) < 2) return;
|
|
40531
40506
|
const depsNode = effectCall.arguments[1];
|
|
40532
40507
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -40591,17 +40566,10 @@ const isShadowedByLocalBinding = (identifier) => {
|
|
|
40591
40566
|
};
|
|
40592
40567
|
const isRealFetchCall = (node) => {
|
|
40593
40568
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
40594
|
-
|
|
40595
|
-
|
|
40596
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
40597
|
-
const receiver = stripParenExpression(callee.object);
|
|
40598
|
-
return isNodeOfType(receiver, "Identifier") && FETCH_MEMBER_OBJECTS.has(receiver.name) && !isShadowedByLocalBinding(receiver);
|
|
40599
|
-
};
|
|
40600
|
-
const isXmlHttpRequestConstruction = (node) => {
|
|
40601
|
-
if (!isNodeOfType(node, "NewExpression")) return false;
|
|
40602
|
-
const callee = stripParenExpression(node.callee);
|
|
40603
|
-
return isNodeOfType(callee, "Identifier") && callee.name === "XMLHttpRequest";
|
|
40569
|
+
if (isNodeOfType(node.callee, "Identifier") && FETCH_CALLEE_NAMES.has(node.callee.name)) return !isShadowedByLocalBinding(node.callee);
|
|
40570
|
+
return isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && FETCH_MEMBER_OBJECTS.has(node.callee.object.name) && !isShadowedByLocalBinding(node.callee.object);
|
|
40604
40571
|
};
|
|
40572
|
+
const isXmlHttpRequestConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "XMLHttpRequest";
|
|
40605
40573
|
const isNetworkRequest = (node) => isRealFetchCall(node) || isXmlHttpRequestConstruction(node);
|
|
40606
40574
|
const resolveLocalFunction = (expression, context) => {
|
|
40607
40575
|
if (!expression) return null;
|
|
@@ -42362,7 +42330,7 @@ const noInitializeState = defineRule({
|
|
|
42362
42330
|
tags: ["test-noise"],
|
|
42363
42331
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
42364
42332
|
create: (context) => ({ CallExpression(node) {
|
|
42365
|
-
if (!
|
|
42333
|
+
if (!isUseEffect(node)) return;
|
|
42366
42334
|
const dependencies = node.arguments?.[1];
|
|
42367
42335
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
42368
42336
|
const analysis = getProgramAnalysis(node);
|
|
@@ -43974,7 +43942,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43974
43942
|
const setterElement = elements[1];
|
|
43975
43943
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
43976
43944
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
43977
|
-
if (!
|
|
43945
|
+
if (!isHookCall$2(declarator.init, "useState")) continue;
|
|
43978
43946
|
const initializer = declarator.init.arguments?.[0];
|
|
43979
43947
|
if (!initializer) continue;
|
|
43980
43948
|
const propRootName = getPropRootName(initializer, propNames);
|
|
@@ -43992,7 +43960,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
43992
43960
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
43993
43961
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
43994
43962
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
43995
|
-
if (!
|
|
43963
|
+
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
43996
43964
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
43997
43965
|
const depsNode = effectCall.arguments[1];
|
|
43998
43966
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -44401,7 +44369,7 @@ const noMultiComp = defineRule({
|
|
|
44401
44369
|
});
|
|
44402
44370
|
//#endregion
|
|
44403
44371
|
//#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
|
|
44404
|
-
const collectUseRefBindingNames = (componentBody
|
|
44372
|
+
const collectUseRefBindingNames = (componentBody) => {
|
|
44405
44373
|
const useRefBindings = /* @__PURE__ */ new Set();
|
|
44406
44374
|
if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
|
|
44407
44375
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -44409,7 +44377,7 @@ const collectUseRefBindingNames = (componentBody, scopes) => {
|
|
|
44409
44377
|
for (const declarator of statement.declarations ?? []) {
|
|
44410
44378
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
44411
44379
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
44412
|
-
if (!
|
|
44380
|
+
if (!isHookCall$2(declarator.init, "useRef")) continue;
|
|
44413
44381
|
useRefBindings.add(declarator.id.name);
|
|
44414
44382
|
}
|
|
44415
44383
|
}
|
|
@@ -44444,12 +44412,12 @@ const noMutableInDeps = defineRule({
|
|
|
44444
44412
|
create: (context) => {
|
|
44445
44413
|
const checkComponent = (componentBody, componentParams = []) => {
|
|
44446
44414
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
44447
|
-
const useRefBindingNames = collectUseRefBindingNames(componentBody
|
|
44415
|
+
const useRefBindingNames = collectUseRefBindingNames(componentBody);
|
|
44448
44416
|
const localBindingNames = collectLocalBindingNames(componentBody);
|
|
44449
44417
|
for (const param of componentParams) collectPatternNames(param, localBindingNames);
|
|
44450
44418
|
walkAst(componentBody, (child) => {
|
|
44451
44419
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
44452
|
-
if (!
|
|
44420
|
+
if (!isHookCall$2(child, HOOKS_WITH_DEPS)) return;
|
|
44453
44421
|
if ((child.arguments?.length ?? 0) < 2) return;
|
|
44454
44422
|
const depsNode = child.arguments[1];
|
|
44455
44423
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
@@ -46362,10 +46330,16 @@ const noPassDataToParent = defineRule({
|
|
|
46362
46330
|
tags: ["test-noise"],
|
|
46363
46331
|
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",
|
|
46364
46332
|
create: (context) => {
|
|
46365
|
-
const isReactUseRefCall = (node) =>
|
|
46366
|
-
|
|
46333
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
46334
|
+
allowGlobalReactNamespace: true,
|
|
46335
|
+
allowUnboundBareCalls: true
|
|
46336
|
+
});
|
|
46337
|
+
const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
|
|
46338
|
+
allowGlobalReactNamespace: true,
|
|
46339
|
+
allowUnboundBareCalls: true
|
|
46340
|
+
});
|
|
46367
46341
|
return { CallExpression(node) {
|
|
46368
|
-
if (!
|
|
46342
|
+
if (!isUseEffect(node)) return;
|
|
46369
46343
|
const analysis = getProgramAnalysis(node);
|
|
46370
46344
|
if (!analysis) return;
|
|
46371
46345
|
if (hasCleanup(analysis, node)) return;
|
|
@@ -46652,7 +46626,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
46652
46626
|
tags: ["test-noise"],
|
|
46653
46627
|
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",
|
|
46654
46628
|
create: (context) => ({ CallExpression(node) {
|
|
46655
|
-
if (!
|
|
46629
|
+
if (!isUseEffect(node)) return;
|
|
46656
46630
|
const analysis = getProgramAnalysis(node);
|
|
46657
46631
|
if (!analysis) return;
|
|
46658
46632
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -47067,7 +47041,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
47067
47041
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
47068
47042
|
const binding = findVariableInitializer(effectNode, element.name);
|
|
47069
47043
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
|
|
47070
|
-
const calleeName = getCalleeName$
|
|
47044
|
+
const calleeName = getCalleeName$2(binding.initializer);
|
|
47071
47045
|
if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
|
|
47072
47046
|
}
|
|
47073
47047
|
return false;
|
|
@@ -47089,7 +47063,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
47089
47063
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
47090
47064
|
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
47091
47065
|
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
47092
|
-
if (getCalleeName$
|
|
47066
|
+
if (getCalleeName$2(binding.initializer) !== "useRef") return null;
|
|
47093
47067
|
const callbackArgument = binding.initializer.arguments?.[0];
|
|
47094
47068
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
47095
47069
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
@@ -47121,7 +47095,7 @@ const noPropCallbackInEffect = defineRule({
|
|
|
47121
47095
|
return {
|
|
47122
47096
|
...propStackTracker.visitors,
|
|
47123
47097
|
CallExpression(node) {
|
|
47124
|
-
if (!
|
|
47098
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
|
|
47125
47099
|
const callback = getEffectCallback(node);
|
|
47126
47100
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
47127
47101
|
const depsNode = node.arguments[1];
|
|
@@ -49014,7 +48988,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
49014
48988
|
tags: ["test-noise"],
|
|
49015
48989
|
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",
|
|
49016
48990
|
create: (context) => ({ CallExpression(node) {
|
|
49017
|
-
if (!
|
|
48991
|
+
if (!isUseEffect(node)) return;
|
|
49018
48992
|
const analysis = getProgramAnalysis(node);
|
|
49019
48993
|
if (!analysis) return;
|
|
49020
48994
|
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
@@ -49283,7 +49257,7 @@ const isTanStackServerFnHandlerCall = (node) => {
|
|
|
49283
49257
|
if (node.callee.property.name !== "handler") return false;
|
|
49284
49258
|
let currentNode = node.callee.object;
|
|
49285
49259
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49286
|
-
const calleeName = getCalleeName$
|
|
49260
|
+
const calleeName = getCalleeName$2(currentNode);
|
|
49287
49261
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
|
|
49288
49262
|
if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
|
|
49289
49263
|
currentNode = currentNode.callee.object;
|
|
@@ -49784,7 +49758,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49784
49758
|
create: (context) => {
|
|
49785
49759
|
const checkFunctionScope = (functionBody) => {
|
|
49786
49760
|
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
49787
|
-
const useStateBindings = collectUseStateBindings(functionBody
|
|
49761
|
+
const useStateBindings = collectUseStateBindings(functionBody);
|
|
49788
49762
|
if (useStateBindings.length === 0) return;
|
|
49789
49763
|
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
49790
49764
|
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
@@ -49793,7 +49767,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
49793
49767
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
49794
49768
|
const effectCall = unwrapDiscardedExpression(statement);
|
|
49795
49769
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
49796
|
-
if (!
|
|
49770
|
+
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
49797
49771
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
49798
49772
|
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
49799
49773
|
if (dependencyStateNames.size === 0) continue;
|
|
@@ -49879,7 +49853,7 @@ const noSetStateInRender = defineRule({
|
|
|
49879
49853
|
create: (context) => {
|
|
49880
49854
|
const checkComponent = (componentBody) => {
|
|
49881
49855
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
49882
|
-
const setterNames = new Set(collectUseStateBindings(componentBody
|
|
49856
|
+
const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
|
|
49883
49857
|
if (setterNames.size === 0) return;
|
|
49884
49858
|
for (const statement of componentBody.body ?? []) {
|
|
49885
49859
|
const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
|
|
@@ -50128,10 +50102,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
|
50128
50102
|
});
|
|
50129
50103
|
return facts;
|
|
50130
50104
|
};
|
|
50131
|
-
const isEffectCallbackFunction = (functionNode
|
|
50105
|
+
const isEffectCallbackFunction = (functionNode) => {
|
|
50132
50106
|
const parent = functionNode.parent;
|
|
50133
50107
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
50134
|
-
return
|
|
50108
|
+
return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
|
|
50135
50109
|
};
|
|
50136
50110
|
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
50137
50111
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -50150,24 +50124,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
|
50150
50124
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
50151
50125
|
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
50152
50126
|
};
|
|
50153
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope
|
|
50127
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
50154
50128
|
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
50155
50129
|
if (cleanupBindingName === null) return false;
|
|
50156
50130
|
let isReturnedFromEffect = false;
|
|
50157
50131
|
walkAst(ownerScope, (child) => {
|
|
50158
50132
|
if (isReturnedFromEffect) return false;
|
|
50159
|
-
if (!isNodeOfType(child, "CallExpression") || !
|
|
50133
|
+
if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
50160
50134
|
const effectCallback = getEffectCallback(child);
|
|
50161
50135
|
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
50162
50136
|
});
|
|
50163
50137
|
return isReturnedFromEffect;
|
|
50164
50138
|
};
|
|
50165
|
-
const isInsideEffectCleanupReturn = (node, ownerScope
|
|
50139
|
+
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
50166
50140
|
let functionNode = findEnclosingFunction$1(node);
|
|
50167
50141
|
while (functionNode) {
|
|
50168
50142
|
const outerFunction = findEnclosingFunction$1(functionNode);
|
|
50169
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction
|
|
50170
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope
|
|
50143
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
50144
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
50171
50145
|
functionNode = outerFunction;
|
|
50172
50146
|
}
|
|
50173
50147
|
return false;
|
|
@@ -50203,10 +50177,10 @@ const noStaleTimerRef = defineRule({
|
|
|
50203
50177
|
if (isShadowedTimerGlobal(node)) return;
|
|
50204
50178
|
const { clearCalleeName, refName } = clearCall;
|
|
50205
50179
|
const refBinding = findVariableInitializer(node, refName);
|
|
50206
|
-
if (!refBinding?.initializer || !
|
|
50180
|
+
if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
|
|
50207
50181
|
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
50208
50182
|
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
50209
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner
|
|
50183
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
50210
50184
|
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
50211
50185
|
context.report({
|
|
50212
50186
|
node,
|
|
@@ -56010,7 +55984,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
56010
55984
|
allReadsAreInSubHandlers = false;
|
|
56011
55985
|
return;
|
|
56012
55986
|
}
|
|
56013
|
-
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$
|
|
55987
|
+
if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$2(subHandlerCall);
|
|
56014
55988
|
});
|
|
56015
55989
|
return {
|
|
56016
55990
|
hasAnyRead,
|
|
@@ -56033,7 +56007,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
56033
56007
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
56034
56008
|
const effectCall = statement.expression;
|
|
56035
56009
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
56036
|
-
if (!
|
|
56010
|
+
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
56037
56011
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
56038
56012
|
const depsNode = effectCall.arguments[1];
|
|
56039
56013
|
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
@@ -56065,11 +56039,11 @@ const preferUseEffectEvent = defineRule({
|
|
|
56065
56039
|
});
|
|
56066
56040
|
//#endregion
|
|
56067
56041
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
56068
|
-
const findUseEffectsInComponent = (componentBody
|
|
56042
|
+
const findUseEffectsInComponent = (componentBody) => {
|
|
56069
56043
|
const effectCalls = [];
|
|
56070
56044
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
56071
56045
|
for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
|
|
56072
|
-
if (isNodeOfType(child, "CallExpression") &&
|
|
56046
|
+
if (isNodeOfType(child, "CallExpression") && isHookCall$2(child, EFFECT_HOOK_NAMES$1)) effectCalls.push(child);
|
|
56073
56047
|
});
|
|
56074
56048
|
return effectCalls;
|
|
56075
56049
|
};
|
|
@@ -56272,7 +56246,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56272
56246
|
};
|
|
56273
56247
|
const checkComponent = (componentBody) => {
|
|
56274
56248
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
56275
|
-
const useStateBindings = collectUseStateBindings(componentBody
|
|
56249
|
+
const useStateBindings = collectUseStateBindings(componentBody);
|
|
56276
56250
|
if (useStateBindings.length === 0) return;
|
|
56277
56251
|
const useStateInitializerByValueName = /* @__PURE__ */ new Map();
|
|
56278
56252
|
for (const binding of useStateBindings) {
|
|
@@ -56285,7 +56259,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56285
56259
|
}
|
|
56286
56260
|
const setterNameToValueName = /* @__PURE__ */ new Map();
|
|
56287
56261
|
for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
|
|
56288
|
-
for (const effectCall of findUseEffectsInComponent(componentBody
|
|
56262
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56289
56263
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
56290
56264
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
56291
56265
|
const depsNode = effectCall.arguments[1];
|
|
@@ -56327,7 +56301,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
56327
56301
|
})).filter((candidate) => candidate.storeName !== null);
|
|
56328
56302
|
if (snapshotBindings.length === 0) return;
|
|
56329
56303
|
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
56330
|
-
for (const effectCall of findUseEffectsInComponent(componentBody
|
|
56304
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
56331
56305
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
56332
56306
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
56333
56307
|
const depsNode = effectCall.arguments[1];
|
|
@@ -56424,7 +56398,7 @@ const preferUseReducer = defineRule({
|
|
|
56424
56398
|
create: (context) => {
|
|
56425
56399
|
const reportCoUpdatedUseState = (body, componentName) => {
|
|
56426
56400
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
56427
|
-
const bindings = collectUseStateBindings(body
|
|
56401
|
+
const bindings = collectUseStateBindings(body);
|
|
56428
56402
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
56429
56403
|
if (setterNames.size < 5) return;
|
|
56430
56404
|
const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
|
|
@@ -56640,7 +56614,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
|
|
|
56640
56614
|
]);
|
|
56641
56615
|
const isQueryCacheSourceCall = (initializer) => {
|
|
56642
56616
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
56643
|
-
const hookName = getCalleeName$
|
|
56617
|
+
const hookName = getCalleeName$2(initializer);
|
|
56644
56618
|
if (!hookName) return false;
|
|
56645
56619
|
return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
|
|
56646
56620
|
};
|
|
@@ -56772,7 +56746,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
56772
56746
|
},
|
|
56773
56747
|
CallExpression(node) {
|
|
56774
56748
|
if (!hasQueryReadUsage) {
|
|
56775
|
-
const callName = getCalleeName$
|
|
56749
|
+
const callName = getCalleeName$2(node);
|
|
56776
56750
|
if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
|
|
56777
56751
|
}
|
|
56778
56752
|
const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
|
|
@@ -57265,30 +57239,26 @@ const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
|
57265
57239
|
["useCallback", "This `useCallback` is dead weight, since React Compiler already caches every function here. Delete it."],
|
|
57266
57240
|
["memo", "This `memo()` is dead weight, since React Compiler already caches the component's output. Delete it."]
|
|
57267
57241
|
]);
|
|
57268
|
-
const resolveReactApiNameForIdentifier = (callee
|
|
57242
|
+
const resolveReactApiNameForIdentifier = (callee) => {
|
|
57269
57243
|
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
57270
|
-
if (context.scopes.symbolFor(callee)?.kind !== "import") return null;
|
|
57271
57244
|
const importedName = getImportedNameFromModule(callee, callee.name, "react");
|
|
57272
57245
|
if (importedName && REMOVAL_MESSAGE_BY_REACT_API_NAME.has(importedName)) return importedName;
|
|
57273
57246
|
return null;
|
|
57274
57247
|
};
|
|
57275
|
-
const resolveReactApiNameForMemberExpression = (callee
|
|
57248
|
+
const resolveReactApiNameForMemberExpression = (callee) => {
|
|
57276
57249
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
57277
57250
|
if (callee.computed) return null;
|
|
57278
|
-
const namespaceIdentifier =
|
|
57251
|
+
const namespaceIdentifier = callee.object;
|
|
57279
57252
|
const propertyIdentifier = callee.property;
|
|
57280
57253
|
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return null;
|
|
57281
57254
|
if (!isNodeOfType(propertyIdentifier, "Identifier")) return null;
|
|
57282
57255
|
if (!REMOVAL_MESSAGE_BY_REACT_API_NAME.has(propertyIdentifier.name)) return null;
|
|
57283
57256
|
const namespaceName = namespaceIdentifier.name;
|
|
57284
|
-
if (
|
|
57285
|
-
if (
|
|
57257
|
+
if (isCanonicalReactNamespaceName(namespaceName)) return propertyIdentifier.name;
|
|
57258
|
+
if (isImportedFromModule(namespaceIdentifier, namespaceName, "react")) return propertyIdentifier.name;
|
|
57286
57259
|
return null;
|
|
57287
57260
|
};
|
|
57288
|
-
const resolveReactApiNameForCallee = (callee
|
|
57289
|
-
const unwrappedCallee = stripParenExpression(callee);
|
|
57290
|
-
return resolveReactApiNameForIdentifier(unwrappedCallee, context) ?? resolveReactApiNameForMemberExpression(unwrappedCallee, context);
|
|
57291
|
-
};
|
|
57261
|
+
const resolveReactApiNameForCallee = (callee) => resolveReactApiNameForIdentifier(callee) ?? resolveReactApiNameForMemberExpression(callee);
|
|
57292
57262
|
const isNullishComparatorArgument = (argumentNode) => isNodeOfType(argumentNode, "Identifier") && argumentNode.name === "undefined" || isNodeOfType(argumentNode, "Literal") && argumentNode.value === null;
|
|
57293
57263
|
const COMPILER_INFERABLE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
57294
57264
|
const calleeTrailingName = (callee) => {
|
|
@@ -57314,7 +57284,7 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
57314
57284
|
requires: ["react-compiler"],
|
|
57315
57285
|
recommendation: "Delete the `useMemo` / `useCallback` / `memo` call and use the plain value or component. React Compiler caches it for you.",
|
|
57316
57286
|
create: (context) => ({ CallExpression(node) {
|
|
57317
|
-
const apiName = resolveReactApiNameForCallee(node.callee
|
|
57287
|
+
const apiName = resolveReactApiNameForCallee(node.callee);
|
|
57318
57288
|
if (!apiName) return;
|
|
57319
57289
|
if (apiName === "memo") {
|
|
57320
57290
|
const comparatorArgument = node.arguments?.[1];
|
|
@@ -59064,7 +59034,7 @@ const rerenderDependencies = defineRule({
|
|
|
59064
59034
|
severity: "error",
|
|
59065
59035
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
59066
59036
|
create: (context) => ({ CallExpression(node) {
|
|
59067
|
-
if (!
|
|
59037
|
+
if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
|
|
59068
59038
|
const depsNode = node.arguments[1];
|
|
59069
59039
|
if (!isNodeOfType(depsNode, "ArrayExpression")) return;
|
|
59070
59040
|
for (const element of depsNode.elements ?? []) {
|
|
@@ -59319,7 +59289,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
59319
59289
|
category: "Performance",
|
|
59320
59290
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
59321
59291
|
create: (context) => ({ CallExpression(node) {
|
|
59322
|
-
if (!
|
|
59292
|
+
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
59323
59293
|
const initializer = stripParenExpression(node.arguments[0]);
|
|
59324
59294
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
59325
59295
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
@@ -59383,7 +59353,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
59383
59353
|
category: "Performance",
|
|
59384
59354
|
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
59385
59355
|
create: (context) => ({ CallExpression(node) {
|
|
59386
|
-
if (!
|
|
59356
|
+
if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
|
|
59387
59357
|
const initializer = findEagerInitializerCall(node.arguments[0]);
|
|
59388
59358
|
if (!initializer) return;
|
|
59389
59359
|
const isConstructor = isNodeOfType(initializer, "NewExpression");
|
|
@@ -60146,11 +60116,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
|
|
|
60146
60116
|
}
|
|
60147
60117
|
return false;
|
|
60148
60118
|
};
|
|
60149
|
-
const collectEffectDependencyInfos = (componentBody, setterNames
|
|
60119
|
+
const collectEffectDependencyInfos = (componentBody, setterNames) => {
|
|
60150
60120
|
const effectInfos = [];
|
|
60151
60121
|
walkAst(componentBody, (child) => {
|
|
60152
60122
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
60153
|
-
if (!
|
|
60123
|
+
if (!isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
60154
60124
|
const dependencyNames = /* @__PURE__ */ new Set();
|
|
60155
60125
|
for (const argument of child.arguments ?? []) {
|
|
60156
60126
|
if (!isNodeOfType(argument, "ArrayExpression")) continue;
|
|
@@ -60206,14 +60176,13 @@ const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
|
|
|
60206
60176
|
});
|
|
60207
60177
|
return effectInfos;
|
|
60208
60178
|
};
|
|
60209
|
-
const collectCustomHookArgumentNames = (componentBody
|
|
60179
|
+
const collectCustomHookArgumentNames = (componentBody) => {
|
|
60210
60180
|
const argumentNames = /* @__PURE__ */ new Set();
|
|
60211
60181
|
walkAst(componentBody, (child) => {
|
|
60212
60182
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
60213
60183
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
60214
60184
|
const calleeName = child.callee.name;
|
|
60215
60185
|
if (!isReactHookName(calleeName)) return;
|
|
60216
|
-
if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
|
|
60217
60186
|
if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
|
|
60218
60187
|
if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
|
|
60219
60188
|
for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
|
|
@@ -60254,21 +60223,21 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
60254
60223
|
create: (context) => {
|
|
60255
60224
|
const checkComponent = (componentBody) => {
|
|
60256
60225
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
60257
|
-
const bindings = collectUseStateBindings(componentBody
|
|
60226
|
+
const bindings = collectUseStateBindings(componentBody);
|
|
60258
60227
|
if (bindings.length === 0) return;
|
|
60259
60228
|
if (collectRenderReachableExpressions(componentBody).length === 0) return;
|
|
60260
|
-
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody
|
|
60229
|
+
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
60261
60230
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
60262
|
-
const directRenderNames = collectRenderReachableNames(componentBody,
|
|
60231
|
+
const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
|
|
60263
60232
|
if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
|
|
60264
60233
|
const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
|
|
60265
60234
|
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
60266
|
-
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames
|
|
60235
|
+
const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
|
|
60267
60236
|
const selfEchoValueNames = /* @__PURE__ */ new Set();
|
|
60268
60237
|
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);
|
|
60269
60238
|
const effectConsumedNames = /* @__PURE__ */ new Set();
|
|
60270
60239
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
60271
|
-
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody
|
|
60240
|
+
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
|
|
60272
60241
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
60273
60242
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
60274
60243
|
walkAst(componentBody, (child) => {
|
|
@@ -68169,7 +68138,7 @@ const declarationAwaitsGate = (declaration, context) => {
|
|
|
68169
68138
|
if (!isNodeOfType(argument, "CallExpression")) continue;
|
|
68170
68139
|
if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
|
|
68171
68140
|
if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
|
|
68172
|
-
const calleeName = getCalleeName$
|
|
68141
|
+
const calleeName = getCalleeName$2(argument);
|
|
68173
68142
|
if (!calleeName) continue;
|
|
68174
68143
|
if (isAuthGuardName(calleeName)) return true;
|
|
68175
68144
|
const [leadingToken] = tokenizeIdentifierWords(calleeName);
|
|
@@ -68809,7 +68778,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
68809
68778
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
68810
68779
|
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
68811
68780
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
68812
|
-
const calleeName = getCalleeName$
|
|
68781
|
+
const calleeName = getCalleeName$2(currentNode);
|
|
68813
68782
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
68814
68783
|
result.isServerFnChain = true;
|
|
68815
68784
|
const optionsArgument = currentNode.arguments?.[0];
|