oxlint-plugin-react-doctor 0.7.7-dev.e5b0690 → 0.7.7-dev.ec144d9

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.
Files changed (2) hide show
  1. package/dist/index.js +413 -79
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5991,15 +5991,17 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5991
5991
  //#endregion
5992
5992
  //#region src/plugin/utils/get-static-property-key-name.ts
5993
5993
  const getStaticPropertyKeyName = (node, options = {}) => {
5994
- if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition")) return null;
5994
+ if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
5995
+ const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
5995
5996
  if (node.computed) {
5996
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
5997
+ if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
5998
+ if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
5997
5999
  return null;
5998
6000
  }
5999
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
6000
- if (isNodeOfType(node.key, "Literal")) {
6001
- if (typeof node.key.value === "string") return node.key.value;
6002
- if (options.stringifyNonStringLiterals) return String(node.key.value);
6001
+ if (isNodeOfType(key, "Identifier")) return key.name;
6002
+ if (isNodeOfType(key, "Literal")) {
6003
+ if (typeof key.value === "string") return key.value;
6004
+ if (options.stringifyNonStringLiterals) return String(key.value);
6003
6005
  }
6004
6006
  return null;
6005
6007
  };
@@ -11003,6 +11005,14 @@ const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
11003
11005
  "catch",
11004
11006
  "finally"
11005
11007
  ]);
11008
+ const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
11009
+ const getPromiseChainCallForCallback = (candidate) => {
11010
+ let callbackContainer = candidate.parent;
11011
+ while (callbackContainer && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(callbackContainer.type)) callbackContainer = callbackContainer.parent;
11012
+ if (!isNodeOfType(callbackContainer, "CallExpression")) return null;
11013
+ if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
11014
+ return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
11015
+ };
11006
11016
  const collectEffectInvokedFunctions = (effectCallback) => {
11007
11017
  const invokedFunctions = new Set([effectCallback]);
11008
11018
  const localFunctionBindings = /* @__PURE__ */ new Map();
@@ -11014,7 +11024,6 @@ const collectEffectInvokedFunctions = (effectCallback) => {
11014
11024
  invokedFunctions.add(strippedCandidate);
11015
11025
  pendingFunctions.push(strippedCandidate);
11016
11026
  };
11017
- const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
11018
11027
  while (pendingFunctions.length > 0) {
11019
11028
  const currentFunction = pendingFunctions.pop();
11020
11029
  if (!currentFunction) break;
@@ -11222,6 +11231,50 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
11222
11231
  return true;
11223
11232
  };
11224
11233
  //#endregion
11234
+ //#region src/plugin/utils/is-node-reachable-within-function.ts
11235
+ const isInsideStaticallyUnreachableBranch = (node) => {
11236
+ let child = node;
11237
+ let parent = node.parent;
11238
+ while (parent) {
11239
+ if (isNodeOfType(parent, "IfStatement") && isNodeOfType(parent.test, "Literal")) {
11240
+ if (parent.test.value === false && parent.consequent === child) return true;
11241
+ if (parent.test.value === true && parent.alternate === child) return true;
11242
+ }
11243
+ if (isNodeOfType(parent, "ConditionalExpression") && isNodeOfType(parent.test, "Literal")) {
11244
+ if (parent.test.value === false && parent.consequent === child) return true;
11245
+ if (parent.test.value === true && parent.alternate === child) return true;
11246
+ }
11247
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === child) {
11248
+ if (isNodeOfType(parent.left, "Literal") && (parent.operator === "&&" && !parent.left.value || parent.operator === "||" && Boolean(parent.left.value))) return true;
11249
+ }
11250
+ child = parent;
11251
+ parent = parent.parent;
11252
+ }
11253
+ return false;
11254
+ };
11255
+ const isNodeReachableWithinFunction = (node, context) => {
11256
+ if (isInsideStaticallyUnreachableBranch(node)) return false;
11257
+ const owner = context.cfg.enclosingFunction(node);
11258
+ if (!owner) return true;
11259
+ const functionCfg = context.cfg.cfgFor(owner);
11260
+ if (!functionCfg) return true;
11261
+ const targetBlock = functionCfg.blockOf(node);
11262
+ if (!targetBlock) return true;
11263
+ const visitedBlocks = new Set([functionCfg.entry]);
11264
+ const pendingBlocks = [functionCfg.entry];
11265
+ while (pendingBlocks.length > 0) {
11266
+ const currentBlock = pendingBlocks.pop();
11267
+ if (!currentBlock) break;
11268
+ if (currentBlock === targetBlock) return true;
11269
+ for (const edge of currentBlock.successors) {
11270
+ if (visitedBlocks.has(edge.to)) continue;
11271
+ visitedBlocks.add(edge.to);
11272
+ pendingBlocks.push(edge.to);
11273
+ }
11274
+ }
11275
+ return false;
11276
+ };
11277
+ //#endregion
11225
11278
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
11226
11279
  const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
11227
11280
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
@@ -11350,27 +11403,6 @@ const findSubscribeLikeUsages = (callback, context) => {
11350
11403
  });
11351
11404
  return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
11352
11405
  };
