oxlint-plugin-react-doctor 0.7.4-dev.5113067 → 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;
@@ -22370,6 +22478,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22370
22478
  const importDeclaration = declarationNode.parent;
22371
22479
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22372
22480
  }));
22481
+ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22482
+ if (def.type !== "ImportBinding") return false;
22483
+ const declarationNode = def.node;
22484
+ if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22485
+ const importDeclaration = declarationNode.parent;
22486
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22487
+ }));
22488
+ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22489
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22490
+ const callee = stripParenExpression(declarator.init.callee);
22491
+ if (isNodeOfType(callee, "Identifier")) {
22492
+ const reference = getRef(analysis, callee);
22493
+ if (!reference?.resolved) return callee.name === hookName;
22494
+ return isReactNamedImportReference(reference, hookName);
22495
+ }
22496
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22497
+ const namespaceReference = getRef(analysis, callee.object);
22498
+ if (!namespaceReference?.resolved) return callee.object.name === "React";
22499
+ return isReactNamespaceImportReference(namespaceReference);
22500
+ };
22373
22501
  const isHookCallee$1 = (analysis, node, hookName) => {
22374
22502
  if (!node) return false;
22375
22503
  if (isNodeOfType(node, "Identifier")) {
@@ -22732,7 +22860,7 @@ const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedL
22732
22860
  };
22733
22861
  //#endregion
22734
22862
  //#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
22735
- const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
22863
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
22736
22864
  "every",
22737
22865
  "filter",
22738
22866
  "find",
@@ -23040,7 +23168,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23040
23168
  const calleeName = getCallCalleeName$1(child);
23041
23169
  const memberName = getStaticMemberName(callee);
23042
23170
  const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
23043
- 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");
23044
23172
  const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
23045
23173
  const callable = resolveWrappedCallable(analysis, callableNode);
23046
23174
  if (!callable || callable.async === true || callable === effectFunction) return;
@@ -23277,10 +23405,15 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
23277
23405
  const definitionNode = definition.node;
23278
23406
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
23279
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
+ };
23280
23412
  const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
23281
23413
  const node = stripParenExpression(expression);
23282
23414
  const evidence = emptyEvidence();
23283
- if (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode)) {
23415
+ const useRefDeclarator = getUseRefDeclarator(analysis, node);
23416
+ if (!useRefDeclarator && (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode))) {
23284
23417
  evidence.readsExternalValue = true;
23285
23418
  return evidence;
23286
23419
  }
@@ -23322,7 +23455,11 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23322
23455
  evidence.hasUnknownSource = true;
23323
23456
  return evidence;
23324
23457
  }
23325
- 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);
23326
23463
  evidence.hasUnknownSource = true;
23327
23464
  return evidence;
23328
23465
  }
@@ -23338,6 +23475,40 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23338
23475
  return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
23339
23476
  }
23340
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
+ }
23341
23512
  if (isNodeOfType(node.object, "Identifier")) {
23342
23513
  const objectReference = getRef(analysis, node.object);
23343
23514
  if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
@@ -23424,6 +23595,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23424
23595
  }
23425
23596
  return evidence;
23426
23597
  };
23598
+ const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23599
+ const evidence = collectValueEvidence(analysis, expression, {
23600
+ functionNode: componentFunction,
23601
+ invocation: null,
23602
+ isDeferred: false,
23603
+ introducedBindings: /* @__PURE__ */ new Set(),
23604
+ substitutions: /* @__PURE__ */ new Map(),
23605
+ currentFilename
23606
+ }, 1);
23607
+ return {
23608
+ sourceReferences: evidence.sourceReferences,
23609
+ isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23610
+ };
23611
+ };
23427
23612
  const findStateSetterReference = (analysis, callExpression) => {
23428
23613
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
23429
23614
  const callee = stripParenExpression(callExpression.callee);
@@ -26476,57 +26661,6 @@ const noDefaultProps = defineRule({
26476
26661
  } })
26477
26662
  });
26478
26663
  //#endregion
