oxlint-plugin-react-doctor 0.7.9-dev.21043e0 → 0.7.9-dev.2979a9b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5503,29 +5503,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
5503
5503
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5504
5504
  readonly create: (context: RuleContext) => RuleVisitors;
5505
5505
  };
5506
- }, {
5507
- readonly key: "react-doctor/no-ref-callback-cleanup-before-react-19";
5508
- readonly id: "no-ref-callback-cleanup-before-react-19";
5509
- readonly source: "react-doctor";
5510
- readonly originallyExternal: false;
5511
- readonly rule: {
5512
- readonly framework: "global";
5513
- readonly category: "Bugs";
5514
- readonly id: string;
5515
- readonly title?: string;
5516
- readonly severity: RuleSeverity;
5517
- readonly requires?: ReadonlyArray<Capability>;
5518
- readonly disabledWhen?: ReadonlyArray<Capability>;
5519
- readonly tags?: ReadonlyArray<string>;
5520
- readonly matchByOccurrence?: boolean;
5521
- readonly defaultEnabled?: boolean;
5522
- readonly lifecycle?: "retired";
5523
- readonly scan?: FileScan;
5524
- readonly committedFilesOnly?: boolean;
5525
- readonly recommendation?: string;
5526
- readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5527
- readonly create: (context: RuleContext) => RuleVisitors;
5528
- };
5529
5506
  }, {
5530
5507
  readonly key: "react-doctor/no-ref-current-in-render";
5531
5508
  readonly id: "no-ref-current-in-render";
@@ -14815,29 +14792,6 @@ declare const RULES: readonly [{
14815
14792
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14816
14793
  readonly create: (context: RuleContext) => RuleVisitors;
14817
14794
  };
14818
- }, {
14819
- readonly key: "react-doctor/no-ref-callback-cleanup-before-react-19";
14820
- readonly id: "no-ref-callback-cleanup-before-react-19";
14821
- readonly source: "react-doctor";
14822
- readonly originallyExternal: false;
14823
- readonly rule: {
14824
- readonly framework: "global";
14825
- readonly category: "Bugs";
14826
- readonly id: string;
14827
- readonly title?: string;
14828
- readonly severity: RuleSeverity;
14829
- readonly requires?: ReadonlyArray<Capability>;
14830
- readonly disabledWhen?: ReadonlyArray<Capability>;
14831
- readonly tags?: ReadonlyArray<string>;
14832
- readonly matchByOccurrence?: boolean;
14833
- readonly defaultEnabled?: boolean;
14834
- readonly lifecycle?: "retired";
14835
- readonly scan?: FileScan;
14836
- readonly committedFilesOnly?: boolean;
14837
- readonly recommendation?: string;
14838
- readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14839
- readonly create: (context: RuleContext) => RuleVisitors;
14840
- };
14841
14795
  }, {
14842
14796
  readonly key: "react-doctor/no-ref-current-in-render";
14843
14797
  readonly id: "no-ref-current-in-render";
package/dist/index.js CHANGED
@@ -14947,8 +14947,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14947
14947
  const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
14948
14948
  if (!releaseHandler) return releaseVerbName === "off";
14949
14949
  const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
14950
- const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
14951
- return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
14950
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
14952
14951
  }
14953
14952
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
14954
14953
  return true;
@@ -14973,25 +14972,6 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
14973
14972
  if (!symbol) return false;
14974
14973
  return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
14975
14974
  };