11353
- const isNodeReachableWithinFunction = (node, context) => {
11354
- const owner = context.cfg.enclosingFunction(node);
11355
- if (!owner) return true;
11356
- const functionCfg = context.cfg.cfgFor(owner);
11357
- if (!functionCfg) return true;
11358
- const targetBlock = functionCfg.blockOf(node);
11359
- if (!targetBlock) return true;
11360
- const visitedBlocks = new Set([functionCfg.entry]);
11361
- const pendingBlocks = [functionCfg.entry];
11362
- while (pendingBlocks.length > 0) {
11363
- const currentBlock = pendingBlocks.pop();
11364
- if (!currentBlock) break;
11365
- if (currentBlock === targetBlock) return true;
11366
- for (const edge of currentBlock.successors) {
11367
- if (visitedBlocks.has(edge.to)) continue;
11368
- visitedBlocks.add(edge.to);
11369
- pendingBlocks.push(edge.to);
11370
- }
11371
- }
11372
- return false;
11373
- };
11374
11406
  const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
11375
11407
  let pathAnchor = usageNode;
11376
11408
  let pathOwner = findEnclosingFunction(pathAnchor);
@@ -11844,6 +11876,94 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
11844
11876
  return didFindUnmountCleanup;
11845
11877
  };
11846
11878
  const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
11879
+ const collectBlockingBooleanStates = (expression, blockedExpressionValue, context) => {
11880
+ const unwrappedExpression = stripParenExpression(expression);
11881
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, context);
11882
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
11883
+ if (!(unwrappedExpression.operator === "||" && blockedExpressionValue || unwrappedExpression.operator === "&&" && !blockedExpressionValue)) return [];
11884
+ return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, context)];
11885
+ }
11886
+ if (isNodeOfType(unwrappedExpression, "BinaryExpression") && [
11887
+ "===",
11888
+ "==",
11889
+ "!==",
11890
+ "!="
11891
+ ].includes(unwrappedExpression.operator)) {
11892
+ const leftValue = readStaticBoolean(unwrappedExpression.left);
11893
+ const rightValue = readStaticBoolean(unwrappedExpression.right);
11894
+ const booleanValue = leftValue ?? rightValue;
11895
+ const comparedKey = resolveExpressionKey(leftValue === null ? unwrappedExpression.left : unwrappedExpression.right, context);
11896
+ if (booleanValue === null || comparedKey === null) return [];
11897
+ return [{
11898
+ key: comparedKey,
11899
+ value: (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==") === blockedExpressionValue ? booleanValue : !booleanValue
11900
+ }];
11901
+ }
11902
+ const expressionKey = resolveExpressionKey(unwrappedExpression, context);
11903
+ return expressionKey === null ? [] : [{
11904
+ key: expressionKey,
11905
+ value: blockedExpressionValue
11906
+ }];
11907
+ };
11908
+ const isDirectEarlyReturnConsequent = (ifStatement) => {
11909
+ if (!isNodeOfType(ifStatement, "IfStatement") || ifStatement.alternate) return false;
11910
+ if (isNodeOfType(ifStatement.consequent, "ReturnStatement")) return true;
11911
+ return isNodeOfType(ifStatement.consequent, "BlockStatement") && ifStatement.consequent.body.length === 1 && isNodeOfType(ifStatement.consequent.body[0], "ReturnStatement");
11912
+ };
11913
+ const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
11914
+ if (!isFunctionLike$2(callback) || callback.async) return [];
11915
+ const guardStates = [];
11916
+ walkAst(callback.body, (child) => {
11917
+ if (child !== callback.body && isFunctionLike$2(child)) return false;
11918
+ if (isNodeOfType(child, "IfStatement") && isDirectEarlyReturnConsequent(child) && doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], callback, context)) guardStates.push(...collectBlockingBooleanStates(child.test, true, context));
11919
+ });
11920
+ let descendant = usageNode;
11921
+ let ancestor = descendant.parent;
11922
+ while (ancestor && ancestor !== callback) {
11923
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, context));
11924
+ descendant = ancestor;
11925
+ ancestor = ancestor.parent;
11926
+ }
11927
+ return guardStates;
11928
+ };
11929
+ const cleanupReturnInvalidatesGuard = (cleanupReturn, guardState, context) => {
11930
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
11931
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
11932
+ if (!cleanupFunction || !isFunctionLike$2(cleanupFunction) || cleanupFunction.async) return false;
11933
+ let didInvalidateGuard = false;
11934
+ walkAst(cleanupFunction.body, (child) => {
11935
+ if (didInvalidateGuard) return false;
11936
+ if (child !== cleanupFunction.body && isFunctionLike$2(child)) return false;
11937
+ if (isNodeOfType(child, "AssignmentExpression") && child.operator === "=" && resolveExpressionKey(child.left, context) === guardState.key && readStaticBoolean(child.right) === guardState.value && context.cfg.isUnconditionalFromEntry(child)) {
11938
+ didInvalidateGuard = true;
11939
+ return false;
11940
+ }
11941
+ });
11942
+ return didInvalidateGuard;
11943
+ };
11944
+ const deferredUsageWritesGuardBeforeUsage = (callback, usageNode, guardState, context) => {
11945
+ const usageStart = getRangeStart(usageNode);
11946
+ if (!isFunctionLike$2(callback) || usageStart === null) return true;
11947
+ let didWriteGuard = false;
11948
+ walkAst(callback.body, (child) => {
11949
+ if (didWriteGuard) return false;
11950
+ if (child !== callback.body && isFunctionLike$2(child)) return false;
11951
+ const childStart = getRangeStart(child);
11952
+ if (childStart === null || childStart >= usageStart) return;
11953
+ const writtenExpression = isNodeOfType(child, "AssignmentExpression") ? child.left : isNodeOfType(child, "UpdateExpression") ? child.argument : null;
11954
+ if (writtenExpression && resolveExpressionKey(writtenExpression, context) === guardState.key) {
11955
+ didWriteGuard = true;
11956
+ return false;
11957
+ }
11958
+ });
11959
+ return didWriteGuard;
11960
+ };
11961
+ const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
11962
+ const usageFunction = findEnclosingFunction(usage.node);
11963
+ const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
11964
+ if (usage.handleKey === null || !usageFunction || usageFunction === callback || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
11965
+ return collectDeferredUsageGuardStates(usageFunction, usage.node, context).some((guardState) => !deferredUsageWritesGuardBeforeUsage(usageFunction, usage.node, guardState, context) && cleanupReturns.every((cleanupReturn) => cleanupReturnInvalidatesGuard(cleanupReturn, guardState, context)));
11966
+ };
11847
11967
  const effectHasCleanupForUsage = (callback, usage, context) => {
11848
11968
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
11849
11969
  if (callback.async) return false;
@@ -11883,6 +12003,7 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
11883
12003
  if (!cleanupFunction || !isFunctionLike$2(cleanupFunction)) return;
11884
12004
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
11885
12005
  });
