oxlint-plugin-react-doctor 0.7.9-dev.9aa98b9 → 0.7.9-dev.b30fb80

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 +236 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6205,6 +6205,23 @@ const asyncDeferAwait = defineRule({
6205
6205
  }
6206
6206
  });
6207
6207
  //#endregion
6208
+ //#region src/plugin/utils/expression-reads-pattern-binding.ts
6209
+ const expressionReadsPatternBinding = (expression, patterns, scopes) => {
6210
+ const bindingSymbolIds = /* @__PURE__ */ new Set();
6211
+ for (const pattern of patterns) walkAst(pattern, (child) => {
6212
+ if (!isNodeOfType(child, "Identifier")) return;
6213
+ const symbol = scopes.symbolFor(child);
6214
+ if (symbol?.bindingIdentifier === child) bindingSymbolIds.add(symbol.id);
6215
+ });
6216
+ let didReadBinding = false;
6217
+ walkAst(expression, (child) => {
6218
+ if (didReadBinding || !isNodeOfType(child, "Identifier")) return;
6219
+ const reference = scopes.referenceFor(child);
6220
+ if (reference?.flag !== "write" && reference?.resolvedSymbol && bindingSymbolIds.has(reference.resolvedSymbol.id)) didReadBinding = true;
6221
+ });
6222
+ return didReadBinding;
6223
+ };
6224
+ //#endregion
6208
6225
  //#region src/plugin/utils/get-callee-identifier-trail.ts
6209
6226
  const getCalleeIdentifierTrail = (call) => {
6210
6227
  let entry = call;
@@ -6310,16 +6327,12 @@ const isInsideTransactionCallback = (node) => {
6310
6327
  return false;
6311
6328
  };
6312
6329
  const reportIfIndependent = (statements, context) => {
6313
- const declaredNames = /* @__PURE__ */ new Set();
6330
+ const declaredPatterns = [];
6314
6331
  for (const statement of statements) {
6315
6332
  const awaitArgument = getAwaitedCall(statement);
6316
6333
  if (!awaitArgument) continue;
6317
- let referencesEarlierResult = false;
6318
- walkAst(awaitArgument, (child) => {
6319
- if (isNodeOfType(child, "Identifier") && declaredNames.has(child.name)) referencesEarlierResult = true;
6320
- });
6321
- if (referencesEarlierResult) return;
6322
- if (isNodeOfType(statement, "VariableDeclaration") && statement.declarations[0]?.id) collectPatternNames(statement.declarations[0].id, declaredNames);
6334
+ if (expressionReadsPatternBinding(awaitArgument, declaredPatterns, context.scopes)) return;
6335
+ if (isNodeOfType(statement, "VariableDeclaration") && statement.declarations[0]?.id) declaredPatterns.push(statement.declarations[0].id);
6323
6336
  }
6324
6337
  context.report({
6325
6338
  node: statements[0],
@@ -8463,10 +8476,21 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
8463
8476
  };
8464
8477
  const isReactApiCall = (node, apiNames, scopes, options = {}) => {
8465
8478
  if (!isNodeOfType(node, "CallExpression")) return false;
8466
- const callee = stripParenExpression(node.callee);
8479
+ return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
8480
+ };
8481
+ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
8482
+ const callee = stripParenExpression(rawCallee);
8483
+ 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));
8467
8484
  if (isNodeOfType(callee, "Identifier")) {
8468
8485
  if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
8469
8486
  if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
8487
+ if (options.resolveConditionalAliases) {
8488
+ const symbol = scopes.symbolFor(callee);
8489
+ if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
8490
+ visitedSymbolIds.add(symbol.id);
8491
+ return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
8492
+ }
8493
+ }
8470
8494
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
8471
8495
  }
8472
8496
  if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
@@ -12323,6 +12347,10 @@ const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
12323
12347
  };
