oxlint-plugin-react-doctor 0.7.4-dev.593824d → 0.7.4-dev.5aa82e8

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
@@ -5296,6 +5296,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
5296
5296
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5297
5297
  readonly create: (context: RuleContext) => RuleVisitors;
5298
5298
  };
5299
+ }, {
5300
+ readonly key: "react-doctor/no-prop-callback-in-render";
5301
+ readonly id: "no-prop-callback-in-render";
5302
+ readonly source: "react-doctor";
5303
+ readonly originallyExternal: false;
5304
+ readonly rule: {
5305
+ readonly framework: "global";
5306
+ readonly category: "Bugs";
5307
+ readonly requires: readonly Capability[];
5308
+ readonly id: string;
5309
+ readonly title?: string;
5310
+ readonly severity: RuleSeverity;
5311
+ readonly disabledWhen?: ReadonlyArray<Capability>;
5312
+ readonly tags?: ReadonlyArray<string>;
5313
+ readonly matchByOccurrence?: boolean;
5314
+ readonly defaultEnabled?: boolean;
5315
+ readonly lifecycle?: "retired";
5316
+ readonly scan?: FileScan;
5317
+ readonly committedFilesOnly?: boolean;
5318
+ readonly recommendation?: string;
5319
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5320
+ readonly create: (context: RuleContext) => RuleVisitors;
5321
+ };
5299
5322
  }, {
5300
5323
  readonly key: "react-doctor/no-prop-types";
5301
5324
  readonly id: "no-prop-types";
@@ -5480,6 +5503,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
5480
5503
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5481
5504
  readonly create: (context: RuleContext) => RuleVisitors;
5482
5505
  };