12006
+ if (hasGuardedDeferredCleanup(callback, usage, matchingCleanupReturns, context)) return true;
11886
12007
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
11887
12008
  };
11888
12009
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
@@ -17201,7 +17322,7 @@ const GLOBAL_BUILTIN_NAMES = new Set([
17201
17322
  ]);
17202
17323
  const STRING_PROTOTYPE_PATH = "String.prototype";
17203
17324
  const REGEXP_PROTOTYPE_PATH = "RegExp.prototype";
17204
- const getStaticStringValue = (argument) => {
17325
+ const getStaticStringValue$1 = (argument) => {
17205
17326
  if (!argument) return null;
17206
17327
  const unwrappedArgument = stripParenExpression(argument);
17207
17328
  if (isNodeOfType(unwrappedArgument, "Literal") && typeof unwrappedArgument.value === "string") return unwrappedArgument.value;
@@ -17209,7 +17330,7 @@ const getStaticStringValue = (argument) => {
17209
17330
  return null;
17210
17331
  };
17211
17332
  const getEffectiveRegExpFlags = (patternArgument, flagsArgument) => {
17212
- if (flagsArgument) return getStaticStringValue(flagsArgument);
17333
+ if (flagsArgument) return getStaticStringValue$1(flagsArgument);
17213
17334
  if (!patternArgument) return "";
17214
17335
  const unwrappedPattern = stripParenExpression(patternArgument);
17215
17336
  if (isNodeOfType(unwrappedPattern, "Literal") && unwrappedPattern.value instanceof RegExp) return unwrappedPattern.value.flags;
@@ -17359,7 +17480,7 @@ const getCallRegExpHazard = (node, context, symbolCache) => {
17359
17480
  const mutationHazard = targetPath === "global" ? "globalRegExpReplaced" : "replaceAllIntegrityLost";
17360
17481
  const guardedPropertyName = targetPath === "global" ? "RegExp" : "replaceAll";
17361
17482
  if (isSinglePropertyMutation) {
17362
- const propertyName = getStaticStringValue(node.arguments?.[1]);
17483
+ const propertyName = getStaticStringValue$1(node.arguments?.[1]);
17363
17484
  return propertyName === null || propertyName === guardedPropertyName ? mutationHazard : "none";
17364
17485
  }
17365
17486
  if (!isPropertyCollectionMutation) return "none";
@@ -25449,8 +25570,11 @@ const unwrapUseCallback = (node) => {
25449
25570
  const callee = node.callee;
25450
25571
  return isNodeOfType(callee, "Identifier") && callee.name === "useCallback" || isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useCallback" ? node.arguments?.[0] : node;
25451
25572
  };
25573
+ const hasParameterDefinition = (ref) => Boolean(ref.resolved?.defs.some((definition) => definition.type === "Parameter"));
25452
25574
  const resolveToFunction = (ref) => {
25453
- const definitionNode = ref.resolved?.defs[0]?.node;
25575
+ const definition = ref.resolved?.defs[0];
25576
+ if (!definition || hasParameterDefinition(ref)) return null;
25577
+ const definitionNode = definition.node;
25454
25578
  if (!definitionNode) return null;
25455
25579
  if (isFunctionLike$2(definitionNode)) return definitionNode;
25456
25580
  if (isNodeOfType(definitionNode, "VariableDeclarator")) {
@@ -25860,7 +25984,7 @@ const isCleanupReturnArgument = (analysis, node) => {
25860
25984
  if (isNodeOfType(node, "MemberExpression")) return true;
25861
25985
  if (isNodeOfType(node, "Identifier")) {
25862
25986
  const ref = getRef(analysis, node);
25863
- if (ref && resolveToFunction(ref)) return true;
25987
+ if (ref && (hasParameterDefinition(ref) || resolveToFunction(ref))) return true;
25864
25988
  }
25865
25989
  if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
25866
25990
  return false;
@@ -26332,7 +26456,7 @@ const resolveWrappedCallable = (analysis, node) => {
26332
26456
  if (isNodeOfType(candidate, "Identifier")) {
26333
26457
  const reference = getRef(analysis, candidate);
26334
26458
  if (!reference) return null;
26335
- if (reference.resolved?.defs.some((definition) => definition.type === "Parameter" || definition.type === "ImportBinding")) return null;
26459
+ if (reference.resolved?.defs.some((definition) => definition.type === "ImportBinding")) return null;
26336
26460
  const resolved = resolveToFunction(reference);
26337
26461
  if (resolved) return resolved;
26338
26462
  const definitionNode = reference.resolved?.defs[0]?.node;
@@ -38141,8 +38265,6 @@ const getWrapperHookWrappedFunction = (initializer) => {
38141
38265
  if (!wrapped || !isFunctionLike$2(wrapped)) return null;
38142
38266
  return wrapped;
38143
38267
  };
38144
- const hasParameterDef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "Parameter"));
38145
- const resolvesToFunctionBinding = (ref) => !hasParameterDef(ref) && Boolean(resolveToFunction(ref));
38146
38268
  const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
38147
38269
  const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
38148
38270
  if (!isProp(analysis, innerRef)) return false;
@@ -38463,7 +38585,7 @@ const noPassDataToParent = defineRule({
38463
38585
  if (isConstant(argRef)) return false;
38464
38586
  if (isParentWiredHookResultRef(analysis, argRef)) return false;
38465
38587
  if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
38466
- if (resolvesToFunctionBinding(argRef)) return false;
38588
+ if (resolveToFunction(argRef)) return false;
38467
38589
  const argIdentifier = argRef.identifier;
38468
38590
  if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
38469
38591
  if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
@@ -46894,7 +47016,258 @@ const queryMutationMissingInvalidation = defineRule({
46894
47016
  }
46895
47017
  });
46896
47018
  //#endregion
47019
+ //#region src/plugin/utils/is-node-conditionally-executed.ts
47020
+ const isNodeConditionallyExecuted = (node, boundary) => {
47021
+ let child = node;
47022
+ let parent = child.parent ?? null;
47023
+ while (parent && parent !== boundary) {
47024
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === child || parent.alternate === child)) return true;
47025
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === child) return true;
47026
+ if (isNodeOfType(parent, "AssignmentPattern") && parent.right === child) return true;
47027
+ child = parent;
47028
+ parent = child.parent ?? null;
47029
+ }
47030
+ return false;
47031
+ };
47032
+ //#endregion
47033
+ //#region src/plugin/rules/tanstack-query/utils/resolve-tanstack-query-hook-name.ts
47034
+ const resolveTanstackNamespaceHookName = (memberExpression, contextNode, scopes) => {
47035
+ const hookName = getStaticPropertyKeyName(memberExpression, { allowComputedString: true });
47036
+ const namespaceObject = stripParenExpression(memberExpression.object);
47037
+ if (!hookName || !TANSTACK_QUERY_HOOKS.has(hookName)) return null;
47038
+ if (!isNodeOfType(namespaceObject, "Identifier")) return null;
47039
+ const resolvedNamespaceSymbol = resolveConstIdentifierAlias(namespaceObject, scopes);
47040
+ if (resolvedNamespaceSymbol?.kind !== "import") return null;
47041
+ const namespaceBinding = getImportBindingForName(contextNode, resolvedNamespaceSymbol.name);
47042
+ return namespaceBinding?.isNamespace && isTanstackQuerySource(namespaceBinding.source) ? hookName : null;
47043
+ };
47044
+ const resolveTanstackQueryHookName = (callExpression, scopes) => {
47045
+ const callee = stripParenExpression(callExpression.callee);
47046
+ if (isNodeOfType(callee, "Identifier")) {
47047
+ const resolvedSymbol = resolveConstIdentifierAlias(callee, scopes);
47048
+ if (!resolvedSymbol) return null;
47049
+ if (resolvedSymbol.kind === "const" && resolvedSymbol.initializer) {
47050
+ const initializer = stripParenExpression(resolvedSymbol.initializer);
47051
+ return isNodeOfType(initializer, "MemberExpression") ? resolveTanstackNamespaceHookName(initializer, callExpression, scopes) : null;
47052
+ }
47053
+ if (resolvedSymbol.kind !== "import") return null;
47054
+ const importBinding = getImportBindingForName(callExpression, resolvedSymbol.name);
47055
+ if (importBinding === null) return null;
47056
+ if (importBinding.isNamespace || !isTanstackQuerySource(importBinding.source)) return null;
47057
+ return importBinding.exportedName !== null && TANSTACK_QUERY_HOOKS.has(importBinding.exportedName) ? importBinding.exportedName : null;
47058
+ }
47059
+ if (isNodeOfType(callee, "MemberExpression")) return resolveTanstackNamespaceHookName(callee, callExpression, scopes);
47060
+ return null;
47061
+ };
47062
+ const resolveTanstackQueryHookNameFromInitializer = (initializer, scopes) => {
47063
+ const unwrappedInitializer = stripParenExpression(initializer);
47064
+ if (isNodeOfType(unwrappedInitializer, "CallExpression")) return resolveTanstackQueryHookName(unwrappedInitializer, scopes);
47065
+ if (!isNodeOfType(unwrappedInitializer, "Identifier")) return null;
47066
+ const resolvedSymbol = resolveConstIdentifierAlias(unwrappedInitializer, scopes);
47067
+ if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
47068
+ const resolvedInitializer = stripParenExpression(resolvedSymbol.initializer);
47069
+ if (!isNodeOfType(resolvedInitializer, "CallExpression")) return null;
47070
+ return resolveTanstackQueryHookName(resolvedInitializer, scopes);
47071
+ };
47072
+ //#endregion
46897
47073
  //#region src/plugin/rules/tanstack-query/query-no-query-in-effect.ts
47074
+ const isTanstackQueryResult = (expression, context) => Boolean(resolveTanstackQueryHookNameFromInitializer(expression, context.scopes));
47075
+ const isStaticRefetchMember = (memberExpression) => getStaticPropertyKeyName(memberExpression, { allowComputedString: true }) === "refetch";
47076
+ const resolveCalledFunction = (callee, context) => {
47077
+ const unwrappedCallee = stripParenExpression(callee);
47078
+ if (isFunctionLike$2(unwrappedCallee)) return unwrappedCallee;
47079
+ if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
47080
+ const symbol = resolveConstIdentifierAlias(unwrappedCallee, context.scopes);
47081
+ if (!symbol) return null;
47082
+ const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
47083
+ return candidate && isFunctionLike$2(candidate) ? candidate : null;
47084
+ };
47085
+ const hasSuspensionBefore = (functionNode, boundary, context) => {
47086
+ if (!isFunctionLike$2(functionNode)) return true;
47087
+ if (functionNode.generator) return true;
47088
+ const boundaryStart = getRangeStart(boundary);
47089
+ if (boundaryStart === null) return true;
47090
+ let hasSuspension = false;
47091
+ walkAst(functionNode, (node) => {
47092
+ if (node !== functionNode && isFunctionLike$2(node)) return false;
47093
+ if (!isNodeOfType(node, "AwaitExpression") || !isNodeReachableWithinFunction(node, context)) return;
47094
+ const suspensionStart = getRangeStart(node);
47095
+ if (suspensionStart !== null && suspensionStart < boundaryStart) {
47096
+ hasSuspension = true;
47097
+ return false;
47098
+ }
47099
+ });
47100
+ return hasSuspension;
47101
+ };
47102
+ const isFunctionAncestor = (ancestor, functionNode) => {
47103
+ let enclosingFunction = findEnclosingFunction(functionNode);
47104
+ while (enclosingFunction) {
47105
+ if (enclosingFunction === ancestor) return true;
47106
+ enclosingFunction = findEnclosingFunction(enclosingFunction);
47107
+ }
47108
+ return false;
47109
+ };
47110
+ const isUnconditionallyExecuted = (node, functionNode, context) => context.cfg.isUnconditionalFromEntry(node) && !isNodeConditionallyExecuted(node, functionNode) && !(isNodeOfType(node, "MemberExpression") && isNodeOfType(node.parent, "AssignmentExpression") && node.parent.left === node && (node.parent.operator === "&&=" || node.parent.operator === "||=" || node.parent.operator === "??="));
47111
+ const functionInvokesTarget = (callerFunction, targetFunction, context, visitedFunctions, canCrossSuspension = false) => {
47112
+ if (visitedFunctions.has(callerFunction)) return false;
47113
+ visitedFunctions.add(callerFunction);
47114
+ let invokesTarget = false;
47115
+ walkAst(callerFunction, (node) => {
47116
+ if (node !== callerFunction && isFunctionLike$2(node)) return false;
47117
+ if (!isNodeOfType(node, "CallExpression")) return;
47118
+ if (!isNodeReachableWithinFunction(node, context) || !isUnconditionallyExecuted(node, callerFunction, context) || !canCrossSuspension && hasSuspensionBefore(callerFunction, node, context)) return;
47119
+ const calledFunction = resolveCalledFunction(node.callee, context);
47120
+ if (calledFunction === targetFunction || calledFunction && functionInvokesTarget(calledFunction, targetFunction, context, visitedFunctions, canCrossSuspension)) {
47121
+ invokesTarget = true;
47122
+ return false;
47123
+ }
47124
+ });
47125
+ return invokesTarget;
47126
+ };
47127
+ const isFunctionInvokedBefore = (invokedFunction, boundary, context) => {
47128
+ const boundaryFunction = findEnclosingFunction(boundary);
47129
+ const boundaryStart = getRangeStart(boundary);
47130
+ if (!boundaryFunction || boundaryStart === null) return false;
47131
+ let isInvokedBefore = false;
47132
+ walkAst(boundaryFunction, (node) => {
47133
+ if (node !== boundaryFunction && isFunctionLike$2(node)) return false;
47134
+ if (!isNodeOfType(node, "CallExpression")) return;
47135
+ const callStart = getRangeStart(node);
47136
+ if (callStart === null || callStart >= boundaryStart || !isNodeReachableWithinFunction(node, context) || !isUnconditionallyExecuted(node, boundaryFunction, context) || hasSuspensionBefore(boundaryFunction, node, context)) return;
47137
+ const calledFunction = resolveCalledFunction(node.callee, context);
47138
+ if (calledFunction === invokedFunction || calledFunction && functionInvokesTarget(calledFunction, invokedFunction, context, /* @__PURE__ */ new Set())) {
47139
+ isInvokedBefore = true;
47140
+ return false;
47141
+ }
47142
+ });
47143
+ return isInvokedBefore;
47144
+ };
47145
+ const isFunctionInvokedAfter = (invokedFunction, boundary, callerFunction, context) => {
47146
+ const boundaryStart = getRangeStart(boundary);
47147
+ if (boundaryStart === null) return false;
47148
+ let isInvokedAfter = false;
47149
+ walkAst(callerFunction, (node) => {
47150
+ if (node !== callerFunction && isFunctionLike$2(node)) return false;
47151
+ if (!isNodeOfType(node, "CallExpression")) return;
47152
+ const callStart = getRangeStart(node);
47153
+ if (callStart === null || callStart <= boundaryStart || !isNodeReachableWithinFunction(node, context) || !isUnconditionallyExecuted(node, callerFunction, context)) return;
47154
+ const calledFunction = resolveCalledFunction(node.callee, context);
47155
+ if (calledFunction === invokedFunction || calledFunction && functionInvokesTarget(calledFunction, invokedFunction, context, /* @__PURE__ */ new Set(), true)) {
47156
+ isInvokedAfter = true;
47157
+ return false;
47158
+ }
47159
+ });
47160
+ return isInvokedAfter;
47161
+ };
47162
+ const isWriteExecutedBefore = (writeNode, boundary, context, deferredExecutionFunction) => {
47163
+ const writeStart = getRangeStart(writeNode);
47164
+ const boundaryStart = getRangeStart(boundary);
47165
+ const writeFunction = findEnclosingFunction(writeNode);
47166
+ const writeExecutionBoundary = writeFunction ?? findProgramRoot(writeNode);
47167
+ if (writeStart === null || boundaryStart === null || !writeExecutionBoundary || !isNodeReachableWithinFunction(writeNode, context) || !isUnconditionallyExecuted(writeNode, writeExecutionBoundary, context)) return false;
47168
+ const boundaryFunction = findEnclosingFunction(boundary);
47169
+ const renderFunction = deferredExecutionFunction ? findEnclosingFunction(deferredExecutionFunction) : null;
47170
+ if (writeFunction === boundaryFunction) return writeStart < boundaryStart;
47171
+ if (!writeFunction) return writeStart < boundaryStart;
47172
+ if (boundaryFunction && isFunctionAncestor(writeFunction, boundaryFunction)) {
47173
+ if (Boolean(renderFunction && (writeFunction === renderFunction || isFunctionAncestor(writeFunction, renderFunction)))) return true;
47174
+ if (deferredExecutionFunction && boundaryFunction !== deferredExecutionFunction) {
47175
+ if (hasSuspensionBefore(boundaryFunction, boundary, context) && isFunctionInvokedBefore(boundaryFunction, writeNode, context)) return true;
47176
+ return isFunctionInvokedAfter(boundaryFunction, writeNode, writeFunction, context);
47177
+ }
47178
+ return writeStart < boundaryStart;
47179
+ }
47180
+ if (renderFunction && isFunctionAncestor(renderFunction, writeFunction) && !hasSuspensionBefore(writeFunction, writeNode, context) && functionInvokesTarget(renderFunction, writeFunction, context, /* @__PURE__ */ new Set())) return true;
47181
+ return !hasSuspensionBefore(writeFunction, writeNode, context) && isFunctionInvokedBefore(writeFunction, boundary, context);
47182
+ };
47183
+ const getStaticStringValue = (node) => {
47184
+ const unwrappedNode = stripParenExpression(node);
47185
+ if (isNodeOfType(unwrappedNode, "Literal") && typeof unwrappedNode.value === "string") return unwrappedNode.value;
47186
+ if (isNodeOfType(unwrappedNode, "TemplateLiteral") && unwrappedNode.expressions.length === 0) return getStaticTemplateLiteralValue(unwrappedNode);
47187
+ return null;
47188
+ };
47189
+ const isSameRefetchMember = (target, candidate, context) => {
47190
+ const unwrappedTarget = stripParenExpression(target);
47191
+ const unwrappedCandidate = stripParenExpression(candidate);
47192
+ if (!isNodeOfType(unwrappedTarget, "Identifier") || !isNodeOfType(unwrappedCandidate, "MemberExpression") || !isStaticRefetchMember(unwrappedCandidate)) return false;
47193
+ const candidateTarget = stripParenExpression(unwrappedCandidate.object);
47194
+ if (!isNodeOfType(candidateTarget, "Identifier")) return false;
47195
+ const targetSymbol = resolveConstIdentifierAlias(unwrappedTarget, context.scopes);
47196
+ const candidateSymbol = resolveConstIdentifierAlias(candidateTarget, context.scopes);
47197
+ return Boolean(targetSymbol && candidateSymbol?.id === targetSymbol.id);
47198
+ };
47199
+ const getRefetchMutationTarget = (node, context) => {
47200
+ if (isNodeOfType(node, "MemberExpression") && isStaticRefetchMember(node)) {
47201
+ const parent = node.parent;
47202
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === node && isSameRefetchMember(node.object, parent.right, context)) return null;
47203
+ return isNodeOfType(parent, "AssignmentExpression") && parent.left === node || isNodeOfType(parent, "UpdateExpression") && parent.argument === node || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" ? node.object : null;
47204
+ }
47205
+ if (!isNodeOfType(node, "CallExpression")) return null;
47206
+ const callee = stripParenExpression(node.callee);
47207
+ if (!isNodeOfType(callee, "MemberExpression")) return null;
47208
+ const receiver = stripParenExpression(callee.object);
47209
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "Object" || context.scopes.symbolFor(receiver)) return null;
47210
+ const methodName = getStaticPropertyKeyName(callee, { allowComputedString: true });
47211
+ const target = node.arguments[0];
47212
+ if (!target || isNodeOfType(target, "SpreadElement")) return null;
47213
+ if (methodName === "defineProperty") {
47214
+ const propertyKey = node.arguments[1];
47215
+ if (!propertyKey || getStaticStringValue(propertyKey) !== "refetch") return null;
47216
+ const descriptor = node.arguments[2];
47217
+ if (isNodeOfType(descriptor, "ObjectExpression")) {
47218
+ const valueProperty = descriptor.properties.find((property) => isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "value");
47219
+ if (isNodeOfType(valueProperty, "Property") && isSameRefetchMember(target, valueProperty.value, context)) return null;
47220
+ }
47221
+ return target;
47222
+ }
47223
+ if (methodName !== "assign") return null;
47224
+ let finalRefetchValue = null;
47225
+ for (const source of node.arguments.slice(1)) {
47226
+ if (!isNodeOfType(source, "ObjectExpression")) continue;
47227
+ for (const property of source.properties) if (isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "refetch") finalRefetchValue = property.value;
47228
+ }
47229
+ if (!finalRefetchValue || isSameRefetchMember(target, finalRefetchValue, context)) return null;
47230
+ return target;
47231
+ };
47232
+ const hasRefetchMemberWriteBefore = (expression, boundary, context, deferredExecutionFunction) => {
47233
+ const unwrappedExpression = stripParenExpression(expression);
47234
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
47235
+ const resultSymbol = resolveConstIdentifierAlias(unwrappedExpression, context.scopes);
47236
+ if (!resultSymbol) return false;
47237
+ const program = findProgramRoot(expression);
47238
+ if (!program) return true;
47239
+ if (getRangeStart(boundary) === null) return true;
47240
+ let hasWrite = false;
47241
+ walkAst(program, (node) => {
47242
+ const mutationTarget = getRefetchMutationTarget(node, context);
47243
+ if (!mutationTarget) return;
47244
+ if (!isWriteExecutedBefore(node, boundary, context, deferredExecutionFunction)) return;
47245
+ const object = stripParenExpression(mutationTarget);
47246
+ if (!isNodeOfType(object, "Identifier")) return;
47247
+ if (resolveConstIdentifierAlias(object, context.scopes)?.id === resultSymbol.id) {
47248
+ hasWrite = true;
47249
+ return false;
47250
+ }
47251
+ });
47252
+ return hasWrite;
47253
+ };
47254
+ const isTanstackRefetchExpression = (expression, context, deferredExecutionFunction, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47255
+ const unwrappedExpression = stripParenExpression(expression);
47256
+ if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isStaticRefetchMember(unwrappedExpression) && isTanstackQueryResult(unwrappedExpression.object, context) && !hasRefetchMemberWriteBefore(unwrappedExpression.object, unwrappedExpression, context, deferredExecutionFunction);
47257
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
47258
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
47259
+ if (!symbol || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read") || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
47260
+ visitedSymbolIds.add(symbol.id);
47261
+ const bindingProperty = symbol.bindingIdentifier.parent;
47262
+ if (isNodeOfType(bindingProperty, "Property") && getStaticPropertyKeyName(bindingProperty, { allowComputedString: true }) === "refetch") {
47263
+ const initializer = symbol.declarationNode.init;
47264
+ return Boolean(initializer && isTanstackQueryResult(initializer, context) && !hasRefetchMemberWriteBefore(initializer, symbol.declarationNode, context, null));
47265
+ }
47266
+ return Boolean(symbol.declarationNode.id === symbol.bindingIdentifier && symbol.initializer && isTanstackRefetchExpression(symbol.initializer, context, null, visitedSymbolIds));
47267
+ };
47268
+ const isTanstackRefetchCall = (callExpression, context, effectCallback) => {
47269
+ return isTanstackRefetchExpression(callExpression.callee, context, effectCallback);
47270
+ };
46898
47271
  const queryNoQueryInEffect = defineRule({
46899
47272
  id: "query-no-query-in-effect",
46900
47273
  title: "Query refetch inside useEffect",
@@ -46910,8 +47283,7 @@ const queryNoQueryInEffect = defineRule({
46910
47283
  walkAst(callback, (child) => {
46911
47284
  if (child !== callback && isFunctionLike$2(child) && !effectInvokedFunctions.has(child)) return false;
46912
47285
  if (!isNodeOfType(child, "CallExpression")) return;
46913
- const callee = child.callee;
46914
- if (isNodeOfType(callee, "Identifier") && callee.name === "refetch" || isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "refetch") context.report({
47286
+ if (isTanstackRefetchCall(child, context, callback)) context.report({
46915
47287
  node: child,
46916
47288
  message: "refetch() inside useEffect duplicates work React Query already does, causing extra fetches."
46917
47289
  });
@@ -46920,28 +47292,6 @@ const queryNoQueryInEffect = defineRule({
46920
47292
  });
46921
47293
  //#endregion
46922
47294
  //#region src/plugin/rules/tanstack-query/query-no-rest-destructuring.ts
46923
- const resolveTanstackQueryHookName = (callExpression) => {
46924
- const callee = callExpression.callee;
46925
- if (isNodeOfType(callee, "Identifier")) {
46926
- const importBinding = getImportBindingForName(callExpression, callee.name);
46927
- if (importBinding === null) return TANSTACK_QUERY_HOOKS.has(callee.name) ? callee.name : null;
46928
- if (importBinding.isNamespace || !isTanstackQuerySource(importBinding.source)) return null;
46929
- return importBinding.exportedName !== null && TANSTACK_QUERY_HOOKS.has(importBinding.exportedName) ? importBinding.exportedName : null;
46930
- }
46931
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && TANSTACK_QUERY_HOOKS.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) {
46932
- const namespaceBinding = getImportBindingForName(callExpression, callee.object.name);
46933
- if (namespaceBinding?.isNamespace && isTanstackQuerySource(namespaceBinding.source)) return callee.property.name;
46934
- }
46935
- return null;
46936
- };
46937
- const resolveHookNameFromInitializer = (initializer, scopes) => {
46938
- if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
46939
- if (!isNodeOfType(initializer, "Identifier")) return null;
46940
- const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
46941
- if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
46942
- if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
46943
- return resolveTanstackQueryHookName(resolvedSymbol.initializer);
46944
- };
46945
47295
  const queryNoRestDestructuring = defineRule({
46946
47296
  id: "query-no-rest-destructuring",
46947
47297
  title: "Rest destructuring on query result",
@@ -46953,7 +47303,7 @@ const queryNoRestDestructuring = defineRule({
46953
47303
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
46954
47304
  if (!node.init) return;
46955
47305
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
46956
- const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
47306
+ const hookName = resolveTanstackQueryHookNameFromInitializer(node.init, context.scopes);
46957
47307
  if (!hookName) return;
46958
47308
  context.report({
46959
47309
  node: node.id,
@@ -55649,22 +55999,6 @@ const isInsideClassComponent = (node) => {
55649
55999
  }
55650
56000
  return false;
55651
56001
  };
55652
- const hasShortCircuitAncestor = (descendant, ancestor) => {
55653
- let current = descendant.parent;
55654
- while (current && current !== ancestor) {
55655
- if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
55656
- if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
55657
- if (isNodeOfType(current, "AssignmentPattern") && isWithinRange(descendant, current.right)) return true;
55658
- current = current.parent ?? null;
55659
- }
55660
- return false;
55661
- };
55662
- const isWithinRange = (descendant, sibling) => {
55663
- const descendantSpan = descendant;
55664
- const siblingSpan = sibling;
55665
- if (typeof descendantSpan.start !== "number" || typeof siblingSpan.start !== "number" || typeof siblingSpan.end !== "number") return false;
55666
- return descendantSpan.start >= siblingSpan.start && (descendantSpan.end ?? siblingSpan.end) <= siblingSpan.end;
55667
- };
55668
56002
  const isInsideTry = (descendant, ancestor) => {
55669
56003
  let current = descendant.parent;
55670
56004
  while (current && current !== ancestor) {
@@ -55856,7 +56190,7 @@ const rulesOfHooks = defineRule({
55856
56190
  });
55857
56191
  return;
55858
56192
  }
55859
- if (hasShortCircuitAncestor(node, enclosing.node)) {
56193
+ if (isNodeConditionallyExecuted(node, enclosing.node)) {
55860
56194
  context.report({
55861
56195
  node: node.callee,
55862
56196
  message: buildConditionalMessage(hookName)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.7-dev.e5b0690",
3
+ "version": "0.7.7-dev.ec144d9",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",