26479
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26480
- const getStateName$1 = (stateDeclarator) => {
26481
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26482
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26483
- const stateBinding = stateDeclarator.id.elements?.[0];
26484
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26485
- const setterBinding = stateDeclarator.id.elements?.[1];
26486
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26487
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26488
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26489
- };
26490
- const noDerivedState = defineRule({
26491
- id: "no-derived-state",
26492
- title: "Derived value copied into state",
26493
- severity: "warn",
26494
- tags: ["test-noise"],
26495
- recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26496
- create: (context) => ({ CallExpression(node) {
26497
- if (!isUseEffect(node)) return;
26498
- const analysis = getProgramAnalysis(node);
26499
- if (!analysis) return;
26500
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26501
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26502
- const stateName = getStateName$1(fact.stateDeclarator);
26503
- context.report({
26504
- node: fact.callExpression,
26505
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26506
- });
26507
- }
26508
- } })
26509
- });
26510
- //#endregion
26511
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26512
- const noDerivedStateEffect = defineRule({
26513
- id: "no-derived-state-effect",
26514
- title: "Derived state stored in an effect",
26515
- severity: "warn",
26516
- tags: ["test-noise"],
26517
- recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
26518
- create: (context) => ({ CallExpression(node) {
26519
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26520
- const analysis = getProgramAnalysis(node);
26521
- if (!analysis) return;
26522
- if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26523
- context.report({
26524
- node,
26525
- message: "You pay an extra render for state you can derive from other values."
26526
- });
26527
- } })
26528
- });
26529
- //#endregion
26530
26664
  //#region src/plugin/utils/is-component-function.ts
26531
26665
  const isFunctionAssignedToComponent = (functionNode) => {
26532
26666
  let cursor = functionNode.parent ?? null;
@@ -26660,6 +26794,236 @@ const createComponentPropStackTracker = (callbacks) => {
26660
26794
  };
26661
26795
  };
26662
26796
  //#endregion