14976
- const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
14977
- if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
14978
- const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
14979
- const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
14980
- if (!isNodeOfType(releaseCall, "CallExpression")) return false;
14981
- const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
14982
- if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
14983
- const ownerFunction = findEnclosingFunction$1(releaseFunction);
14984
- if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
14985
- const triggerRegistrations = [];
14986
- walkAst(ownerFunction.body, (child) => {
14987
- if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
14988
- if (!isNodeOfType(child, "CallExpression")) return;
14989
- const registrationDetails = getCallRegistrationDetails(child, context);
14990
- if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
14991
- });
14992
- if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
14993
- return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
14994
- };
14995
14975
  const isReleaseReachableForUsage = (releaseNode, usage, context) => {
14996
14976
  if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
14997
14977
  const releaseFunction = findEnclosingFunction$1(releaseNode);
@@ -14999,7 +14979,6 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
14999
14979
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
15000
14980
  const usageFunction = findEnclosingFunction$1(usage.node);
15001
14981
  if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
15002
- if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
15003
14982
  return isPotentiallyReachableFunction(releaseFunction, context);
15004
14983
  };
15005
14984
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -17133,7 +17112,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
17133
17112
  keys.add(depKey);
17134
17113
  continue;
17135
17114
  }
17136
- const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
17115
+ const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
17137
17116
  if (identitySourceKeys) {
17138
17117
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
17139
17118
  for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
@@ -17216,161 +17195,6 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
17216
17195
  if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
17217
17196
  return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
17218
17197
  };