5506
+ }, {
5507
+ readonly key: "react-doctor/no-ref-current-in-render";
5508
+ readonly id: "no-ref-current-in-render";
5509
+ readonly source: "react-doctor";
5510
+ readonly originallyExternal: false;
5511
+ readonly rule: {
5512
+ readonly framework: "global";
5513
+ readonly category: "Bugs";
5514
+ readonly requires: readonly Capability[];
5515
+ readonly id: string;
5516
+ readonly title?: string;
5517
+ readonly severity: RuleSeverity;
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
+ };
5483
5529
  }, {
5484
5530
  readonly key: "react-doctor/no-render-in-render";
5485
5531
  readonly id: "no-render-in-render";
@@ -14516,6 +14562,29 @@ declare const RULES: readonly [{
14516
14562
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14517
14563
  readonly create: (context: RuleContext) => RuleVisitors;
14518
14564
  };
14565
+ }, {
14566
+ readonly key: "react-doctor/no-prop-callback-in-render";
14567
+ readonly id: "no-prop-callback-in-render";
14568
+ readonly source: "react-doctor";
14569
+ readonly originallyExternal: false;
14570
+ readonly rule: {
14571
+ readonly framework: "global";
14572
+ readonly category: "Bugs";
14573
+ readonly requires: readonly Capability[];
14574
+ readonly id: string;
14575
+ readonly title?: string;
14576
+ readonly severity: RuleSeverity;
14577
+ readonly disabledWhen?: ReadonlyArray<Capability>;
14578
+ readonly tags?: ReadonlyArray<string>;
14579
+ readonly matchByOccurrence?: boolean;
14580
+ readonly defaultEnabled?: boolean;
14581
+ readonly lifecycle?: "retired";
14582
+ readonly scan?: FileScan;
14583
+ readonly committedFilesOnly?: boolean;
14584
+ readonly recommendation?: string;
14585
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14586
+ readonly create: (context: RuleContext) => RuleVisitors;
14587
+ };
14519
14588
  }, {
14520
14589
  readonly key: "react-doctor/no-prop-types";
14521
14590
  readonly id: "no-prop-types";
@@ -14700,6 +14769,29 @@ declare const RULES: readonly [{
14700
14769
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14701
14770
  readonly create: (context: RuleContext) => RuleVisitors;
14702
14771
  };
14772
+ }, {
14773
+ readonly key: "react-doctor/no-ref-current-in-render";
14774
+ readonly id: "no-ref-current-in-render";
14775
+ readonly source: "react-doctor";
14776
+ readonly originallyExternal: false;
14777
+ readonly rule: {
14778
+ readonly framework: "global";
14779
+ readonly category: "Bugs";
14780
+ readonly requires: readonly Capability[];
14781
+ readonly id: string;
14782
+ readonly title?: string;
14783
+ readonly severity: RuleSeverity;
14784
+ readonly disabledWhen?: ReadonlyArray<Capability>;
14785
+ readonly tags?: ReadonlyArray<string>;
14786
+ readonly matchByOccurrence?: boolean;
14787
+ readonly defaultEnabled?: boolean;
14788
+ readonly lifecycle?: "retired";
14789
+ readonly scan?: FileScan;
14790
+ readonly committedFilesOnly?: boolean;
14791
+ readonly recommendation?: string;
14792
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14793
+ readonly create: (context: RuleContext) => RuleVisitors;
14794
+ };
14703
14795
  }, {
14704
14796
  readonly key: "react-doctor/no-render-in-render";
14705
14797
  readonly id: "no-render-in-render";
package/dist/index.js CHANGED
@@ -553,7 +553,7 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
553
553
  "ResizeObserver",
554
554
  "PerformanceObserver"
555
555
  ]);
556
- const STORAGE_OBJECTS$1 = new Set(["localStorage", "sessionStorage"]);
556
+ const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
557
557
  //#endregion
558
558
  //#region src/plugin/constants/react.ts
559
559
  const INDEX_PARAMETER_NAMES = new Set([
@@ -5139,8 +5139,10 @@ const STORAGE_GLOBALS = new Set([
5139
5139
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5140
5140
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5141
5141
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5142
+ const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5142
5143
  const isAuthCredentialKey = (key) => {
5143
5144
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5145
+ if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5144
5146
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
5145
5147
  return true;
5146
5148
  };
@@ -6135,7 +6137,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
6135
6137
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6136
6138
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6137
6139
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6138
- const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6139
6140
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6140
6141
  const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6141
6142
  const resolveStringKey = (keyArg, context) => {
@@ -6158,7 +6159,7 @@ const clientLocalstorageNoVersion = defineRule({
6158
6159
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6159
6160
  const receiver = stripParenExpression(node.callee.object);
6160
6161
  if (!isNodeOfType(receiver, "Identifier")) return;
6161
- if (!STORAGE_OBJECTS.has(receiver.name)) return;
6162
+ if (receiver.name !== "localStorage") return;
6162
6163
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6163
6164
  if (node.callee.property.name !== "setItem") return;
6164
6165
  const keyArg = node.arguments?.[0];
@@ -8938,7 +8939,7 @@ const getListenerAbortControllerKey = (usage, context) => {
8938
8939
  }
8939
8940
  return null;
8940
8941
  };
8941
- const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
8942
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES$2 = new Set([
8942
8943
  "every",
8943
8944
  "filter",
8944
8945
  "flatMap",
@@ -8954,7 +8955,7 @@ const isSynchronousIteratorCallback = (functionNode) => {
8954
8955
  const callee = stripParenExpression(callNode.callee);
8955
8956
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
8956
8957
  if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
8957
- return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
8958
+ return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
8958
8959
  };
8959
8960
  const findDirectCallForReference = (identifier) => {
8960
8961
  const expressionRoot = findTransparentExpressionRoot(identifier);
@@ -10589,6 +10590,19 @@ const isUnstableInitializer = (node) => {
10589
10590
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10590
10591
  return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
10591
10592
  };
10593
+ const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
10594
+ const candidate = unwrapExpression$3(node);
10595
+ if (isUnstableInitializer(candidate)) return true;
10596
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
10597
+ if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
10598
+ if (!isNodeOfType(candidate, "Identifier")) return false;
10599
+ const symbol = scopes.symbolFor(candidate);
10600
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
10601
+ if (symbol.kind === "let" || symbol.kind === "var") return true;
10602
+ if (symbol.kind !== "const" || !symbol.initializer) return false;
10603
+ visitedSymbolIds.add(symbol.id);
10604
+ return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
10605
+ };
10592
10606
  const isExtraDepAllowedForHook = (hookName, node, scopes) => {
10593
10607
  if (!isExtraReactiveDepAllowed(node, scopes)) return false;
10594
10608
  if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
@@ -10602,6 +10616,52 @@ const isStableSetterLikeSymbol = (symbol, scopes) => {
10602
10616
  if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10603
10617
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10604
10618
  };
10619
+ const isConvergingFunctionalUpdater = (node, scopes) => {
10620
+ const updater = unwrapExpression$3(node);
10621
+ if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
10622
+ const previousValueParameter = updater.params?.[0];
10623
+ if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
10624
+ let returnedExpression = updater.body;
10625
+ if (isNodeOfType(updater.body, "BlockStatement")) {
10626
+ returnedExpression = null;
10627
+ for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
10628
+ returnedExpression = statement.argument;
10629
+ break;
10630
+ }
10631
+ }
10632
+ const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
10633
+ if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
10634
+ const test = unwrapExpression$3(conditional.test);
10635
+ if (!isNodeOfType(test, "BinaryExpression") || ![
10636
+ "===",
10637
+ "!==",
10638
+ "==",
10639
+ "!="
10640
+ ].includes(test.operator)) return false;
10641
+ const isPreviousValue = (expression) => {
10642
+ const candidate = unwrapExpression$3(expression);
10643
+ return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
10644
+ };
10645
+ let comparedValue = null;
10646
+ if (isPreviousValue(test.left)) comparedValue = test.right;
10647
+ else if (isPreviousValue(test.right)) comparedValue = test.left;
10648
+ if (!comparedValue) return false;
10649
+ if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
10650
+ const isSameComparedValue = (expression) => {
10651
+ const candidate = unwrapExpression$3(expression);
10652
+ const compared = unwrapExpression$3(comparedValue);
10653
+ if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
10654
+ if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
10655
+ return false;
10656
+ };
10657
+ return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
10658
+ };
10659
+ const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
10660
+ const parent = identifier.parent;
10661
+ if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
10662
+ const writtenValue = parent.arguments?.[0];
10663
+ return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
10664
+ };
10605
10665
  const findStableSetterReference = (node, scopes) => {
10606
10666
  let setterName = null;
10607
10667
  const visit = (current) => {
@@ -10609,7 +10669,7 @@ const findStableSetterReference = (node, scopes) => {
10609
10669
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10610
10670
  if (isNodeOfType(current, "Identifier")) {
10611
10671
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10612
- if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
10672
+ if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
10613
10673
  setterName = symbol.name;
10614
10674
  return;
10615
10675
  }
@@ -13502,7 +13562,7 @@ const jsCacheStorage = defineRule({
13502
13562
  CallExpression(node) {
13503
13563
  if (!isMemberProperty(node.callee, "getItem")) return;
13504
13564
  const receiver = stripParenExpression(node.callee.object);
13505
- if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
13565
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS.has(receiver.name)) return;
13506
13566
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
13507
13567
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
13508
13568
  const storageKey = String(node.arguments[0].value);
@@ -13769,6 +13829,11 @@ const jsEarlyExit = defineRule({
13769
13829
  });
13770
13830
  //#endregion
13771
13831
  //#region src/plugin/rules/js-performance/js-flatmap-filter.ts
13832
+ const BOUNDED_PIPELINE_SOURCE_METHOD_NAMES = new Set(["slice", "split"]);
13833
+ const isBoundedPipelineSource = (node) => {
13834
+ const receiver = stripParenExpression(node);
13835
+ return isNodeOfType(receiver, "CallExpression") && isNodeOfType(receiver.callee, "MemberExpression") && isNodeOfType(receiver.callee.property, "Identifier") && BOUNDED_PIPELINE_SOURCE_METHOD_NAMES.has(receiver.callee.property.name);
13836
+ };
13772
13837
  const jsFlatmapFilter = defineRule({
13773
13838
  id: "js-flatmap-filter",
13774
13839
  title: ".map().filter(Boolean) loops twice",
@@ -13786,6 +13851,7 @@ const jsFlatmapFilter = defineRule({
13786
13851
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
13787
13852
  if (innerCall.callee.property.name !== "map") return;
13788
13853
  const receiver = stripParenExpression(innerCall.callee.object);
13854
+ if (receiver && isBoundedPipelineSource(receiver)) return;
13789
13855
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
13790
13856
  const elements = receiver.elements ?? [];
13791
13857
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -15721,6 +15787,40 @@ const isArrayVariableRenderedAsList = (declarator) => {
15721
15787
  renderedAsListCache.set(declarator, didFindRenderingUse);
15722
15788
  return didFindRenderingUse;
15723
15789
  };
15790
+ const namedCallbackIteratorCallCache = /* @__PURE__ */ new WeakMap();
15791
+ const findNamedCallbackIteratorCall = (functionNode) => {
15792
+ const cached = namedCallbackIteratorCallCache.get(functionNode);
15793
+ if (cached !== void 0) return cached;
15794
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
15795
+ if (!bindingIdentifier) {
15796
+ namedCallbackIteratorCallCache.set(functionNode, null);
15797
+ return null;
15798
+ }
15799
+ const programRoot = findProgramRoot(functionNode);
15800
+ if (!programRoot) {
15801
+ namedCallbackIteratorCallCache.set(functionNode, null);
15802
+ return null;
15803
+ }
15804
+ let iteratorCall = null;
15805
+ walkAst(programRoot, (node) => {
15806
+ if (iteratorCall) return false;
15807
+ if (!isNodeOfType(node, "Identifier") || node === bindingIdentifier || node.name !== bindingIdentifier.name) return;
15808
+ const binding = findVariableInitializer(node, node.name);
15809
+ if (!binding || binding.bindingIdentifier !== bindingIdentifier) return;
15810
+ const callbackExpression = findTransparentExpressionRoot(node);
15811
+ const callExpression = callbackExpression.parent;
15812
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) return;
15813
+ const callee = callExpression.callee;
15814
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !ITERATOR_METHOD_NAMES$1.has(callee.property.name)) return;
15815
+ const callbackIndex = callee.property.name === "from" ? 1 : 0;
15816
+ if (callExpression.arguments[callbackIndex] !== callbackExpression) return;
15817
+ if (isNonChildrenJsxAttributeValue(callExpression)) return;
15818
+ iteratorCall = callExpression;
15819
+ return false;
15820
+ });
15821
+ namedCallbackIteratorCallCache.set(functionNode, iteratorCall);
15822
+ return iteratorCall;
15823
+ };
15724
15824
  const findEnclosingIteratorContext = (jsxNode) => {
15725
15825
  let current = jsxNode;
15726
15826
  let isOutsideContainingFunction = false;
@@ -15735,6 +15835,11 @@ const findEnclosingIteratorContext = (jsxNode) => {
15735
15835
  const grandparent = parent.parent;
15736
15836
  if (grandparent && isNodeOfType(grandparent, "Property")) return null;
15737
15837
  if (isOutsideContainingFunction) return null;
15838
+ const namedCallbackIteratorCall = findNamedCallbackIteratorCall(parent);
15839
+ if (namedCallbackIteratorCall) return {
15840
+ kind: "iterator",
15841
+ callExpression: namedCallbackIteratorCall
15842
+ };
15738
15843
  isOutsideContainingFunction = true;
15739
15844
  } else if (isNodeOfType(parent, "ArrayExpression")) {
15740
15845
  if (isOutsideContainingFunction) return null;
@@ -15778,7 +15883,10 @@ const resolveIterationItemName = (callExpression) => {
15778
15883
  if (!isNodeOfType(callee, "MemberExpression")) return null;
15779
15884
  if (!isNodeOfType(callee.property, "Identifier")) return null;
15780
15885
  const targetArgIndex = callee.property.name === "from" ? 1 : 0;
15781
- const callback = callExpression.arguments[targetArgIndex];
15886
+ const callbackArgument = callExpression.arguments[targetArgIndex];
15887
+ if (!callbackArgument) return null;
15888
+ const unwrappedCallback = stripParenExpression(callbackArgument);
15889
+ const callback = isNodeOfType(unwrappedCallback, "Identifier") ? findVariableInitializer(unwrappedCallback, unwrappedCallback.name)?.initializer : unwrappedCallback;
15782
15890
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
15783
15891
  const firstParam = callback.params[0];
15784
15892
  return firstParam && isNodeOfType(firstParam, "Identifier") ? firstParam.name : null;
@@ -22752,7 +22860,7 @@ const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedL
22752
22860
  };
22753
22861
  //#endregion
22754
22862
  //#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
22755
- const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
22863
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
22756
22864
  "every",
22757
22865
  "filter",
22758
22866
  "find",
@@ -23060,7 +23168,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23060
23168
  const calleeName = getCallCalleeName$1(child);
23061
23169
  const memberName = getStaticMemberName(callee);
23062
23170
  const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
23063
- const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(memberName) && isNodeOfType(callee, "MemberExpression");
23171
+ const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(memberName) && isNodeOfType(callee, "MemberExpression");
23064
23172
  const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
23065
23173
  const callable = resolveWrappedCallable(analysis, callableNode);
23066
23174
  if (!callable || callable.async === true || callable === effectFunction) return;
@@ -23297,10 +23405,15 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
23297
23405
  const definitionNode = definition.node;
23298
23406
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
23299
23407
  }) === true;
23408
+ const getUseRefDeclarator = (analysis, memberExpression) => {
23409
+ if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
23410
+ return getRef(analysis, memberExpression.object)?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && getCallCalleeName$1(definitionNode.init) === "useRef") ?? null;
23411
+ };
23300
23412
  const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
23301
23413
  const node = stripParenExpression(expression);
23302
23414
  const evidence = emptyEvidence();
23303
- if (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode)) {
23415
+ const useRefDeclarator = getUseRefDeclarator(analysis, node);
23416
+ if (!useRefDeclarator && (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode))) {
23304
23417
  evidence.readsExternalValue = true;
23305
23418
  return evidence;
23306
23419
  }
@@ -23342,7 +23455,11 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23342
23455
  evidence.hasUnknownSource = true;
23343
23456
  return evidence;
23344
23457
  }
23345
- if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) {
23458
+ const nonInitializerWrites = reference.resolved.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init);
23459
+ if (nonInitializerWrites.length > 0) {
23460
+ const writtenIdentifier = nonInitializerWrites[0]?.identifier;
23461
+ const assignment = writtenIdentifier.parent;
23462
+ if (nonInitializerWrites.length === 1 && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.operator === "=" && assignment.left === writtenIdentifier) return collectValueEvidence(analysis, assignment.right, frame, remainingCallFrames, visitedBindings);
23346
23463
  evidence.hasUnknownSource = true;
23347
23464
  return evidence;
23348
23465
  }
@@ -23358,6 +23475,40 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23358
23475
  return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
23359
23476
  }
23360
23477
  if (isNodeOfType(node, "MemberExpression")) {
23478
+ if (getStaticMemberName(node) === "current" && isNodeOfType(node.object, "Identifier")) {
23479
+ const refBinding = getRef(analysis, node.object)?.resolved;
23480
+ const refDeclarator = useRefDeclarator;
23481
+ if (refBinding && refDeclarator && isNodeOfType(refDeclarator, "VariableDeclarator") && isNodeOfType(refDeclarator.init, "CallExpression")) {
23482
+ if (visitedBindings.has(refBinding)) {
23483
+ evidence.hasUnknownSource = true;
23484
+ return evidence;
23485
+ }
23486
+ const refVisitedBindings = new Set(visitedBindings);
23487
+ refVisitedBindings.add(refBinding);
23488
+ const initialValue = refDeclarator.init.arguments?.[0];
23489
+ if (initialValue) mergeEvidence(evidence, collectValueEvidence(analysis, initialValue, frame, remainingCallFrames, new Set(refVisitedBindings)));
23490
+ for (const candidateReference of refBinding.references) {
23491
+ if (candidateReference.init) continue;
23492
+ const identifier = candidateReference.identifier;
23493
+ const member = identifier.parent;
23494
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName(member) !== "current") {
23495
+ evidence.hasUnknownSource = true;
23496
+ continue;
23497
+ }
23498
+ const memberParent = member.parent;
23499
+ if (memberParent && isNodeOfType(memberParent, "AssignmentExpression") && memberParent.left === member) {
23500
+ if (memberParent.operator !== "=") {
23501
+ evidence.hasUnknownSource = true;
23502
+ continue;
23503
+ }
23504
+ mergeEvidence(evidence, collectValueEvidence(analysis, memberParent.right, frame, remainingCallFrames, new Set(refVisitedBindings)));
23505
+ continue;
23506
+ }
23507
+ if (memberParent && isNodeOfType(memberParent, "UpdateExpression")) evidence.hasUnknownSource = true;
23508
+ }
23509
+ return evidence;
23510
+ }
23511
+ }
23361
23512
  if (isNodeOfType(node.object, "Identifier")) {
23362
23513
  const objectReference = getRef(analysis, node.object);
23363
23514
  if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
@@ -29136,6 +29287,18 @@ const MUTATING_COLLECTION_METHOD_NAMES = new Set([
29136
29287
  "set",
29137
29288
  "add"
29138
29289
  ]);
29290
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
29291
+ "every",
29292
+ "filter",
29293
+ "find",
29294
+ "findIndex",
29295
+ "flatMap",
29296
+ "forEach",
29297
+ "map",
29298
+ "reduce",
29299
+ "reduceRight",
29300
+ "some"
29301
+ ]);
29139
29302
  const getMemberRootBindingName = (node, scope) => {
29140
29303
  let currentNode = node;
29141
29304
  while (isNodeOfType(currentNode, "MemberExpression")) currentNode = currentNode.object;
@@ -29189,6 +29352,26 @@ const addMutatingCollectionCallDependencies = (graph, expression, scope, eventHa
29189
29352
  addDependencyNames(dependencyNames, controlDependencyNames);
29190
29353
  addDependencies(graph, receiverRootName, dependencyNames);
29191
29354
  };
29355
+ const addIteratorMutationDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
29356
+ if (!isNodeOfType(expression, "CallExpression")) return;
29357
+ if (!isNodeOfType(expression.callee, "MemberExpression")) return;
29358
+ const methodName = getStaticMemberPropertyName(expression.callee);
29359
+ if (!methodName || !SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(methodName)) return;
29360
+ const iteratorDependencyNames = collectScopedReferenceNames(expression.callee.object, scope, eventHandlerReferenceNames);
29361
+ addDependencyNames(iteratorDependencyNames, controlDependencyNames);
29362
+ for (const argument of expression.arguments ?? []) {
29363
+ if (!isNodeOfType(argument, "ArrowFunctionExpression") && !isNodeOfType(argument, "FunctionExpression")) continue;
29364
+ walkAst(argument.body, (node) => {
29365
+ if (node !== argument.body && isFunctionLike$1(node)) return false;
29366
+ if (!isNodeOfType(node, "CallExpression")) return;
29367
+ if (!isNodeOfType(node.callee, "MemberExpression")) return;
29368
+ const nestedMethodName = getStaticMemberPropertyName(node.callee);
29369
+ if (!nestedMethodName || !MUTATING_COLLECTION_METHOD_NAMES.has(nestedMethodName)) return;
29370
+ const receiverRootName = getMemberRootBindingName(node.callee.object, scope);
29371
+ if (receiverRootName) addDependencies(graph, receiverRootName, iteratorDependencyNames);
29372
+ });
29373
+ }
29374
+ };
29192
29375
  const collectExpressionDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
29193
29376
  if (isNodeOfType(expression, "AssignmentExpression")) {
29194
29377
  addAssignmentExpressionDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
@@ -29196,6 +29379,7 @@ const collectExpressionDependencies = (graph, expression, scope, eventHandlerRef
29196
29379
  }
29197
29380
  if (isNodeOfType(expression, "CallExpression")) {
29198
29381
  addMutatingCollectionCallDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
29382
+ addIteratorMutationDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
29199
29383
  return;
29200
29384
  }
29201
29385
  if (isNodeOfType(expression, "SequenceExpression")) {
@@ -30236,28 +30420,26 @@ const noGiantComponent = defineRule({
30236
30420
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
30237
30421
  return lineCount > 300 ? lineCount : null;
30238
30422
  };
30239
- const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30423
+ const reportOversizedComponent = (nameNode, componentName) => {
30240
30424
  context.report({
30241
30425
  node: nameNode,
30242
- message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
30426
+ message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
30243
30427
  });
30244
30428
  };
30245
30429
  return {
30246
30430
  FunctionDeclaration(node) {
30247
30431
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
30248
- const lineCount = getOversizedComponentLineCount(node);
30249
- if (lineCount === null) return;
30432
+ if (getOversizedComponentLineCount(node) === null) return;
30250
30433
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
30251
- reportOversizedComponent(node.id, node.id.name, lineCount);
30434
+ reportOversizedComponent(node.id, node.id.name);
30252
30435
  },
30253
30436
  VariableDeclarator(node) {
30254
30437
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
30255
30438
  const functionNode = unwrapReactHocFunction(node.init);
30256
30439
  if (!functionNode) return;
30257
- const lineCount = getOversizedComponentLineCount(functionNode);
30258
- if (lineCount === null) return;
30440
+ if (getOversizedComponentLineCount(functionNode) === null) return;
30259
30441
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
30260
- reportOversizedComponent(node.id, node.id.name, lineCount);
30442
+ reportOversizedComponent(node.id, node.id.name);
30261
30443
  }
30262
30444
  };
30263
30445
  }
@@ -35028,6 +35210,66 @@ const noPropCallbackInEffect = defineRule({
35028
35210
  }
35029
35211
  });
35030
35212
  //#endregion
35213
+ //#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
35214
+ const isPreservedThroughConciseArrow = (callExpression, scopes) => {
35215
+ let node = callExpression;
35216
+ let parent = node.parent;
35217
+ while (parent) {
35218
+ if (isNodeOfType(parent, "ChainExpression")) {
35219
+ node = parent;
35220
+ parent = node.parent;
35221
+ continue;
35222
+ }
35223
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
35224
+ node = parent;
35225
+ parent = node.parent;
35226
+ continue;
35227
+ }
35228
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
35229
+ node = parent;
35230
+ parent = node.parent;
35231
+ continue;
35232
+ }
35233
+ if (isNodeOfType(parent, "SequenceExpression")) {
35234
+ const expressions = parent.expressions ?? [];
35235
+ if (expressions[expressions.length - 1] !== node) return false;
35236
+ node = parent;
35237
+ parent = node.parent;
35238
+ continue;
35239
+ }
35240
+ if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
35241
+ const invocation = parent.parent;
35242
+ if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
35243
+ if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
35244
+ const callee = stripParenExpression(invocation.callee);
35245
+ return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
35246
+ }
35247
+ node = invocation;
35248
+ parent = node.parent;
35249
+ }
35250
+ return false;
35251
+ };
35252
+ const noPropCallbackInRender = defineRule({
35253
+ id: "no-prop-callback-in-render",
35254
+ title: "Prop callback invoked during render",
35255
+ severity: "error",
35256
+ recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
35257
+ create: (context) => ({ CallExpression(node) {
35258
+ if (!isResultDiscardedCall(node)) return;
35259
+ if (isPreservedThroughConciseArrow(node, context.scopes)) return;
35260
+ if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
35261
+ const analysis = getProgramAnalysis(node);
35262
+ if (!analysis) return;
35263
+ const callee = stripParenExpression(node.callee);
35264
+ if (isFunctionLike$1(callee)) return;
35265
+ if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
35266
+ context.report({
35267
+ node,
35268
+ message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
35269
+ });
35270
+ } })
35271
+ });
35272
+ //#endregion
35031
35273
  //#region src/plugin/rules/architecture/no-prop-types.ts
35032
35274
  const PROP_TYPES_PROPERTY = "propTypes";
35033
35275
  const isPropTypesKey = (key, computed) => {
@@ -35722,6 +35964,63 @@ const noRedundantShouldComponentUpdate = defineRule({
35722
35964
  }
35723
35965
  });
35724
35966
  //#endregion
35967
+ //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
35968
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
35969
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
35970
+ if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
35971
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
35972
+ if (!symbol?.initializer) return null;
35973
+ const initializer = stripParenExpression(symbol.initializer);
35974
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
35975
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
35976
+ };
35977
+ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
35978
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
35979
+ const receiver = stripParenExpression(node.object);
35980
+ return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
35981
+ };
35982
+ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
35983
+ if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
35984
+ let descendant = assignmentExpression;
35985
+ let ancestor = descendant.parent;
35986
+ while (ancestor) {
35987
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
35988
+ const { left, right } = ancestor.test;
35989
+ if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
35990
+ }
35991
+ descendant = ancestor;
35992
+ ancestor = descendant.parent;
35993
+ }
35994
+ return false;
35995
+ };
35996
+ const noRefCurrentInRender = defineRule({
35997
+ id: "no-ref-current-in-render",
35998
+ title: "Ref mutated during render",
35999
+ severity: "error",
36000
+ recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
36001
+ create: (context) => {
36002
+ const report = (memberExpression) => {
36003
+ if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
36004
+ if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
36005
+ context.report({
36006
+ node: memberExpression,
36007
+ message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
36008
+ });
36009
+ };
36010
+ return {
36011
+ AssignmentExpression(node) {
36012
+ const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
36013
+ if (!refSymbol) return;
36014
+ if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
36015
+ report(node.left);
36016
+ },
36017
+ UpdateExpression(node) {
36018
+ report(node.argument);
36019
+ }
36020
+ };
36021
+ }
36022
+ });
36023
+ //#endregion
35725
36024
  //#region src/plugin/rules/architecture/no-render-in-render.ts
35726
36025
  const isInsideComponentContext = (node) => {
35727
36026
  let cursor = node.parent;
@@ -57112,6 +57411,18 @@ const reactDoctorRules = [
57112
57411
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
57113
57412
  }
57114
57413
  },
57414
+ {
57415
+ key: "react-doctor/no-prop-callback-in-render",
57416
+ id: "no-prop-callback-in-render",
57417
+ source: "react-doctor",
57418
+ originallyExternal: false,
57419
+ rule: {
57420
+ ...noPropCallbackInRender,
57421
+ framework: "global",
57422
+ category: "Bugs",
57423
+ requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
57424
+ }
57425
+ },
57115
57426
  {
57116
57427
  key: "react-doctor/no-prop-types",
57117
57428
  id: "no-prop-types",
@@ -57203,6 +57514,18 @@ const reactDoctorRules = [
57203
57514
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
57204
57515
  }
57205
57516
  },
57517
+ {
57518
+ key: "react-doctor/no-ref-current-in-render",
57519
+ id: "no-ref-current-in-render",
57520
+ source: "react-doctor",
57521
+ originallyExternal: false,
57522
+ rule: {
57523
+ ...noRefCurrentInRender,
57524
+ framework: "global",
57525
+ category: "Bugs",
57526
+ requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
57527
+ }
57528
+ },
57206
57529
  {
57207
57530
  key: "react-doctor/no-render-in-render",
57208
57531
  id: "no-render-in-render",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.4-dev.593824d",
3
+ "version": "0.7.4-dev.5aa82e8",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",