oxlint-plugin-react-doctor 0.7.2-dev.2953b25 → 0.7.2-dev.6ae8e46

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
@@ -5595,29 +5595,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
5595
5595
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5596
5596
  readonly create: (context: RuleContext) => RuleVisitors;
5597
5597
  };
5598
- }, {
5599
- readonly key: "react-doctor/no-stale-timer-ref";
5600
- readonly id: "no-stale-timer-ref";
5601
- readonly source: "react-doctor";
5602
- readonly originallyExternal: false;
5603
- readonly rule: {
5604
- readonly framework: "global";
5605
- readonly category: "Bugs";
5606
- readonly requires: readonly Capability[];
5607
- readonly id: string;
5608
- readonly title?: string;
5609
- readonly severity: RuleSeverity;
5610
- readonly disabledWhen?: ReadonlyArray<Capability>;
5611
- readonly tags?: ReadonlyArray<string>;
5612
- readonly matchByOccurrence?: boolean;
5613
- readonly defaultEnabled?: boolean;
5614
- readonly lifecycle?: "retired";
5615
- readonly scan?: FileScan;
5616
- readonly committedFilesOnly?: boolean;
5617
- readonly recommendation?: string;
5618
- readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5619
- readonly create: (context: RuleContext) => RuleVisitors;
5620
- };
5621
5598
  }, {
5622
5599
  readonly key: "react-doctor/no-static-element-interactions";
5623
5600
  readonly id: "no-static-element-interactions";
@@ -14677,29 +14654,6 @@ declare const RULES: readonly [{
14677
14654
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14678
14655
  readonly create: (context: RuleContext) => RuleVisitors;
14679
14656
  };
14680
- }, {
14681
- readonly key: "react-doctor/no-stale-timer-ref";
14682
- readonly id: "no-stale-timer-ref";
14683
- readonly source: "react-doctor";
14684
- readonly originallyExternal: false;
14685
- readonly rule: {
14686
- readonly framework: "global";
14687
- readonly category: "Bugs";
14688
- readonly requires: readonly Capability[];
14689
- readonly id: string;
14690
- readonly title?: string;
14691
- readonly severity: RuleSeverity;
14692
- readonly disabledWhen?: ReadonlyArray<Capability>;
14693
- readonly tags?: ReadonlyArray<string>;
14694
- readonly matchByOccurrence?: boolean;
14695
- readonly defaultEnabled?: boolean;
14696
- readonly lifecycle?: "retired";
14697
- readonly scan?: FileScan;
14698
- readonly committedFilesOnly?: boolean;
14699
- readonly recommendation?: string;
14700
- readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14701
- readonly create: (context: RuleContext) => RuleVisitors;
14702
- };
14703
14657
  }, {
14704
14658
  readonly key: "react-doctor/no-static-element-interactions";
14705
14659
  readonly id: "no-static-element-interactions";
package/dist/index.js CHANGED
@@ -7717,12 +7717,6 @@ const enclosingComponentOrHookName = (node) => {
7717
7717
  return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
7718
7718
  };
7719
7719
  //#endregion
7720
- //#region src/plugin/utils/get-range-start.ts
7721
- const getRangeStart = (node) => {
7722
- const rangeStart = node.range?.[0];
7723
- return typeof rangeStart === "number" ? rangeStart : null;
7724
- };
7725
- //#endregion
7726
7720
  //#region src/plugin/utils/is-result-discarded-call.ts
7727
7721
  const isResultDiscardedCall = (callExpression) => {
7728
7722
  let node = callExpression;
@@ -8009,6 +8003,10 @@ const collectCleanupBindings = (effectCallback) => {
8009
8003
  });
8010
8004
  return bindings;
8011
8005
  };
8006
+ const getRangeStart = (node) => {
8007
+ const rangeStart = node.range?.[0];
8008
+ return typeof rangeStart === "number" ? rangeStart : null;
8009
+ };
8012
8010
  const removeSynchronouslyReleasedUsages = (callback, usages) => {
8013
8011
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8014
8012
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -8982,6 +8980,24 @@ const isAstDescendant = (inner, outer) => {
8982
8980
  return false;
8983
8981
  };
8984
8982
  //#endregion
8983
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
8984
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
8985
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
8986
+ if (importSource === null) return false;
8987
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
8988
+ };
8989
+ //#endregion
8990
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
8991
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
8992
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
8993
+ return Boolean(symbol && symbol.kind !== "import");
8994
+ };
8995
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
8996
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
8997
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
8998
+ return false;
8999
+ };
9000
+ //#endregion
8985
9001
  //#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