17219
- const isPureDerivedExpression = (expression) => {
17220
- const candidate = unwrapExpression$3(expression);
17221
- if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
17222
- if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
17223
- if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
17224
- if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
17225
- if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
17226
- if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
17227
- return false;
17228
- };
17229
- const isPureDerivedStatement = (statement) => {
17230
- if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
17231
- if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
17232
- if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
17233
- return false;
17234
- };
17235
- const isPureDerivedFunction = (functionNode) => {
17236
- if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
17237
- if (functionNode.async || functionNode.generator) return false;
17238
- return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
17239
- };
17240
- const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
17241
- if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
17242
- const referenceRoot = findTransparentExpressionRoot(reference.identifier);
17243
- const callExpression = referenceRoot.parent;
17244
- if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
17245
- const functionNode = getFunctionValueNode(symbol);
17246
- if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
17247
- const sourceKeys = /* @__PURE__ */ new Set();
17248
- for (const capturedReference of closureCaptures(functionNode, scopes)) {
17249
- const capturedSymbol = capturedReference.resolvedSymbol;
17250
- if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
17251
- if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
17252
- const capturedKey = computeDepKey(capturedReference);
17253
- if (!capturedKey) return null;
17254
- if (capturedKey === capturedSymbol.name) {
17255
- const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
17256
- if (nestedSourceKeys) {
17257
- for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
17258
- continue;
17259
- }
17260
- }
17261
- sourceKeys.add(capturedKey);
17262
- }
17263
- return sourceKeys.size > 0 ? sourceKeys : null;
17264
- };
17265
- const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
17266
- const sourceKeys = /* @__PURE__ */ new Set();
17267
- for (const expression of expressions) {
17268
- const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
17269
- if (!expressionSourceKeys) return null;
17270
- for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
17271
- }
17272
- return sourceKeys;
17273
- };
17274
- const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
17275
- const candidate = unwrapExpression$3(expression);
17276
- if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
17277
- if (isNodeOfType(candidate, "Identifier")) {
17278
- if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
17279
- const sourceSymbol = scopes.symbolFor(candidate);
17280
- if (!sourceSymbol) return null;
17281
- if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
17282
- if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
17283
- visitedSymbolIds.add(sourceSymbol.id);
17284
- const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
17285
- visitedSymbolIds.delete(sourceSymbol.id);
17286
- if (sourceKeys) return sourceKeys;
17287
- }
17288
- return new Set([sourceSymbol.name]);
17289
- }
17290
- if (isNodeOfType(candidate, "MemberExpression")) {
17291
- if (hasComputedMemberExpression(candidate)) return null;
17292
- const sourceKey = stringifyMemberChain(candidate);
17293
- const rootIdentifier = getMemberRootIdentifier(candidate);
17294
- const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
17295
- if (!sourceKey || !rootSymbol) return null;
17296
- if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
17297
- return new Set([sourceKey]);
17298
- }
17299
- if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
17300
- if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
17301
- if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
17302
- candidate.test,
17303
- candidate.consequent,
17304
- candidate.alternate
17305
- ], scopes, visitedSymbolIds);
17306
- if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
17307
- if (isNodeOfType(candidate, "NewExpression")) {
17308
- const callee = unwrapExpression$3(candidate.callee);
17309
- if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
17310
- const argumentsToAnalyze = [];
17311
- for (const argument of candidate.arguments) {
17312
- if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
17313
- argumentsToAnalyze.push(argument);
17314
- }
17315
- return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
17316
- }
17317
- return null;
17318
- };
17319
- const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
17320
- const sourceKeys = /* @__PURE__ */ new Set();
17321
- let currentNode = assignment;
17322
- while (currentNode.parent && currentNode.parent !== boundaryFunction) {
17323
- const parentNode = currentNode.parent;
17324
- if (isNodeOfType(parentNode, "IfStatement")) {
17325
- if (parentNode.test === currentNode) return null;
17326
- const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
17327
- if (!testSourceKeys) return null;
17328
- for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
17329
- } else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
17330
- currentNode = parentNode;
17331
- }
17332
- return currentNode.parent === boundaryFunction ? sourceKeys : null;
17333
- };
17334
- const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
17335
- const referenceRoot = findTransparentExpressionRoot(referenceNode);
17336
- const callExpression = referenceRoot.parent;
17337
- return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
17338
- allowGlobalReactNamespace: true,
17339
- allowUnboundBareCalls: true,
17340
- resolveNamedAliases: true
17341
- });
17342
- };
17343
- const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
17344
- if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
17345
- const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
17346
- if (!boundaryFunction) return null;
17347
- const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
17348
- if (!capturingFunction || capturingFunction === boundaryFunction) return null;
17349
- const sourceKeys = /* @__PURE__ */ new Set();
17350
- if (symbol.initializer) {
17351
- const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
17352
- if (!initializerSourceKeys) return null;
17353
- for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
17354
- }
17355
- let writeCount = 0;
17356
- for (const symbolReference of symbol.references) {
17357
- if (symbolReference.flag === "read") {
17358
- if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
17359
- continue;
17360
- }
17361
- if (symbolReference.flag !== "write") return null;
17362
- const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
17363
- const assignment = referenceRoot.parent;
17364
- if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
17365
- const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
17366
- const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
17367
- if (!assignmentSourceKeys || !controlSourceKeys) return null;
17368
- for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
17369
- for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
17370
- writeCount += 1;
17371
- }
17372
- return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
17373
- };
17374
17198
  const isUseCallbackResultDep = (node, scopes) => {
17375
17199
  const rootSymbol = getRootSymbol(node, scopes);
17376
17200
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
@@ -41089,223 +40913,41 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
41089
40913
  if (leftResult === false && rightResult === false) return false;
41090
40914
  return null;
41091
40915
  };