12324
12348
  const executesDuringRender = (functionNode, scopes) => {
12325
12349
  const parent = functionNode.parent;
12350
+ if (isNodeOfType(parent, "NewExpression")) {
12351
+ const callee = stripParenExpression(parent.callee);
12352
+ return Boolean(scopes && parent.arguments?.[0] === functionNode && isNodeOfType(callee, "Identifier") && callee.name === "Promise" && scopes.isGlobalReference(callee));
12353
+ }
12326
12354
  if (!isNodeOfType(parent, "CallExpression")) return false;
12327
12355
  if (parent.callee === functionNode) return true;
12328
12356
  if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
@@ -29812,18 +29840,143 @@ const ARITHMETIC_KEY_OPERATORS = new Set([
29812
29840
  "%"
29813
29841
  ]);
29814
29842
  const bindingMutationResultsByProgram = /* @__PURE__ */ new WeakMap();
29815
- const extractCandidateIdentifiers = (expression) => {
29843
+ const UNKNOWN_STATIC_KEY_BRANCH_VALUE = {
29844
+ isNullish: null,
29845
+ isTruthy: null
29846
+ };
29847
+ const mergeStaticKeyBranchValues = (firstValue, secondValue) => ({
29848
+ isNullish: firstValue.isNullish === secondValue.isNullish ? firstValue.isNullish : null,
29849
+ isTruthy: firstValue.isTruthy === secondValue.isTruthy ? firstValue.isTruthy : null
29850
+ });
29851
+ const readStaticKeyBranchValue = (expression, depth) => {
29852
+ if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return null;
29853
+ const node = stripParenExpression(expression);
29854
+ if (isNodeOfType(node, "Literal")) return {
29855
+ isNullish: node.value === null,
29856
+ isTruthy: Boolean(node.value)
29857
+ };
29858
+ if (isNodeOfType(node, "ArrayExpression") || isNodeOfType(node, "ObjectExpression") || isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ClassExpression") || isNodeOfType(node, "NewExpression")) return {
29859
+ isNullish: false,
29860
+ isTruthy: true
29861
+ };
29862
+ if (isNodeOfType(node, "TemplateLiteral")) return {
29863
+ isNullish: false,
29864
+ isTruthy: (node.quasis ?? []).some((quasi) => typeof quasi.value?.cooked === "string" && quasi.value.cooked.length > 0) ? true : null
29865
+ };
29866
+ if (isNodeOfType(node, "BinaryExpression")) return {
29867
+ isNullish: false,
29868
+ isTruthy: null
29869
+ };
29870
+ if (isNodeOfType(node, "Identifier")) {
29871
+ const binding = findVariableInitializer(node, node.name);
29872
+ if (!binding) {
29873
+ if (node.name === "undefined") return {
29874
+ isNullish: true,
29875
+ isTruthy: false
29876
+ };
29877
+ if (node.name === "NaN") return {
29878
+ isNullish: false,
29879
+ isTruthy: false
29880
+ };
29881
+ if (node.name === "Infinity") return {
29882
+ isNullish: false,
29883
+ isTruthy: true
29884
+ };
29885
+ return null;
29886
+ }
29887
+ if (!isConstDeclaredBinding(binding) || !binding.initializer) return null;
29888
+ const declarator = binding.bindingIdentifier.parent;
29889
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.id !== binding.bindingIdentifier || declarator.init !== binding.initializer) return null;
29890
+ return readStaticKeyBranchValue(binding.initializer, depth + 1);
29891
+ }
29892
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
29893
+ if (!Boolean(findVariableInitializer(node.callee, node.callee.name)) && STRING_COERCION_FUNCTIONS.has(node.callee.name)) return {
29894
+ isNullish: false,
29895
+ isTruthy: null
29896
+ };
29897
+ }
29898
+ if (isNodeOfType(node, "UnaryExpression")) {
29899
+ if (node.operator === "void") return {
29900
+ isNullish: true,
29901
+ isTruthy: false
29902
+ };
29903
+ if (node.operator === "typeof") return {
29904
+ isNullish: false,
29905
+ isTruthy: true
29906
+ };
29907
+ if (node.operator === "+" || node.operator === "-" || node.operator === "~") return {
29908
+ isNullish: false,
29909
+ isTruthy: null
29910
+ };
29911
+ if (node.operator !== "!") return null;
29912
+ const argumentValue = readStaticKeyBranchValue(node.argument, depth + 1);
29913
+ if (!argumentValue || argumentValue.isTruthy === null) return null;
29914
+ return {
29915
+ isNullish: false,
29916
+ isTruthy: !argumentValue.isTruthy
29917
+ };
29918
+ }
29919
+ if (isNodeOfType(node, "SequenceExpression")) {
29920
+ const finalExpression = node.expressions.at(-1);
29921
+ return finalExpression ? readStaticKeyBranchValue(finalExpression, depth + 1) : null;
29922
+ }
29923
+ if (isNodeOfType(node, "LogicalExpression")) {
29924
+ const leftValue = readStaticKeyBranchValue(node.left, depth + 1) ?? UNKNOWN_STATIC_KEY_BRANCH_VALUE;
29925
+ if (node.operator === "&&" && leftValue.isTruthy !== null) return leftValue.isTruthy ? readStaticKeyBranchValue(node.right, depth + 1) : leftValue;
29926
+ if (node.operator === "||" && leftValue.isTruthy !== null) return leftValue.isTruthy ? leftValue : readStaticKeyBranchValue(node.right, depth + 1);
29927
+ if (node.operator === "??" && leftValue.isNullish !== null) return leftValue.isNullish ? readStaticKeyBranchValue(node.right, depth + 1) : leftValue;
29928
+ const rightValue = readStaticKeyBranchValue(node.right, depth + 1) ?? UNKNOWN_STATIC_KEY_BRANCH_VALUE;
29929
+ if (node.operator === "||") return {
29930
+ isNullish: rightValue.isNullish === false ? false : null,
29931
+ isTruthy: rightValue.isTruthy === true ? true : null
29932
+ };
29933
+ if (node.operator === "&&") return {
29934
+ isNullish: leftValue.isNullish === false && rightValue.isNullish === false ? false : null,
29935
+ isTruthy: rightValue.isTruthy === false ? false : null
29936
+ };
29937
+ return {
29938
+ isNullish: rightValue.isNullish === false ? false : null,
29939
+ isTruthy: leftValue.isTruthy !== null && leftValue.isTruthy === rightValue.isTruthy ? leftValue.isTruthy : null
29940
+ };
29941
+ }
29942
+ if (isNodeOfType(node, "ConditionalExpression")) {
29943
+ const testValue = readStaticKeyBranchValue(node.test, depth + 1);
29944
+ if (testValue?.isTruthy !== null && testValue?.isTruthy !== void 0) return readStaticKeyBranchValue(testValue.isTruthy ? node.consequent : node.alternate, depth + 1);
29945
+ return mergeStaticKeyBranchValues(readStaticKeyBranchValue(node.consequent, depth + 1) ?? UNKNOWN_STATIC_KEY_BRANCH_VALUE, readStaticKeyBranchValue(node.alternate, depth + 1) ?? UNKNOWN_STATIC_KEY_BRANCH_VALUE);
29946
+ }
29947
+ return null;
29948
+ };
29949
+ const extractCandidateIdentifiers = (expression, depth = 0) => {
29950
+ if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return [];
29816
29951
  const node = stripParenExpression(expression);
29817
29952
  if (isNodeOfType(node, "Identifier")) return [node];
29818
- if (isNodeOfType(node, "UnaryExpression") && (node.operator === "-" || node.operator === "+" || node.operator === "~") && isNodeOfType(node.argument, "Identifier")) return [node.argument];
29953
+ if (isNodeOfType(node, "UnaryExpression") && (node.operator === "-" || node.operator === "+" || node.operator === "~") && node.argument) return extractCandidateIdentifiers(node.argument, depth + 1);
29819
29954
  if (isNodeOfType(node, "TemplateLiteral")) {
29820
29955
  const identifiers = [];
29821
- for (const templateExpression of node.expressions ?? []) if (isNodeOfType(templateExpression, "Identifier")) identifiers.push(templateExpression);
29956
+ for (const templateExpression of node.expressions ?? []) identifiers.push(...extractCandidateIdentifiers(templateExpression, depth + 1));
29822
29957
  return identifiers;
29823
29958
  }
29959
+ if (isNodeOfType(node, "LogicalExpression")) {
29960
+ const leftValue = readStaticKeyBranchValue(node.left, 0);
29961
+ if (leftValue) {
29962
+ if (node.operator === "&&" && leftValue.isTruthy !== null) return extractCandidateIdentifiers(leftValue.isTruthy ? node.right : node.left, depth + 1);
29963
+ if (node.operator === "||" && leftValue.isTruthy !== null) return extractCandidateIdentifiers(leftValue.isTruthy ? node.left : node.right, depth + 1);
29964
+ if (node.operator === "??" && leftValue.isNullish !== null) return extractCandidateIdentifiers(leftValue.isNullish ? node.right : node.left, depth + 1);
29965
+ }
29966
+ return [...extractCandidateIdentifiers(node.left, depth + 1), ...extractCandidateIdentifiers(node.right, depth + 1)];
29967
+ }
29968
+ if (isNodeOfType(node, "ConditionalExpression")) {
29969
+ const testValue = readStaticKeyBranchValue(node.test, 0);
29970
+ if (testValue && testValue.isTruthy !== null) return extractCandidateIdentifiers(testValue.isTruthy ? node.consequent : node.alternate, depth + 1);
29971
+ return [...extractCandidateIdentifiers(node.consequent, depth + 1), ...extractCandidateIdentifiers(node.alternate, depth + 1)];
29972
+ }
29973
+ if (isNodeOfType(node, "SequenceExpression")) {
29974
+ const finalExpression = node.expressions.at(-1);
29975
+ return finalExpression ? extractCandidateIdentifiers(finalExpression, depth + 1) : [];
29976
+ }
29824
29977
  if (isNodeOfType(node, "CallExpression")) {
29825
- if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "toString") return [node.callee.object];
29826
- if (isNodeOfType(node.callee, "Identifier") && STRING_COERCION_FUNCTIONS.has(node.callee.name) && isNodeOfType(node.arguments?.[0], "Identifier")) return [node.arguments[0]];
29978
+ if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "toString") return extractCandidateIdentifiers(node.callee.object, depth + 1);
29979
+ if (isNodeOfType(node.callee, "Identifier") && STRING_COERCION_FUNCTIONS.has(node.callee.name) && !findVariableInitializer(node.callee, node.callee.name) && node.arguments?.[0]) return extractCandidateIdentifiers(node.arguments[0], depth + 1);
29827
29980
  return [];