8986
9002
  /**
8987
9003
  * Symbol-stability helpers consumed by the `exhaustive-deps` rule.
@@ -8989,8 +9005,8 @@ const isAstDescendant = (inner, outer) => {
8989
9005
  * One cohesive concept: "given a captured symbol, is its value
8990
9006
  * structurally stable across re-renders (and therefore unnecessary
8991
9007
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8992
- * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8993
- * multiple sites — extracting
9008
+ * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
9009
+ * `isRecursiveInitializerCapture` at multiple sites — extracting
8994
9010
  * them lets the rule body stay focused on the diff-the-captured-vs-
8995
9011
  * declared logic.
8996
9012
  *
@@ -9046,6 +9062,12 @@ const symbolHasStableHookOrigin = (symbol) => {
9046
9062
  }
9047
9063
  return false;
9048
9064
  };
9065
+ const symbolHasUseEffectEventOrigin = (symbol, scopes) => {
9066
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
9067
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9068
+ if (getHookName(initializer.callee) !== "useEffectEvent") return false;
9069
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9070
+ };
9049
9071
  const getFunctionValueNode = (symbol) => {
9050
9072
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
9051
9073
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -9131,37 +9153,6 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9131
9153
  };
9132
9154
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9133
9155
  //#endregion
9134
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
9135
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9136
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
9137
- if (importSource === null) return false;
9138
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9139
- };
9140
- //#endregion
9141
- //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9142
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9143
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9144
- return Boolean(symbol && symbol.kind !== "import");
9145
- };
9146
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9147
- if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9148
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9149
- return false;
9150
- };
9151
- //#endregion
9152
- //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9153
- const getUseEffectEventCalleeName = (callee) => {
9154
- if (isNodeOfType(callee, "Identifier")) return callee.name;
9155
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9156
- return null;
9157
- };
9158
- const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9159
- const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9160
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9161
- if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9162
- return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9163
- };
9164
- //#endregion
9165
9156
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
9166
9157
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
9167
9158
  "useEffect",
@@ -9812,7 +9803,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9812
9803
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9813
9804
  if (isNodeOfType(stripped, "Identifier")) {
9814
9805
  const depSymbol = context.scopes.symbolFor(stripped);
9815
- if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9806
+ if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol, context.scopes)) {
9816
9807
  context.report({
9817
9808
  node: elementNode,
9818
9809
  message: buildEffectEventDepMessage()
@@ -25762,45 +25753,18 @@ const collectUseStateBindings = (componentBody) => {
25762
25753
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25763
25754
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25764
25755
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25765
- const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25766
- const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25767
- "assign",
25768
- "entries",
25769
- "fromEntries",
25770
- "keys",
25771
- "values"
25772
- ]);
25773
- const ARRAY_COPY_METHOD_NAMES = new Set([
25774
- "map",
25775
- "filter",
25776
- "slice",
25777
- "concat",
25778
- "flat",
25779
- "flatMap",
25780
- "toSorted",
25781
- "toReversed",
25782
- "toSpliced",
25783
- "with"
25784
- ]);
25785
25756
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25786
25757
  const isPlainDataProducerCall = (expression) => {
25787
25758
  if (!isNodeOfType(expression, "CallExpression")) return false;
25788
25759
  const callee = expression.callee;
25789
25760
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25790
25761
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25791
- if (isNodeOfType(callee.object, "Identifier")) {
25792
- if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25793
- if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25794
- if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25795
- }
25762
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25796
25763
  return producesPlainStateValue(callee.object);
25797
25764
  };
25798
- const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25799
- const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25800
25765
  const producesPlainStateValue = (expression) => {
25801
25766
  const unwrapped = stripParenExpression(expression);
25802
25767
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25803
- if (isPlainDataNewExpression(unwrapped)) return true;
25804
25768
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25805
25769
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25806
25770
  return isPlainDataProducerCall(unwrapped);
@@ -25816,22 +25780,17 @@ const initializerMarksPlainState = (initializerArgument) => {
25816
25780
  return producesPlainStateValue(unwrapped);
25817
25781
  };
25818
25782
  const producesOpaqueInstanceValue = (expression) => {
25819
- if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25820
- if (!isNodeOfType(expression, "CallExpression")) return false;
25821
- const callee = expression.callee;
25822
- if (!isNodeOfType(callee, "MemberExpression")) return false;
25823
- if (isPlainDataProducerCall(expression)) return false;
25824
- if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25825
- return true;
25783
+ if (isNodeOfType(expression, "NewExpression")) return true;
25784
+ return isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "MemberExpression") && !isPlainDataProducerCall(expression);
25826
25785
  };
25827
25786
  const collectSetterValueObservations = (componentBody, setterNames) => {
25828
25787
  const plainFedSetterNames = /* @__PURE__ */ new Set();