41092
- const readHydrationConditionResult = (expression, context, runtime, state) => {
40916
+ const readHydrationConditionResult = (expression, context, runtime) => {
41093
40917
  const unwrappedExpression = stripParenExpression(expression);
41094
40918
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
41095
40919
  if (predicateMatch) return predicateMatch[`${runtime}Result`];
41096
40920
  const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
41097
40921
  if (staticResult !== null) return staticResult;
41098
- const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
41099
- const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
41100
- if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41101
- state.visitedSymbolIds.add(expressionSymbol.id);
41102
- const result = readHydrationConditionResult(parameterValue, context, runtime, state);
41103
- state.visitedSymbolIds.delete(expressionSymbol.id);
41104
- return result;
41105
- }
41106
- if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41107
- state.visitedSymbolIds.add(expressionSymbol.id);
41108
- const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
41109
- state.visitedSymbolIds.delete(expressionSymbol.id);
41110
- return result;
41111
- }
41112
- if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41113
- const callArguments = unwrappedExpression.arguments ?? [];
41114
- if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41115
- allowGlobalReactNamespace: true,
41116
- resolveNamedAliases: true
41117
- })) {
41118
- const callbackArgument = callArguments[0];
41119
- if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41120
- const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41121
- return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
41122
- }
41123
- const callee = stripParenExpression(unwrappedExpression.callee);
41124
- if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
41125
- const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41126
- if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41127
- const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41128
- for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41129
- const parameter = helperFunction.params[parameterIndex];
41130
- const argument = callArguments[parameterIndex];
41131
- if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41132
- const parameterSymbol = context.scopes.symbolFor(parameter);
41133
- if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41134
- }
41135
- return readHydrationFunctionResult(helperFunction, context, runtime, {
41136
- ...state,
41137
- parameterValuesBySymbolId
41138
- });
41139
- }
41140
40922
  if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
41141
- const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
40923
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
41142
40924
  return argumentResult === null ? null : !argumentResult;
41143
40925
  }
41144
40926
  if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
41145
- return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
40927
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
41146
40928
  };
41147
- const readHydrationStatementResult = (statement, context, runtime, state) => {
41148
- if (isNodeOfType(statement, "ReturnStatement")) return {
41149
- didReturn: true,
41150
- value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
41151
- };
41152
- if (isNodeOfType(statement, "BlockStatement")) {
41153
- for (const childStatement of statement.body) {
41154
- const result = readHydrationStatementResult(childStatement, context, runtime, state);
41155
- if (result.didReturn) return result;
41156
- if (statementAlwaysExits(childStatement)) break;
41157
- }
41158
- return {
41159
- didReturn: false,
41160
- value: null
41161
- };
41162
- }
41163
- if (!isNodeOfType(statement, "IfStatement")) return {
41164
- didReturn: false,
41165
- value: null
41166
- };
41167
- const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
41168
- if (conditionResult !== null) {
41169
- const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
41170
- return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
41171
- didReturn: false,
41172
- value: null
41173
- };
41174
- }
41175
- const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
41176
- const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
41177
- didReturn: false,
41178
- value: null
41179
- };
41180
- return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
41181
- didReturn: consequentResult.didReturn || alternateResult.didReturn,
41182
- value: null
41183
- };
41184
- };
41185
- const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
41186
- if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41187
- state.visitedFunctionNodes.add(functionNode);
41188
- const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
41189
- state.visitedFunctionNodes.delete(functionNode);
41190
- return result;
41191
- };
41192
- const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
41193
- const left = stripParenExpression(leftExpression);
41194
- const right = stripParenExpression(rightExpression);
41195
- if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
41196
- const leftSymbol = scopes.symbolFor(left);
41197
- const rightSymbol = scopes.symbolFor(right);
41198
- return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
41199
- }
41200
- if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
41201
- if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
41202
- const rightArguments = right.arguments ?? [];
41203
- return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
41204
- const rightArgument = rightArguments[index];
41205
- return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
41206
- });
41207
- }
41208
- return true;
41209
- };
41210
- const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
41211
- if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
41212
- const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
41213
- const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
41214
- return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
41215
- };
41216
- const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
41217
- const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
41218
- return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
41219
- };
41220
- const matchHydrationConditionInternal = (expression, context, state) => {
40929
+ const matchHydrationCondition = (expression, context) => {
41221
40930
  const unwrappedExpression = stripParenExpression(expression);
41222
40931
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
41223
40932
  if (predicateMatch) return {
41224
40933
  predicateMatch,
41225
40934
  predicateNode: unwrappedExpression
41226
40935
  };
41227
- if (isNodeOfType(unwrappedExpression, "Identifier")) {
41228
- const symbol = context.scopes.symbolFor(unwrappedExpression);
41229
- const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
41230
- if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
41231
- state.visitedSymbolIds.add(symbol.id);
41232
- const match = matchHydrationConditionInternal(parameterValue, context, state);
41233
- state.visitedSymbolIds.delete(symbol.id);
41234
- return match;
41235
- }
41236
- if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
41237
- state.visitedSymbolIds.add(symbol.id);
41238
- const match = matchHydrationConditionInternal(symbol.initializer, context, state);
41239
- state.visitedSymbolIds.delete(symbol.id);
41240
- return match;
41241
- }
41242
- if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41243
- const callArguments = unwrappedExpression.arguments ?? [];
41244
- if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41245
- allowGlobalReactNamespace: true,
41246
- resolveNamedAliases: true
41247
- })) {
41248
- const callbackArgument = callArguments[0];
41249
- if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41250
- const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41251
- return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
41252
- }
41253
- const callee = stripParenExpression(unwrappedExpression.callee);
41254
- if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
41255
- const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41256
- if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41257
- const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41258
- for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41259
- const parameter = helperFunction.params[parameterIndex];
41260
- const argument = callArguments[parameterIndex];
41261
- if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41262
- const parameterSymbol = context.scopes.symbolFor(parameter);
41263
- if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41264
- }
41265
- return matchHydrationFunctionResult(helperFunction, context, {
41266
- ...state,
41267
- parameterValuesBySymbolId
41268
- });
41269
- }
41270
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
40936
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
41271
40937
  if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