26797
+ //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26798
+ const findReferenceDeclarator = (reference) => {
26799
+ for (const definition of reference.resolved?.defs ?? []) {
26800
+ const definitionNode = definition.node;
26801
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26802
+ }
26803
+ return null;
26804
+ };
26805
+ const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26806
+ const node = stripParenExpression(expression);
26807
+ if (!isNodeOfType(node, "Identifier")) return null;
26808
+ const reference = getRef(analysis, node);
26809
+ if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26810
+ if (isProp(analysis, reference)) {
26811
+ if (isWholePropsObjectReference(analysis, reference)) return null;
26812
+ return { bindingIdentity: reference.resolved };
26813
+ }
26814
+ if (isState(analysis, reference)) {
26815
+ const stateDeclarator = getUseStateDecl(analysis, reference);
26816
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26817
+ return { bindingIdentity: reference.resolved };
26818
+ }
26819
+ if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26820
+ const declarator = findReferenceDeclarator(reference);
26821
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26822
+ const nextVisitedBindings = new Set(visitedBindings);
26823
+ nextVisitedBindings.add(reference.resolved);
26824
+ return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26825
+ };
26826
+ const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26827
+ const getDirectBranchStatements = (branch) => {
26828
+ if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26829
+ return [branch];
26830
+ };
26831
+ const getDirectCallExpression = (statement) => {
26832
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26833
+ const expression = stripParenExpression(statement.expression);
26834
+ return isNodeOfType(expression, "CallExpression") ? expression : null;
26835
+ };
26836
+ const getDirectAssignmentExpression = (statement) => {
26837
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26838
+ const expression = stripParenExpression(statement.expression);
26839
+ return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26840
+ };
26841
+ const findDirectStateSetterReference = (analysis, callExpression) => {
26842
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
26843
+ const callee = stripParenExpression(callExpression.callee);
26844
+ if (!isNodeOfType(callee, "Identifier")) return null;
26845
+ const reference = getRef(analysis, callee);
26846
+ return reference && isStateSetter(analysis, reference) ? reference : null;
26847
+ };
26848
+ const getStaticRefCurrentReference = (analysis, expression) => {
26849
+ const node = stripParenExpression(expression);
26850
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26851
+ const reference = getRef(analysis, node.object);
26852
+ if (!reference) return null;
26853
+ const declarator = findReferenceDeclarator(reference);
26854
+ return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26855
+ };
26856
+ const getStateTracker = (analysis, expression) => {
26857
+ const node = stripParenExpression(expression);
26858
+ if (!isNodeOfType(node, "Identifier")) return null;
26859
+ const reference = getRef(analysis, node);
26860
+ if (!reference || !isState(analysis, reference)) return null;
26861
+ const declarator = getUseStateDecl(analysis, reference);
26862
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26863
+ return {
26864
+ kind: "state",
26865
+ declarator
26866
+ };
26867
+ };
26868
+ const getRefTracker = (analysis, expression) => {
26869
+ const reference = getStaticRefCurrentReference(analysis, expression);
26870
+ return reference ? {
26871
+ kind: "ref",
26872
+ reference
26873
+ } : null;
26874
+ };
26875
+ const getTrackerInitializer = (tracker) => {
26876
+ const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26877
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26878
+ const initializer = declarator.init.arguments?.[0];
26879
+ return initializer ? initializer : null;
26880
+ };
26881
+ const isTrackerSynchronized = (analysis, guard) => {
26882
+ for (const statement of guard.statements) {
26883
+ if (guard.tracker.kind === "state") {
26884
+ const callExpression = getDirectCallExpression(statement);
26885
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26886
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26887
+ if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26888
+ const writtenValue = callExpression.arguments?.[0];
26889
+ if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26890
+ continue;
26891
+ }
26892
+ const assignmentExpression = getDirectAssignmentExpression(statement);
26893
+ if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26894
+ if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26895
+ if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26896
+ }
26897
+ return false;
26898
+ };
26899
+ const parseRenderTrackerGuard = (analysis, statement) => {
26900
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26901
+ const left = statement.test.left;
26902
+ const right = statement.test.right;
26903
+ const candidates = [{
26904
+ sourceExpression: left,
26905
+ trackerExpression: right
26906
+ }, {
26907
+ sourceExpression: right,
26908
+ trackerExpression: left
26909
+ }];
26910
+ for (const candidate of candidates) {
26911
+ const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26912
+ if (!source) continue;
26913
+ const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26914
+ if (!tracker) continue;
26915
+ const trackerInitializer = getTrackerInitializer(tracker);
26916
+ if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26917
+ const guard = {
26918
+ source,
26919
+ tracker,
26920
+ statements: getDirectBranchStatements(statement.consequent)
26921
+ };
26922
+ return isTrackerSynchronized(analysis, guard) ? guard : null;
26923
+ }
26924
+ return null;
26925
+ };
26926
+ const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26927
+ if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26928
+ const references = setterReference.resolved.references.filter((reference) => !reference.init);
26929
+ if (references.length !== 1) return false;
26930
+ return references[0].identifier === callExpression.callee;
26931
+ };
26932
+ const getStateInitializer = (stateDeclarator) => {
26933
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26934
+ const initializer = stateDeclarator.init.arguments?.[0];
26935
+ return initializer ? initializer : null;
26936
+ };
26937
+ const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26938
+ if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26939
+ const componentFunction = componentBody.parent;
26940
+ const facts = [];
26941
+ for (const statement of componentBody.body ?? []) {
26942
+ const guard = parseRenderTrackerGuard(analysis, statement);
26943
+ if (!guard) continue;
26944
+ for (const branchStatement of guard.statements) {
26945
+ const callExpression = getDirectCallExpression(branchStatement);
26946
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26947
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26948
+ if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26949
+ const stateDeclarator = getUseStateDecl(analysis, setterReference);
26950
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26951
+ const writtenValue = callExpression.arguments?.[0];
26952
+ const initializer = getStateInitializer(stateDeclarator);
26953
+ if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26954
+ facts.push({
26955
+ callExpression,
26956
+ stateDeclarator
26957
+ });
26958
+ }
26959
+ }
26960
+ return facts;
26961
+ };
26962
+ //#endregion
26963
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26964
+ const getStateName$1 = (stateDeclarator) => {
26965
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26966
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26967
+ const stateBinding = stateDeclarator.id.elements?.[0];
26968
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26969
+ const setterBinding = stateDeclarator.id.elements?.[1];
26970
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26971
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26972
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26973
+ };
26974
+ const noDerivedState = defineRule({
26975
+ id: "no-derived-state",
26976
+ title: "Derived value copied into state",
26977
+ severity: "warn",
26978
+ tags: ["test-noise"],
26979
+ recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26980
+ create: (context) => {
26981
+ const reportStateWrite = (callExpression, stateDeclarator) => {
26982
+ const stateName = getStateName$1(stateDeclarator);
26983
+ context.report({
26984
+ node: callExpression,
26985
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26986
+ });
26987
+ };
26988
+ return {
26989
+ ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26990
+ if (!componentBody) return;
26991
+ const analysis = getProgramAnalysis(componentBody);
26992
+ if (!analysis) return;
26993
+ for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26994
+ } }).visitors,
26995
+ CallExpression(node) {
26996
+ if (!isUseEffect(node)) return;
26997
+ const analysis = getProgramAnalysis(node);
26998
+ if (!analysis) return;
26999
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
27000
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
27001
+ reportStateWrite(fact.callExpression, fact.stateDeclarator);
27002
+ }
27003
+ }
27004
+ };
27005
+ }
27006
+ });
27007
+ //#endregion
27008
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
27009
+ const noDerivedStateEffect = defineRule({
27010
+ id: "no-derived-state-effect",
27011
+ title: "Derived state stored in an effect",
27012
+ severity: "warn",
27013
+ tags: ["test-noise"],
27014
+ recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
27015
+ create: (context) => ({ CallExpression(node) {
27016
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
27017
+ const analysis = getProgramAnalysis(node);
27018
+ if (!analysis) return;
27019
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
27020
+ context.report({
27021
+ node,
27022
+ message: "You pay an extra render for state you can derive from other values."
27023
+ });
27024
+ } })
27025
+ });
27026
+ //#endregion
26663
27027
  //#region src/plugin/utils/is-initial-only-prop-name.ts