25829
25788
  const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25830
- walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25789
+ walkAst(componentBody, (node) => {
25831
25790
  if (!isNodeOfType(node, "CallExpression")) return;
25832
25791
  if (!isNodeOfType(node.callee, "Identifier")) return;
25833
25792
  const setterName = node.callee.name;
25834
- if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25793
+ if (!setterNames.has(setterName)) return;
25835
25794
  const argument = node.arguments?.[0];
25836
25795
  if (!argument) return;
25837
25796
  const unwrapped = stripParenExpression(argument);
@@ -25841,7 +25800,7 @@ const collectSetterValueObservations = (componentBody, setterNames) => {
25841
25800
  return;
25842
25801
  }
25843
25802
  if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25844
- }, true);
25803
+ });
25845
25804
  return {
25846
25805
  plainFedSetterNames,
25847
25806
  opaqueFedSetterNames
@@ -34665,204 +34624,6 @@ const noSideTabBorder = defineRule({
34665
34624
  })
34666
34625
  });
34667
34626
  //#endregion
34668
- //#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
34669
- const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
34670
- "window",
34671
- "globalThis",
34672
- "self"
34673
- ]);
34674
- const NULLISH_COMPARISON_OPERATORS = new Set([
34675
- "==",
34676
- "===",
34677
- "!=",
34678
- "!=="
34679
- ]);
34680
- const getGlobalTimerCalleeName = (node) => {
34681
- if (!isNodeOfType(node, "CallExpression")) return null;
34682
- const callee = stripParenExpression(node.callee);
34683
- if (isNodeOfType(callee, "Identifier")) return callee.name;
34684
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
34685
- const receiver = stripParenExpression(callee.object);
34686
- if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
34687
- }
34688
- return null;
34689
- };
34690
- const getRefCurrentReceiverName = (node) => {
34691
- if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
34692
- if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
34693
- const receiver = stripParenExpression(node.object);
34694
- return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
34695
- };
34696
- const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
34697
- const parseClearCallOnTimerRef = (node) => {
34698
- const clearCalleeName = getGlobalTimerCalleeName(node);
34699
- if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
34700
- if (!isNodeOfType(node, "CallExpression")) return null;
34701
- const clearedArgument = node.arguments?.[0];
34702
- if (!clearedArgument) return null;
34703
- const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
34704
- return refName === null ? null : {
34705
- clearCalleeName,
34706
- refName
34707
- };
34708
- };
34709
- const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
34710
- const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
34711
- const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
34712
- const isClearGuardIfIdiom = (ifStatement, refName) => {
34713
- if (ifStatement.alternate) return false;
34714
- const consequent = ifStatement.consequent;
34715
- return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
34716
- if (!isNodeOfType(statement, "ExpressionStatement")) return false;
34717
- const expression = stripParenExpression(statement.expression);
34718
- if (isClearCallOnRef(expression, refName)) return true;
34719
- return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
34720
- });
34721
- };
34722
- const climbBooleanProjection = (refCurrentMember) => {
34723
- let cursor = refCurrentMember;
34724
- const logicalAncestors = [];
34725
- let didPassBooleanProjection = false;
34726
- while (cursor.parent) {
34727
- const parent = cursor.parent;
34728
- if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
34729
- cursor = parent;
34730
- continue;
34731
- }
34732
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
34733
- didPassBooleanProjection = true;
34734
- cursor = parent;
34735
- continue;
34736
- }
34737
- if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
34738
- didPassBooleanProjection = true;
34739
- cursor = parent;
34740
- continue;
34741
- }
34742
- if (isNodeOfType(parent, "LogicalExpression")) {
34743
- logicalAncestors.push(parent);
34744
- cursor = parent;
34745
- continue;
34746
- }
34747
- break;
34748
- }
34749
- return {
34750
- conditionRoot: cursor,
34751
- logicalAncestors,
34752
- didPassBooleanProjection
34753
- };
34754
- };
34755
- const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
34756
- const isPendingSignalRead = (refCurrentMember, refName) => {
34757
- const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
34758
- const conditionParent = conditionRoot.parent;
34759
- if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
34760
- if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
34761
- if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
34762
- return didPassBooleanProjection;
34763
- };
34764
- const collectTimerRefUsageFacts = (ownerScope, refName) => {
34765
- const facts = {
34766
- holdsScheduledTimerId: false,
34767
- hasPendingSignalRead: false
34768
- };
34769
- walkAst(ownerScope, (child) => {
34770
- if (isAssignmentToRefCurrent(child, refName)) {
34771
- const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
34772
- if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
34773
- return;
34774
- }
34775
- if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
34776
- });
34777
- return facts;
34778
- };
34779
- const isEffectCallbackFunction = (functionNode) => {
34780
- const parent = functionNode.parent;
34781
- if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
34782
- return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
34783
- };
34784
- const doesEffectCallbackReturnName = (effectCallback, name) => {
34785
- if (!isFunctionLike$1(effectCallback)) return false;
34786
- let isReturnedByName = false;
34787
- walkInsideStatementBlocks(effectCallback.body, (child) => {
34788
- if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
34789
- const returned = stripParenExpression(child.argument);
34790
- if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
34791
- });
34792
- return isReturnedByName;
34793
- };
34794
- const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
34795
- if (!isFunctionLike$1(effectCallback)) return false;
34796
- if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
34797
- if (effectCallback.body === functionNode) return true;
34798
- const cleanupBindingName = getFunctionBindingName$1(functionNode);
34799
- return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
34800
- };
34801
- const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
34802
- const cleanupBindingName = getFunctionBindingName$1(functionNode);
34803
- if (cleanupBindingName === null) return false;
34804
- let isReturnedFromEffect = false;
34805
- walkAst(ownerScope, (child) => {
34806
- if (isReturnedFromEffect) return false;
34807
- if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
34808
- const effectCallback = getEffectCallback(child);
34809
- if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
34810
- });
34811
- return isReturnedFromEffect;
34812
- };
34813
- const isInsideEffectCleanupReturn = (node, ownerScope) => {
34814
- let functionNode = findEnclosingFunction(node);
34815
- while (functionNode) {
34816
- const outerFunction = findEnclosingFunction(functionNode);
34817
- if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
34818
- if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
34819
- functionNode = outerFunction;
34820
- }
34821
- return false;
34822
- };
34823
- const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
34824
- const enclosingFunction = findEnclosingFunction(clearCall);
34825
- if (!isFunctionLike$1(enclosingFunction)) return false;
34826
- if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
34827
- const clearStart = getRangeStart(clearCall);
34828
- if (clearStart === null) return false;
34829
- let didFindLaterReassignment = false;
34830
- walkInsideStatementBlocks(enclosingFunction.body, (child) => {
34831
- if (didFindLaterReassignment) return;
34832
- if (!isAssignmentToRefCurrent(child, refName)) return;
34833
- const assignmentStart = getRangeStart(child);
34834
- if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
34835
- });
34836
- return didFindLaterReassignment;
34837
- };
34838
- const isShadowedTimerGlobal = (clearCall) => {
34839
- const callee = stripParenExpression(clearCall.callee);
34840
- if (!isNodeOfType(callee, "Identifier")) return false;
34841
- return findVariableInitializer(callee, callee.name) !== null;
34842
- };
34843
- const noStaleTimerRef = defineRule({
34844
- id: "no-stale-timer-ref",
34845
- title: "Cleared timer ref keeps the stale id",
34846
- severity: "warn",
34847
- recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
34848
- create: (context) => ({ CallExpression(node) {
34849
- const clearCall = parseClearCallOnTimerRef(node);
34850
- if (!clearCall) return;
34851
- if (isShadowedTimerGlobal(node)) return;
34852
- const { clearCalleeName, refName } = clearCall;
34853
- const refBinding = findVariableInitializer(node, refName);
34854
- if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
34855
- const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
34856
- if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
34857
- if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
34858
- if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
34859
- context.report({
34860
- node,
34861
- message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
34862
- });
34863
- } })
34864
- });
34865
- //#endregion
34866
34627
  //#region src/plugin/utils/is-abstract-role.ts