29828
29981
  }
29829
29982
  if (isNodeOfType(node, "BinaryExpression")) {
@@ -31976,6 +32129,7 @@ const createStateTriggerReachability = ({ analysis, context, effectFunction }) =
31976
32129
  };
31977
32130
  //#endregion
31978
32131
  //#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
32132
+ const APPLICABLE_EFFECT_HOOK_NAMES = new Set(["useEffect", "useLayoutEffect"]);
31979
32133
  const getUseStateDeclarator = (ref) => (ref.resolved?.defs ?? []).map((def) => def.node).find((node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "ArrayPattern")) ?? null;
31980
32134
  const isDeclaredWithin = (node, container) => {
31981
32135
  let walker = node;
@@ -32027,7 +32181,12 @@ const noChainStateUpdates = defineRule({
32027
32181
  tags: ["test-noise"],
32028
32182
  recommendation: "Set all the related state together in the event handler that starts it, instead of having one useEffect react to a state change and set more state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations",
32029
32183
  create: (context) => ({ CallExpression(node) {
32030
- if (!isUseEffect(node)) return;
32184
+ if (!isReactApiCall(node, APPLICABLE_EFFECT_HOOK_NAMES, context.scopes, {
32185
+ allowGlobalReactNamespace: true,
32186
+ allowUnboundBareCalls: true,
32187
+ resolveConditionalAliases: true,
32188
+ resolveNamedAliases: true
32189
+ })) return;
32031
32190
  const analysis = getProgramAnalysis(node);
32032
32191
  if (!analysis) return;
32033
32192
  if (hasCleanup(analysis, node)) return;
@@ -42005,7 +42164,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
42005
42164
  variables
42006
42165
  };
42007
42166
  };
42008
- const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
42167
+ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall, isReactUseEffectCall) => {
42009
42168
  const callee = stripParenExpression(callExpression.callee);
42010
42169
  if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
42011
42170
  const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
@@ -42013,7 +42172,9 @@ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactU
42013
42172
  const { refCall, variables } = bindingProvenance;
42014
42173
  const componentFunction = findEnclosingFunction$1(effectCall);
42015
42174
  if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
42016
- if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
42175
+ const notificationEffectStatement = getDirectComponentBodyStatement(effectCall, componentFunction.body);
42176
+ const notificationEffectRoot = findTransparentExpressionRoot(effectCall);
42177
+ if (!notificationEffectStatement || !isNodeOfType(notificationEffectStatement, "ExpressionStatement") || notificationEffectRoot.parent !== notificationEffectStatement || !isReactUseEffectCall(effectCall)) return null;
42017
42178
  const callbackPropNames = /* @__PURE__ */ new Set();
42018
42179
  const initializer = refCall.arguments?.[0];
42019
42180
  const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
@@ -42031,9 +42192,20 @@ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactU
42031
42192
  const assignment = getRefMemberAssignment(identifier);
42032
42193
  if (!assignment) continue;
42033
42194
  const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
42034
- if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
42195
+ if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement")) return null;
42035
42196
  const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
42036
42197
  if (!assignedCallbackName) return null;
42198
+ if (assignmentStatement.parent !== componentFunction.body) {
42199
+ if (!initializerCallbackName || assignedCallbackName !== initializerCallbackName) return null;
42200
+ const assignmentEffectBody = assignmentStatement.parent;
42201
+ if (!assignmentEffectBody || !isNodeOfType(assignmentEffectBody, "BlockStatement")) return null;
42202
+ const assignmentEffectFunction = assignmentEffectBody.parent;
42203
+ if (!assignmentEffectFunction || !isFunctionLike$1(assignmentEffectFunction) || assignmentEffectFunction.body !== assignmentEffectBody) return null;
42204
+ const assignmentEffectCall = findTransparentExpressionRoot(assignmentEffectFunction).parent;
42205
+ if (!assignmentEffectCall || !isNodeOfType(assignmentEffectCall, "CallExpression") || !isReactUseEffectCall(assignmentEffectCall) || getEffectFn(analysis, assignmentEffectCall) !== assignmentEffectFunction) return null;
42206
+ const assignmentEffectStatement = getDirectComponentBodyStatement(assignmentEffectCall, componentFunction.body);
42207
+ if (!assignmentEffectStatement || !isNodeOfType(assignmentEffectStatement, "ExpressionStatement") || assignmentEffectCall.parent !== assignmentEffectStatement || assignmentEffectStatement.range[0] >= notificationEffectStatement.range[0]) return null;
42208
+ }
42037
42209
  callbackPropNames.add(assignedCallbackName);
42038
42210
  }
42039
42211
  return callbackPropNames.size > 0 ? { callbackPropNames } : null;
@@ -42246,6 +42418,10 @@ const noPassDataToParent = defineRule({
42246
42418
  allowGlobalReactNamespace: true,
42247
42419
  allowUnboundBareCalls: true
42248
42420
  });
42421
+ const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
42422
+ allowGlobalReactNamespace: true,
42423
+ allowUnboundBareCalls: true
42424
+ });
42249
42425
  return { CallExpression(node) {
42250
42426
  if (!isUseEffect(node)) return;
42251
42427
  const analysis = getProgramAnalysis(node);
@@ -42258,7 +42434,7 @@ const noPassDataToParent = defineRule({
42258
42434
  for (const ref of effectFnRefs) {
42259
42435
  const callExpr = getCallExpr(ref);
42260
42436
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
42261
- const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
42437
+ const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
42262
42438
  if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
42263
42439
  if (!isSynchronous(ref.identifier, effectFn)) continue;
42264
42440
  const calleeNode = unwrapChainExpression(callExpr.callee);
@@ -43820,9 +43996,9 @@ const isInsideComponentContext = (node) => {
43820
43996
  }
43821
43997
  return false;
43822
43998
  };
43823
- const functionBodyOf = (node) => {
43824
- if (isFunctionLike$1(node)) return node.body ?? null;
43825
- if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
43999
+ const getFunctionFromDeclaration = (node) => {
44000
+ if (isFunctionLike$1(node)) return node;
44001
+ if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init;
43826
44002
  return null;
43827
44003
  };
43828
44004
  const isHookCallee = (callee) => {
@@ -43830,23 +44006,43 @@ const isHookCallee = (callee) => {
43830
44006
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
43831
44007
  return false;
43832
44008
  };
43833
- const containsHookCall = (body) => {
43834
- let found = false;
43835
- walkAst(body, (child) => {
43836
- if (found) return false;
43837
- if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
43838
- if (!isNodeOfType(child, "CallExpression")) return;
43839
- if (isHookCallee(child.callee)) found = true;
44009
+ const containsReachableHookCall = (functionNode, rootFunction, context, visitedFunctions) => {
44010
+ if (!isFunctionLike$1(functionNode) || visitedFunctions.has(functionNode)) return false;
44011
+ visitedFunctions.add(functionNode);
44012
+ let didFindReachableHook = false;
44013
+ walkAst(functionNode.body, (child) => {
44014
+ if (didFindReachableHook) return false;
44015
+ if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
44016
+ if (!isNodeOfType(child, "CallExpression") && !isNodeOfType(child, "NewExpression")) return;
44017
+ if (isNodeOfType(child, "CallExpression")) {
44018
+ if (isHookCallee(child.callee)) {
44019
+ didFindReachableHook = true;
44020
+ return false;
44021
+ }
44022
+ const calledFunction = resolveExactLocalFunction(child.callee, context.scopes);
44023
+ if (calledFunction && isAstDescendant(calledFunction, rootFunction) && containsReachableHookCall(calledFunction, rootFunction, context, visitedFunctions)) {
44024
+ didFindReachableHook = true;
44025
+ return false;
44026
+ }
44027
+ }
44028
+ for (const callArgument of child.arguments ?? []) {
44029
+ if (!executesDuringRender(callArgument, context.scopes)) continue;
44030
+ const callbackFunction = resolveExactLocalFunction(callArgument, context.scopes);
44031
+ if (callbackFunction && isAstDescendant(callbackFunction, rootFunction) && containsReachableHookCall(callbackFunction, rootFunction, context, visitedFunctions)) {
44032
+ didFindReachableHook = true;
44033
+ return false;
44034
+ }
44035
+ }
43840
44036
  });
43841
- return found;
44037
+ return didFindReachableHook;
43842
44038
  };
43843
- const isHookCallingRenderHelper = (symbol) => {
44039
+ const isHookCallingRenderHelper = (symbol, context) => {
43844
44040
  if (!symbol) return false;
43845
44041
  const declaration = symbol.declarationNode;
43846
44042
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
43847
- const body = functionBodyOf(declaration);
43848
- if (!body) return false;
43849
- return containsHookCall(body);
44043
+ const functionNode = getFunctionFromDeclaration(declaration);
44044
+ if (!functionNode) return false;
44045
+ return containsReachableHookCall(functionNode, functionNode, context, /* @__PURE__ */ new Set());
43850
44046
  };
43851
44047
  const noRenderInRender = defineRule({
43852
44048
  id: "no-render-in-render",
@@ -43861,7 +44057,7 @@ const noRenderInRender = defineRule({
43861
44057
  const calleeName = expression.callee.name;
43862
44058
  if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
43863
44059
  if (!isInsideComponentContext(node)) return;
43864
- if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
44060
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee), context)) return;
43865
44061
  context.report({
43866
44062
  node: expression,
43867
44063
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -62844,29 +63040,19 @@ const serverNoMutableModuleState = defineRule({
62844
63040
  });
62845
63041
  //#endregion
62846
63042
  //#region src/plugin/rules/server/server-sequential-independent-await.ts
62847
- const collectDeclaredNames = (declaration) => {
62848
- const names = /* @__PURE__ */ new Set();
62849
- if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
62850
- for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
62851
- return names;
62852
- };
62853
63043
  const declarationStartsWithAwait = (declaration) => {
62854
63044
  if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
62855
63045
  for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.init, "AwaitExpression")) return true;
62856
63046
  return false;
62857
63047
  };
62858
- const declarationReadsAnyName = (declaration, names) => {
62859
- if (names.size === 0) return false;
63048
+ const declarationReadsAnyPatternBinding = (declaration, patterns, context) => {
63049
+ if (patterns.length === 0) return false;
62860
63050
  if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
62861
- let didRead = false;
62862
63051
  for (const declarator of declaration.declarations ?? []) {
62863
63052
  if (!declarator.init) continue;
62864
- walkAst(declarator.init, (child) => {
62865
- if (didRead) return;
62866
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
62867
- });
63053
+ if (expressionReadsPatternBinding(declarator.init, patterns, context.scopes)) return true;
62868
63054
  }
62869
- return didRead;
63055
+ return false;
62870
63056
  };
62871
63057
  const GATE_LEADING_VERBS = new Set([
62872
63058
  "require",
@@ -62940,11 +63126,11 @@ const serverSequentialIndependentAwait = defineRule({
62940
63126
  const currentStatement = statements[statementIndex];
62941
63127
  if (!isNodeOfType(currentStatement, "VariableDeclaration")) continue;
62942
63128
  if (!declarationStartsWithAwait(currentStatement)) continue;
62943
- const declaredNames = collectDeclaredNames(currentStatement);
63129
+ const declaredPatterns = currentStatement.declarations.map((declarator) => declarator.id);
62944
63130
  const nextStatement = statements[statementIndex + 1];
62945
63131
  if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
62946
63132
  if (!declarationStartsWithAwait(nextStatement)) continue;
62947
- if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
63133
+ if (declarationReadsAnyPatternBinding(nextStatement, declaredPatterns, context)) continue;
62948
63134
  if (declarationAwaitsExistingPromise(nextStatement)) continue;
62949
63135
  if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
62950
63136
  if (declarationAwaitsGate(currentStatement, context)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.9-dev.9aa98b9",
3
+ "version": "0.7.9-dev.b30fb80",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",