41272
- const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
41273
- const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
40938
+ const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
40939
+ const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
40940
+ if (leftMatch && rightMatch) {
40941
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
40942
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
40943
+ return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
40944
+ }
41274
40945
  const nestedMatch = leftMatch ?? rightMatch;
41275
40946
  if (!nestedMatch) return null;
41276
- const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
41277
- const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
41278
- return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
41279
- };
41280
- const matchHydrationReturningStatement = (statement, context, state) => {
41281
- if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
41282
- if (isNodeOfType(statement, "IfStatement")) {
41283
- const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
41284
- const consequentValues = getReturnedValues(statement.consequent);
41285
- const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
41286
- if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
41287
- return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
41288
- }
41289
- if (!isNodeOfType(statement, "BlockStatement")) return null;
41290
- for (const childStatement of statement.body) {
41291
- const match = matchHydrationReturningStatement(childStatement, context, state);
41292
- if (match) return match;
41293
- if (statementAlwaysExits(childStatement)) break;
41294
- }
41295
- return null;
40947
+ const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
40948
+ if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
40949
+ return nestedMatch;
41296
40950
  };
41297
- const matchHydrationFunctionResult = (functionNode, context, state) => {
41298
- if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41299
- state.visitedFunctionNodes.add(functionNode);
41300
- const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
41301
- state.visitedFunctionNodes.delete(functionNode);
41302
- return match;
41303
- };
41304
- const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
41305
- parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
41306
- visitedFunctionNodes: /* @__PURE__ */ new Set(),
41307
- visitedSymbolIds: /* @__PURE__ */ new Set()
41308
- });
41309
40951
  const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
41310
40952
  const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
41311
40953
  if (!leftNode || !rightNode) return leftNode === rightNode;
@@ -41448,17 +41090,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
41448
41090
  const { predicateMatch, predicateNode } = conditionMatch;
41449
41091
  if (reportedNodes.has(predicateNode)) return;
41450
41092
  if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
41451
- const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
41093
+ const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
41452
41094
  if (!componentOrHookNode) return;
41453
41095
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
41454
- if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
41096
+ if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
41455
41097
  if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
41456
- const attribute = findEnclosingJsxAttribute(conditionNode);
41098
+ const attribute = findEnclosingJsxAttribute(predicateNode);
41457
41099
  if (!attribute || isEventHandlerAttribute(attribute)) return;
41458
41100
  }