34867
34628
  const isAbstractRole = (openingElement, settings) => {
34868
34629
  const elementType = getElementType(openingElement, settings);
@@ -37627,7 +37388,7 @@ const isRouteFactoryCall = (expression) => {
37627
37388
  }
37628
37389
  return false;
37629
37390
  };
37630
- const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37391
+ const isConfigOnlyFactoryCall = (call) => call.arguments.every((argument) => {
37631
37392
  const expression = skipTsExpression(argument);
37632
37393
  return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37633
37394
  });
@@ -48834,6 +48595,16 @@ const isInsideLoop = (descendant, ancestor) => {
48834
48595
  }
48835
48596
  return false;
48836
48597
  };
48598
+ const isUseEffectEventSymbol = (symbol) => {
48599
+ const initializer = symbol.initializer;
48600
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
48601
+ return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
48602
+ };
48603
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
48604
+ const initializer = symbol.initializer;
48605
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
48606
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
48607
+ };
48837
48608
  const findEnclosingComponentOrHookFunction = (node) => {
48838
48609
  let current = node.parent;
48839
48610
  while (current) {
@@ -49023,7 +48794,8 @@ const rulesOfHooks = defineRule({
49023
48794
  },
49024
48795
  Identifier(node) {
49025
48796
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
49026
- if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
48797
+ if (!symbol || !isUseEffectEventSymbol(symbol)) return;
48798
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
49027
48799
  if (!isSameComponentOrHookScope(symbol, node)) return;
49028
48800
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
49029
48801
  context.report({
@@ -54800,18 +54572,6 @@ const reactDoctorRules = [
54800
54572
  category: "Maintainability"
54801
54573
  }
54802
54574
  },
54803
- {
54804
- key: "react-doctor/no-stale-timer-ref",
54805
- id: "no-stale-timer-ref",
54806
- source: "react-doctor",
54807
- originallyExternal: false,
54808
- rule: {
54809
- ...noStaleTimerRef,
54810
- framework: "global",
54811
- category: "Bugs",
54812
- requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
54813
- }
54814
- },
54815
54575
  {
54816
54576
  key: "react-doctor/no-static-element-interactions",
54817
54577
  id: "no-static-element-interactions",
@@ -57260,7 +57020,6 @@ const CROSS_FILE_RULE_IDS = new Set([
57260
57020
  "nextjs-no-use-search-params-without-suspense",
57261
57021
  "no-dynamic-import-path",
57262
57022
  "no-full-lodash-import",
57263
- "no-locale-format-in-render",
57264
57023
  "no-mutating-reducer-state",
57265
57024
  "prefer-dynamic-import",
57266
57025
  "rendering-hydration-mismatch-time",
@@ -57395,7 +57154,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
57395
57154
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
57396
57155
  ["no-dynamic-import-path", collectNearestManifestDependencies],
57397
57156
  ["no-full-lodash-import", collectNearestManifestDependencies],
57398
- ["no-locale-format-in-render", collectNearestManifestDependencies],
57399
57157
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
57400
57158
  ["prefer-dynamic-import", collectNearestManifestDependencies],
57401
57159
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.2-dev.2953b25",
3
+ "version": "0.7.2-dev.6ae8e46",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",