26664
27028
  const isInitialOnlyPropName = (propName) => {
26665
27029
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -28923,6 +29287,18 @@ const MUTATING_COLLECTION_METHOD_NAMES = new Set([
28923
29287
  "set",
28924
29288
  "add"
28925
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
+ ]);
28926
29302
  const getMemberRootBindingName = (node, scope) => {
28927
29303
  let currentNode = node;
28928
29304
  while (isNodeOfType(currentNode, "MemberExpression")) currentNode = currentNode.object;
@@ -28976,6 +29352,26 @@ const addMutatingCollectionCallDependencies = (graph, expression, scope, eventHa
28976
29352
  addDependencyNames(dependencyNames, controlDependencyNames);
28977
29353
  addDependencies(graph, receiverRootName, dependencyNames);
28978
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
+ };
28979
29375
  const collectExpressionDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
28980
29376
  if (isNodeOfType(expression, "AssignmentExpression")) {
28981
29377
  addAssignmentExpressionDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
@@ -28983,6 +29379,7 @@ const collectExpressionDependencies = (graph, expression, scope, eventHandlerRef
28983
29379
  }
28984
29380
  if (isNodeOfType(expression, "CallExpression")) {
28985
29381
  addMutatingCollectionCallDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
29382
+ addIteratorMutationDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
28986
29383
  return;
28987
29384
  }
28988
29385
  if (isNodeOfType(expression, "SequenceExpression")) {
@@ -30023,28 +30420,26 @@ const noGiantComponent = defineRule({
30023
30420
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
30024
30421
  return lineCount > 300 ? lineCount : null;
30025
30422
  };
30026
- const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30423
+ const reportOversizedComponent = (nameNode, componentName) => {
30027
30424
  context.report({
30028
30425
  node: nameNode,
30029
- 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.`
30030
30427
  });
30031
30428
  };
30032
30429
  return {
30033
30430
  FunctionDeclaration(node) {
30034
30431
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
30035
- const lineCount = getOversizedComponentLineCount(node);
30036
- if (lineCount === null) return;
30432
+ if (getOversizedComponentLineCount(node) === null) return;
30037
30433
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
30038
- reportOversizedComponent(node.id, node.id.name, lineCount);
30434
+ reportOversizedComponent(node.id, node.id.name);
30039
30435
  },
30040
30436
  VariableDeclarator(node) {
30041
30437
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
30042
30438
  const functionNode = unwrapReactHocFunction(node.init);
30043
30439
  if (!functionNode) return;
30044
- const lineCount = getOversizedComponentLineCount(functionNode);
30045
- if (lineCount === null) return;
30440
+ if (getOversizedComponentLineCount(functionNode) === null) return;
30046
30441
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
30047
- reportOversizedComponent(node.id, node.id.name, lineCount);
30442
+ reportOversizedComponent(node.id, node.id.name);
30048
30443
  }
30049
30444
  };
30050
30445
  }
@@ -34815,6 +35210,66 @@ const noPropCallbackInEffect = defineRule({
34815
35210
  }
34816
35211
  });
34817
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
34818
35273
  //#region src/plugin/rules/architecture/no-prop-types.ts
34819
35274
  const PROP_TYPES_PROPERTY = "propTypes";
34820
35275
  const isPropTypesKey = (key, computed) => {
@@ -35509,6 +35964,63 @@ const noRedundantShouldComponentUpdate = defineRule({
35509
35964
  }
35510
35965
  });
35511
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
35512
36024
  //#region src/plugin/rules/architecture/no-render-in-render.ts
35513
36025
  const isInsideComponentContext = (node) => {
35514
36026
  let cursor = node.parent;
@@ -56899,6 +57411,18 @@ const reactDoctorRules = [
56899
57411
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
56900
57412
  }
56901
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
+ },
56902
57426
  {
56903
57427
  key: "react-doctor/no-prop-types",
56904
57428
  id: "no-prop-types",
@@ -56990,6 +57514,18 @@ const reactDoctorRules = [
56990
57514
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
56991
57515
  }
56992
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
+ },
56993
57529
  {
56994
57530
  key: "react-doctor/no-render-in-render",
56995
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.5113067",
3
+ "version": "0.7.4-dev.5aa82e8",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",