41459
- if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
41460
- if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
41461
- const openingElement = findEnclosingJsxOpeningElement(conditionNode);
41101
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
41102
+ if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
41103
+ const openingElement = findEnclosingJsxOpeningElement(predicateNode);
41462
41104
  if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
41463
41105
  if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
41464
41106
  if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
@@ -47444,69 +47086,6 @@ const noRedundantShouldComponentUpdate = defineRule({
47444
47086
  }
47445
47087
  });
47446
47088
  //#endregion
47447
- //#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
47448
- const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47449
- const expression = stripParenExpression(rawExpression);
47450
- if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
47451
- if (isNodeOfType(expression, "ConditionalExpression")) {
47452
- if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
47453
- return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
47454
- }
47455
- if (isNodeOfType(expression, "LogicalExpression")) {
47456
- if (isNodeOfType(expression.left, "Literal")) {
47457
- const isLeftTruthy = Boolean(expression.left.value);
47458
- if (expression.operator === "&&" && !isLeftTruthy) return [];
47459
- if (expression.operator === "||" && isLeftTruthy) return [];
47460
- if (expression.operator === "??" && expression.left.value !== null) return [];
47461
- }
47462
- if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
47463
- return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
47464
- }
47465
- if (isNodeOfType(expression, "SequenceExpression")) {
47466
- const finalExpression = expression.expressions.at(-1);
47467
- return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
47468
- }
47469
- if (isNodeOfType(expression, "CallExpression")) {
47470
- if (!isReactApiCall(expression, "useCallback", scopes)) return [];
47471
- const callback = expression.arguments[0];
47472
- return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
47473
- }
47474
- if (!isNodeOfType(expression, "Identifier")) return [];
47475
- const symbol = scopes.symbolFor(expression);
47476
- if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
47477
- if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
47478
- const initializer = getDirectConstInitializer(symbol);
47479
- if (!initializer) return [];
47480
- return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
47481
- };
47482
- const functionReturnsCleanupFunction = (functionExpression, scopes) => {
47483
- if (!isFunctionLike$1(functionExpression)) return false;
47484
- if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
47485
- return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
47486
- };
47487
- const callbackReturnsCleanupFunction = (callback, scopes) => {
47488
- return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
47489
- };
47490
- const noRefCallbackCleanupBeforeReact19 = defineRule({
47491
- id: "no-ref-callback-cleanup-before-react-19",
47492
- title: "Ref cleanup requires React 19",
47493
- requires: ["react:18"],
47494
- disabledWhen: ["react:19"],
47495
- severity: "warn",
47496
- recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
47497
- create: (context) => ({ JSXAttribute(node) {
47498
- if (getJsxAttributeName(node.name) !== "ref") return;
47499
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
47500
- const callback = node.value.expression;
47501
- if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
47502
- if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
47503
- context.report({
47504
- node,
47505
- message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
47506
- });
47507
- } })
47508
- });
47509
- //#endregion
47510
47089
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
47511
47090
  const REPEATED_ANCESTOR_TYPES = new Set([
47512
47091
  "DoWhileStatement",
@@ -72274,17 +71853,6 @@ const reactDoctorRules = [
72274
71853
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
72275
71854
  }
72276
71855
  },
72277
- {
72278
- key: "react-doctor/no-ref-callback-cleanup-before-react-19",
72279
- id: "no-ref-callback-cleanup-before-react-19",
72280
- source: "react-doctor",
72281
- originallyExternal: false,
72282
- rule: {
72283
- ...noRefCallbackCleanupBeforeReact19,
72284
- framework: "global",
72285
- category: "Bugs"
72286
- }
72287
- },
72288
71856
  {
72289
71857
  key: "react-doctor/no-ref-current-in-render",
72290
71858
  id: "no-ref-current-in-render",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.9-dev.21043e0",
3
+ "version": "0.7.9-dev.2979a9b",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",