oxlint-plugin-react-doctor 0.7.4-dev.b686594 → 0.7.4-dev.dfccac4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1660 -1148
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -614,11 +614,24 @@ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
614
614
  "URLSearchParams",
615
615
  "AbortController"
616
616
  ]);
617
+ const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
618
+ "Boolean",
619
+ "String",
620
+ "Number",
621
+ "Array",
622
+ "Object",
623
+ "parseInt",
624
+ "parseFloat",
625
+ "isNaN",
626
+ "isFinite",
627
+ "BigInt",
628
+ "Symbol"
629
+ ]);
617
630
  const SETTER_PATTERN = /^set[A-Z]/;
618
631
  const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
619
632
  const UPPERCASE_PATTERN = /^[A-Z]/;
620
633
  const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
621
- const HOOK_NAME_PATTERN$3 = /^use[A-Z]/;
634
+ const HOOK_NAME_PATTERN$4 = /^use[A-Z]/;
622
635
  const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
623
636
  const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
624
637
  const HOOKS_WITH_DEPS = new Set([
@@ -7394,7 +7407,7 @@ const containsJsx$1 = (root) => {
7394
7407
  containsJsxCache.set(root, found);
7395
7408
  return found;
7396
7409
  };
7397
- const getStaticMemberName$1 = (node) => {
7410
+ const getStaticMemberName = (node) => {
7398
7411
  if (!isNodeOfType(node, "MemberExpression")) return null;
7399
7412
  if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
7400
7413
  if (node.computed && isNodeOfType(node.property, "Literal") && typeof node.property.value === "string") return node.property.value;
@@ -7408,7 +7421,7 @@ const getAssignedName = (node) => {
7408
7421
  if (isNodeOfType(parent, "AssignmentExpression")) {
7409
7422
  const left = parent.left;
7410
7423
  if (isNodeOfType(left, "Identifier")) return left.name;
7411
- if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$1(left);
7424
+ if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName(left);
7412
7425
  }
7413
7426
  return null;
7414
7427
  };
@@ -7416,20 +7429,20 @@ const isModuleExportsAssignment = (node) => {
7416
7429
  const parent = node.parent;
7417
7430
  if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
7418
7431
  const left = parent.left;
7419
- return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$1(left) === "exports";
7432
+ return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName(left) === "exports";
7420
7433
  };
7421
7434
  const isCreateClassLikeCall = (node) => {
7422
7435
  if (!isNodeOfType(node, "CallExpression")) return false;
7423
7436
  if (isEs5Component(node)) return true;
7424
7437
  const callee = node.callee;
7425
- if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$1(callee) === "createClass";
7438
+ if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee) === "createClass";
7426
7439
  return false;
7427
7440
  };
7428
7441
  const isCreateContextCall = (node) => {
7429
7442
  if (!isNodeOfType(node, "CallExpression")) return false;
7430
7443
  const callee = node.callee;
7431
7444
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
7432
- return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$1(callee) === "createContext";
7445
+ return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
7433
7446
  };
7434
7447
  const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
7435
7448
  const firstCallArgument = (node) => {
@@ -7487,7 +7500,7 @@ const memberExpressionPath = (node) => {
7487
7500
  if (isNodeOfType(node, "Identifier")) return [node.name];
7488
7501
  if (!isNodeOfType(node, "MemberExpression")) return [];
7489
7502
  const objectPath = memberExpressionPath(node.object);
7490
- const propertyName = getStaticMemberName$1(node);
7503
+ const propertyName = getStaticMemberName(node);
7491
7504
  return propertyName ? [...objectPath, propertyName] : objectPath;
7492
7505
  };
7493
7506
  const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
@@ -7497,7 +7510,7 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
7497
7510
  const identifierTargets = /* @__PURE__ */ new Set();
7498
7511
  const memberPathSegments = /* @__PURE__ */ new Set();
7499
7512
  const visit = (node) => {
7500
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$1(node.left) === "displayName") {
7513
+ if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
7501
7514
  if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
7502
7515
  for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
7503
7516
  }
@@ -12171,7 +12184,7 @@ const isAtomFromJotai = (callExpression) => {
12171
12184
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
12172
12185
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
12173
12186
  };
12174
- const isFunctionExpressionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
12187
+ const isFunctionExpressionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
12175
12188
  const getFirstParameterName = (fn) => {
12176
12189
  const parameters = fn.params ?? [];
12177
12190
  if (parameters.length !== 1) return null;
@@ -12304,7 +12317,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
12304
12317
  const args = node.arguments ?? [];
12305
12318
  if (args.length === 0) return;
12306
12319
  const reader = args[0];
12307
- if (!isFunctionExpressionLike$2(reader)) return;
12320
+ if (!isFunctionExpressionLike$3(reader)) return;
12308
12321
  const getParameterName = getFirstParameterName(reader);
12309
12322
  if (!getParameterName) return;
12310
12323
  const freshReturn = getFreshReturnForFunction(reader);
@@ -12396,14 +12409,14 @@ const getHandlerNamedBindingName = (functionNode) => {
12396
12409
  const containingFunctionIsComponentOrHook = (functionNode) => {
12397
12410
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
12398
12411
  const declaredName = functionNode.id.name;
12399
- return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$3.test(declaredName);
12412
+ return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$4.test(declaredName);
12400
12413
  }
12401
12414
  let cursor = functionNode.parent ?? null;
12402
12415
  while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
12403
12416
  if (!cursor) return false;
12404
12417
  if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
12405
12418
  if (!isNodeOfType(cursor.id, "Identifier")) return false;
12406
- return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$3.test(cursor.id.name);
12419
+ return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$4.test(cursor.id.name);
12407
12420
  };
12408
12421
  const isBindingInvokedOnRenderPath = (root, bindingName) => {
12409
12422
  let didFindRenderPathInvocation = false;
@@ -21325,6 +21338,19 @@ const noAccessKey = defineRule({
21325
21338
  }
21326
21339
  });
21327
21340
  //#endregion
21341
+ //#region src/plugin/utils/get-call-method-name.ts
21342
+ /**
21343
+ * Returns the static method name of a call's callee when it's a
21344
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
21345
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
21346
+ * `no-pass-live-state-to-parent` to match the receiver-bound
21347
+ * method-call shape against an allow/block list.
21348
+ */
21349
+ const getCallMethodName = (callee) => {
21350
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
21351
+ return null;
21352
+ };
21353
+ //#endregion
21328
21354
  //#region src/plugin/utils/reads-post-mount-value.ts
21329
21355
  const DOM_QUERY_MEMBER_NAMES = new Set([
21330
21356
  "getBoundingClientRect",
@@ -21422,6 +21448,13 @@ const isPostMountMemberRead = (node) => {
21422
21448
  if (!LAYOUT_MEASUREMENT_MEMBER_NAMES.has(memberName)) return false;
21423
21449
  return isRefLikeReceiver(node.object);
21424
21450
  };
21451
+ const isMeasurementMemberRead = (node) => {
21452
+ if (!isPostMountMemberRead(node)) return false;
21453
+ if (!isNodeOfType(node, "MemberExpression") || !isNodeOfType(node.property, "Identifier")) return false;
21454
+ if (!DOM_QUERY_MEMBER_NAMES.has(node.property.name)) return true;
21455
+ const parent = node.parent;
21456
+ return Boolean(parent && isNodeOfType(parent, "CallExpression") && parent.callee === node);
21457
+ };
21425
21458
  const isPropertyNamePosition$1 = (identifier) => {
21426
21459
  const parent = identifier.parent;
21427
21460
  if (!parent) return false;
@@ -21539,10 +21572,10 @@ const getScopeForNode = (node, manager) => {
21539
21572
  //#endregion
21540
21573
  //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
21541
21574
  const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
21542
- const HOOK_NAME_PATTERN$2 = /^use[A-Z0-9]/;
21575
+ const HOOK_NAME_PATTERN$3 = /^use[A-Z0-9]/;
21543
21576
  const isInsideCallbackArgumentOf = (identifier, initializer) => {
21544
21577
  if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
21545
- if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN$2.test(initializer.callee.name)) return false;
21578
+ if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN$3.test(initializer.callee.name)) return false;
21546
21579
  const callbackArguments = (initializer.arguments ?? []).filter((argument) => isFunctionLike$1(argument));
21547
21580
  if (callbackArguments.length === 0) return false;
21548
21581
  let node = identifier;
@@ -21706,133 +21739,6 @@ const getEventualCallRefsTo = (analysis, ref, predicate) => {
21706
21739
  return callExprRefs.filter(predicate);
21707
21740
  };
21708
21741
  //#endregion
21709
- //#region src/plugin/rules/state-and-effects/utils/effect/external-state.ts
21710
- const DEFERRING_CALLEE_NAMES$1 = new Set([
21711
- "setTimeout",
21712
- "setInterval",
21713
- "setImmediate",
21714
- "requestAnimationFrame",
21715
- "requestIdleCallback",
21716
- "queueMicrotask",
21717
- "addEventListener",
21718
- "addListener",
21719
- "subscribe",
21720
- "observe",
21721
- "watch",
21722
- "watchPosition",
21723
- "then",
21724
- "catch",
21725
- "finally",
21726
- "on",
21727
- "once"
21728
- ]);
21729
- const getCalleeName$1 = (callee) => {
21730
- if (!callee) return null;
21731
- if (isNodeOfType(callee, "Identifier")) return callee.name;
21732
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
21733
- return null;
21734
- };
21735
- const parentOf = (node) => node.parent ?? null;
21736
- const argumentsInclude = (args, target) => (args ?? []).some((argument) => argument === target);
21737
- const isDeferredCallbackPosition = (expression) => {
21738
- const parent = parentOf(expression);
21739
- if (!parent) return false;
21740
- if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
21741
- const name = getCalleeName$1(parent.callee);
21742
- if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
21743
- }
21744
- if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
21745
- const name = getCalleeName$1(parent.callee);
21746
- if (name && (name.endsWith("Observer") || name === "Promise")) return true;
21747
- }
21748
- if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
21749
- return false;
21750
- };
21751
- const getHandlerDeclarator = (fn) => {
21752
- let current = fn;
21753
- let parent = parentOf(current);
21754
- while (parent && isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, current)) {
21755
- current = parent;
21756
- parent = parentOf(current);
21757
- }
21758
- if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent;
21759
- return null;
21760
- };
21761
- const isNamedHandlerUsedAsDeferredCallback = (analysis, fn) => {
21762
- const declarator = getHandlerDeclarator(fn);
21763
- if (!declarator || !isNodeOfType(declarator.id, "Identifier")) return false;
21764
- const name = declarator.id.name;
21765
- for (const scope of analysis.scopeManager.scopes) {
21766
- const variable = scope.variables.find((candidate) => candidate.name === name && candidate.defs.some((def) => def.node === declarator));
21767
- if (!variable) continue;
21768
- return variable.references.some((reference) => isDeferredCallbackPosition(reference.identifier));
21769
- }
21770
- return false;
21771
- };
21772
- const isDeferredCallbackFunction = (analysis, fn) => {
21773
- if (isDeferredCallbackPosition(fn)) return true;
21774
- return isNamedHandlerUsedAsDeferredCallback(analysis, fn);
21775
- };
21776
- const isInsideDeferredCallback$1 = (analysis, node, boundary) => {
21777
- let current = parentOf(node);
21778
- while (current && current !== boundary) {
21779
- if (isFunctionLike$1(current) && isDeferredCallbackFunction(analysis, current)) return true;
21780
- current = parentOf(current);
21781
- }
21782
- return false;
21783
- };
21784
- const findUseStateDeclarator = (ref) => {
21785
- for (const def of ref.resolved?.defs ?? []) {
21786
- const node = def.node;
21787
- if (!isNodeOfType(node, "VariableDeclarator")) continue;
21788
- if (!isNodeOfType(node.init, "CallExpression")) continue;
21789
- if (!isNodeOfType(node.id, "ArrayPattern")) continue;
21790
- return node;
21791
- }
21792
- return null;
21793
- };
21794
- const declaratorToExternallyDriven = /* @__PURE__ */ new WeakMap();
21795
- const isExternallyDrivenState = (analysis, ref) => {
21796
- const declarator = findUseStateDeclarator(ref);
21797
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
21798
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
21799
- const cached = declaratorToExternallyDriven.get(declarator);
21800
- if (cached !== void 0) return cached;
21801
- const result = computeExternallyDriven(analysis, declarator);
21802
- declaratorToExternallyDriven.set(declarator, result);
21803
- return result;
21804
- };
21805
- const computeExternallyDriven = (analysis, declarator) => {
21806
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
21807
- const setterElement = declarator.id.elements?.[1];
21808
- if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
21809
- const setterName = setterElement.name;
21810
- let setterVariable = null;
21811
- for (const scope of analysis.scopeManager.scopes) {
21812
- const match = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
21813
- if (match) {
21814
- setterVariable = match;
21815
- break;
21816
- }
21817
- }
21818
- if (!setterVariable) return false;
21819
- let hasDeferredCallSite = false;
21820
- for (const setterReference of setterVariable.references) {
21821
- const identifier = setterReference.identifier;
21822
- const parent = parentOf(identifier);
21823
- if (!parent) continue;
21824
- if (isDeferredCallbackPosition(identifier)) {
21825
- hasDeferredCallSite = true;
21826
- continue;
21827
- }
21828
- if (!isNodeOfType(parent, "CallExpression")) continue;
21829
- if (parent.callee !== identifier) continue;
21830
- if (!isInsideDeferredCallback$1(analysis, parent, declarator)) return false;
21831
- hasDeferredCallSite = true;
21832
- }
21833
- return hasDeferredCallSite;
21834
- };
21835
- //#endregion
21836
21742
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
21837
21743
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
21838
21744
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
@@ -22096,736 +22002,167 @@ const findContainingNode = (analysis, node) => {
22096
22002
  return findContainingNode(analysis, parent);
22097
22003
  };
22098
22004
  //#endregion
22099
- //#region src/plugin/rules/state-and-effects/utils/event-handler-reference.ts
22100
- const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
22101
- const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
22102
- if (!isNodeOfType(node, "MemberExpression")) return null;
22103
- if (!isNodeOfType(node.object, "Identifier")) return null;
22104
- const propertyName = getStaticMemberPropertyName(node);
22105
- return propertyName ? `${resolveName(node.object.name)}.${propertyName}` : null;
22106
- };
22107
- const isEventHandlerValue = (node, eventHandlerReferenceNames, resolveName = (name) => name) => {
22108
- if (isInlineFunctionExpression(node)) return true;
22109
- if (isNodeOfType(node, "Identifier")) return eventHandlerReferenceNames.has(resolveName(node.name));
22110
- const memberReferenceName = getStaticMemberReferenceName(node, resolveName);
22111
- return Boolean(memberReferenceName && eventHandlerReferenceNames.has(memberReferenceName));
22112
- };
22113
- const isIntrinsicJsxAttribute = (node) => {
22114
- if (!isNodeOfType(node, "JSXAttribute")) return false;
22115
- const openingElement = node.parent;
22116
- if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
22117
- const elementName = openingElement.name;
22118
- if (!isNodeOfType(elementName, "JSXIdentifier")) return false;
22119
- return /^[a-z]/.test(elementName.name);
22120
- };
22121
- //#endregion
22122
- //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
22123
- const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
22124
- const getOwnScopeBoundNames = (functionNode) => {
22125
- const cached = ownScopeBoundNamesCache.get(functionNode);
22126
- if (cached) return cached;
22127
- const boundNames = /* @__PURE__ */ new Set();
22128
- if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
22129
- walkAst(functionNode, (child) => {
22130
- if (child !== functionNode && isFunctionLike$1(child)) return false;
22131
- if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
22132
- });
22133
- ownScopeBoundNamesCache.set(functionNode, boundNames);
22134
- return boundNames;
22135
- };
22136
- const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
22137
- const isPropertyNamePosition = (identifier) => {
22138
- const parent = identifier.parent;
22139
- if (!parent) return false;
22140
- if (isNodeOfType(parent, "MemberExpression")) return parent.property === identifier && !parent.computed;
22141
- if (isNodeOfType(parent, "Property")) return parent.key === identifier && !parent.computed;
22142
- return false;
22143
- };
22144
- const referencesIdentifierNamed = (root, identifierName) => {
22145
- let isReferenced = false;
22146
- walkAst(root, (child) => {
22147
- if (isReferenced) return false;
22148
- if (child !== root && isFunctionLike$1(child) && declaresBindingNamed(child, identifierName)) return false;
22149
- if (isNodeOfType(child, "Identifier") && child.name === identifierName && !isPropertyNamePosition(child)) {
22150
- isReferenced = true;
22151
- return false;
22152
- }
22153
- });
22154
- return isReferenced;
22155
- };
22156
- const isSetterWiredToJsxHandler = (componentFunction, setterName) => {
22157
- let isWired = false;
22158
- walkAst(componentFunction, (child) => {
22159
- if (isWired) return false;
22160
- if (child !== componentFunction && isFunctionLike$1(child) && declaresBindingNamed(child, setterName)) return false;
22161
- if (!isNodeOfType(child, "JSXAttribute") || !child.value) return;
22162
- const attributeName = getJsxAttributeName(child.name);
22163
- if (!attributeName || !isEventHandlerName(attributeName)) return;
22164
- if (referencesIdentifierNamed(child.value, setterName)) {
22165
- isWired = true;
22166
- return false;
22167
- }
22168
- });
22169
- return isWired;
22170
- };
22171
- //#endregion
22172
- //#region src/plugin/rules/state-and-effects/utils/has-user-input-setter-writer.ts
22173
- const HANDLER_BINDING_NAME_PATTERN = /^(on|handle)[A-Z_]/;
22174
- const isEventHandlerPropertyKey = (property) => isNodeOfType(property, "Property") && !property.computed && isNodeOfType(property.key, "Identifier") && isEventHandlerName(property.key.name);
22175
- const DEFERRED_CALLBACK_MEMBER_NAMES = new Set([
22176
- "then",
22177
- "catch",
22178
- "finally",
22179
- "subscribe"
22180
- ]);
22181
- const DEFERRED_CALLBACK_CALLEE_NAMES = new Set([
22182
- "setTimeout",
22183
- "setInterval",
22184
- "requestAnimationFrame",
22185
- "addEventListener",
22186
- "addListener"
22187
- ]);
22188
- const isDeferredCallbackArgumentOf = (callExpr, child) => {
22189
- if (!isNodeOfType(callExpr, "CallExpression")) return false;
22190
- if (!(callExpr.arguments ?? []).includes(child)) return false;
22191
- const callee = callExpr.callee;
22192
- if (isNodeOfType(callee, "Identifier")) return DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.name);
22193
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return DEFERRED_CALLBACK_MEMBER_NAMES.has(callee.property.name) || DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.property.name);
22005
+ //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
22006
+ const isLiteralOnlyExpression$1 = (node) => {
22007
+ if (!node) return false;
22008
+ if (isNodeOfType(node, "Literal")) return true;
22009
+ if (isNodeOfType(node, "Identifier") && node.name === "undefined") return true;
22010
+ if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression$1(node.argument);
22011
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isLiteralOnlyExpression$1(property.value));
22012
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => isLiteralOnlyExpression$1(element));
22013
+ if (isNodeOfType(node, "NewExpression")) {
22014
+ const callee = node.callee;
22015
+ return isNodeOfType(callee, "Identifier") && (callee.name === "Map" || callee.name === "Set") && (node.arguments ?? []).length === 0;
22016
+ }
22194
22017
  return false;
22195
22018
  };
22196
- const isIndependentWriterIdentifier = (componentFunction, identifier, includeDeferredWriters) => {
22197
- let outermostFunctionBelowComponent = null;
22198
- let previous = identifier;
22199
- let cursor = identifier.parent;
22200
- while (cursor && cursor !== componentFunction) {
22201
- if (isNodeOfType(cursor, "JSXAttribute")) {
22202
- const attributeName = getJsxAttributeName(cursor.name);
22203
- if (attributeName && isEventHandlerName(attributeName)) return true;
22204
- }
22205
- if (isEventHandlerPropertyKey(cursor)) return true;
22206
- if (includeDeferredWriters && isDeferredCallbackArgumentOf(cursor, previous)) return true;
22207
- if (isFunctionLike$1(cursor)) {
22208
- outermostFunctionBelowComponent = cursor;
22209
- if (includeDeferredWriters && cursor.async === true) return true;
22019
+ const isSpreadMergeOfLiterals = (body) => {
22020
+ if (!body || !isNodeOfType(body, "ObjectExpression")) return false;
22021
+ const properties = body.properties ?? [];
22022
+ let sawSpread = false;
22023
+ for (const property of properties) {
22024
+ if (isNodeOfType(property, "SpreadElement")) {
22025
+ sawSpread = true;
22026
+ continue;
22210
22027
  }
22211
- previous = cursor;
22212
- cursor = cursor.parent ?? null;
22028
+ if (!isNodeOfType(property, "Property")) return false;
22029
+ if (!isLiteralOnlyExpression$1(property.value)) return false;
22213
22030
  }
22214
- if (!outermostFunctionBelowComponent) return false;
22215
- const bindingName = getFunctionBindingName$1(outermostFunctionBelowComponent);
22216
- if (!bindingName) return false;
22217
- if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
22218
- return isSetterWiredToJsxHandler(componentFunction, bindingName);
22031
+ return sawSpread;
22219
22032
  };
22220
- const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters = false) => {
22221
- if (!setterRef.resolved) return false;
22222
- const componentFunction = findEnclosingFunction(effectNode);
22223
- if (!componentFunction) return false;
22224
- for (const reference of setterRef.resolved.references) {
22225
- if (reference.init) continue;
22226
- const identifier = reference.identifier;
22227
- if (isAstDescendant(identifier, effectNode)) continue;
22228
- if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
22033
+ const isConstantSetterArgument = (analysis, callExpr) => {
22034
+ const argument = callExpr.arguments?.[0];
22035
+ if (!argument) return true;
22036
+ if (isLiteralOnlyExpression$1(argument)) return true;
22037
+ if (isNodeOfType(argument, "Identifier")) {
22038
+ const argumentRef = getRef(analysis, argument);
22039
+ if (argumentRef && isConstant(argumentRef)) return true;
22229
22040
  }
22041
+ if (isNodeOfType(argument, "ArrowFunctionExpression")) return isSpreadMergeOfLiterals(argument.body);
22230
22042
  return false;
22231
22043
  };
22232
- //#endregion
22233
- //#region src/plugin/rules/state-and-effects/utils/reads-post-mount-through-locals.ts
22234
- const isBareRefCurrentRead = (node) => isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === "current";
22235
- const matchesPostMountRead = (node, options) => {
22236
- if (isPostMountGlobalRead(node)) return true;
22237
- if (!isPostMountMemberRead(node)) return false;
22238
- return !(options.ignoreBareRefCurrent === true && isBareRefCurrentRead(node));
22239
- };
22240
- const objectPatternBindsName = (pattern, name) => {
22241
- if (!isNodeOfType(pattern, "ObjectPattern")) return false;
22242
- return (pattern.properties ?? []).some((property) => {
22243
- if (!isNodeOfType(property, "Property")) return false;
22244
- const bound = property.value;
22245
- return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
22246
- });
22247
- };
22248
- const findEffectLocalInitializer = (effectFn, name) => {
22249
- let initializer = null;
22250
- walkAst(effectFn, (child) => {
22251
- if (initializer) return false;
22252
- if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
22253
- if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName(child.id, name)) {
22254
- initializer = child.init;
22255
- return false;
22256
- }
22257
- });
22258
- return initializer;
22259
- };
22260
- const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedLocalNames = /* @__PURE__ */ new Set()) => {
22261
- let found = false;
22262
- walkAst(root, (child) => {
22263
- if (found) return false;
22264
- if (matchesPostMountRead(child, options)) {
22265
- found = true;
22266
- return false;
22267
- }
22268
- if (!isNodeOfType(child, "Identifier")) return;
22269
- if (visitedLocalNames.has(child.name)) return;
22270
- visitedLocalNames.add(child.name);
22271
- const localInitializer = findEffectLocalInitializer(effectFn, child.name);
22272
- if (localInitializer && readsPostMountValueThroughLocals(localInitializer, effectFn, options, visitedLocalNames)) {
22273
- found = true;
22274
- return false;
22275
- }
22276
- });
22277
- return found;
22278
- };
22279
- //#endregion
22280
- //#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
22281
- const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
22282
- "every",
22283
- "filter",
22284
- "find",
22285
- "findIndex",
22286
- "flatMap",
22287
- "forEach",
22288
- "map",
22289
- "reduce",
22290
- "reduceRight",
22291
- "some"
22292
- ]);
22293
- const DEFERRING_CALLEE_NAMES = new Set([
22294
- "queueMicrotask",
22295
- "requestAnimationFrame",
22296
- "requestIdleCallback",
22297
- "setImmediate",
22298
- "setInterval",
22299
- "setTimeout"
22300
- ]);
22301
- const DEFERRING_MEMBER_NAMES = new Set([
22302
- "catch",
22303
- "finally",
22304
- "then"
22305
- ]);
22306
- const PURE_GLOBAL_CALLEE_NAMES = new Set([
22307
- "Array",
22308
- "BigInt",
22309
- "Boolean",
22310
- "Number",
22311
- "Object",
22312
- "String",
22313
- "parseFloat",
22314
- "parseInt"
22315
- ]);
22316
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22317
- const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22318
- "concat",
22319
- "filter",
22320
- "join",
22321
- "map",
22322
- "split",
22323
- "toLowerCase",
22324
- "toString",
22325
- "toUpperCase",
22326
- "trim"
22327
- ]);
22328
- const STORAGE_GLOBAL_NAMES$1 = new Set([
22329
- "indexedDB",
22330
- "localStorage",
22331
- "sessionStorage"
22044
+ const SUBSCRIPTION_REGISTRATION_METHOD_NAMES = new Set([
22045
+ "addEventListener",
22046
+ "addListener",
22047
+ "on",
22048
+ "once",
22049
+ "subscribe",
22050
+ "observe",
22051
+ "observeDeep",
22052
+ "watch"
22332
22053
  ]);
22333
- const getStaticMemberName = (node) => {
22334
- if (!isNodeOfType(node, "MemberExpression")) return null;
22335
- if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
22336
- if (node.computed && isNodeOfType(node.property, "Literal")) return typeof node.property.value === "string" ? node.property.value : null;
22337
- return null;
22338
- };
22339
- const getMemberRoot = (node) => {
22340
- let current = stripParenExpression(node);
22341
- while (isNodeOfType(current, "MemberExpression")) current = stripParenExpression(current.object);
22342
- return current;
22343
- };
22344
- const getCallCalleeName$1 = (callExpression) => {
22345
- if (!isNodeOfType(callExpression, "CallExpression")) return null;
22346
- const callee = stripParenExpression(callExpression.callee);
22347
- if (isNodeOfType(callee, "Identifier")) return callee.name;
22348
- return getStaticMemberName(callee);
22349
- };
22350
- const getFunctionParameters = (functionNode) => isFunctionLike$1(functionNode) ? functionNode.params ?? [] : [];
22351
- const getIdentifierBindingIdentity = (analysis, identifier) => {
22352
- if (!isNodeOfType(identifier, "Identifier")) return null;
22353
- return getRef(analysis, identifier)?.resolved ?? null;
22354
- };
22355
- const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22356
- if (!isNodeOfType(parameter, "Identifier")) return parameter;
22357
- for (const scope of analysis.scopeManager.scopes) {
22358
- const variable = scope.variables.find((candidate) => candidate.name === parameter.name && candidate.defs.some((definition) => definition.type === "Parameter" && definition.node === functionNode));
22359
- if (variable) return variable;
22360
- }
22361
- return parameter;
22362
- };
22363
- const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22364
- const substitutions = /* @__PURE__ */ new Map();
22365
- const parameters = getFunctionParameters(functionNode);
22366
- for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
22367
- const parameter = parameters[parameterIndex];
22368
- const argument = argumentExpressions[parameterIndex];
22369
- if (!parameter || !argument || !isNodeOfType(parameter, "Identifier")) continue;
22370
- substitutions.set(getParameterBindingIdentity(analysis, functionNode, parameter), {
22371
- expression: argument,
22372
- frame: parentFrame
22373
- });
22054
+ const isSubscriptionRegistrationCall = (node) => {
22055
+ if (isNodeOfType(node, "NewExpression")) {
22056
+ const callee = node.callee;
22057
+ return isNodeOfType(callee, "Identifier") && callee.name.endsWith("Observer");
22374
22058
  }
22375
- return substitutions;
22376
- };
22377
- const isReactUseEffectEventCallee = (analysis, callee) => {
22378
- if (isNodeOfType(callee, "MemberExpression")) return !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffectEvent";
22379
- if (!isNodeOfType(callee, "Identifier")) return false;
22380
- const calleeReference = getRef(analysis, callee);
22381
- if (!calleeReference?.resolved) return callee.name === "useEffectEvent";
22382
- return calleeReference.resolved.defs.some((definition) => {
22383
- if (definition.type !== "ImportBinding") return false;
22384
- const definitionNode = definition.node;
22385
- if (!isNodeOfType(definitionNode, "ImportSpecifier")) return false;
22386
- if (!isNodeOfType(definitionNode.imported, "Identifier")) return false;
22387
- if (definitionNode.imported.name !== "useEffectEvent") return false;
22388
- const declaration = definitionNode.parent;
22389
- return Boolean(declaration && isNodeOfType(declaration, "ImportDeclaration") && isNodeOfType(declaration.source, "Literal") && declaration.source.value === "react");
22390
- });
22391
- };
22392
- const localUseEventPreservesCallback = (analysis, callee) => {
22393
- if (!isNodeOfType(callee, "Identifier")) return false;
22394
- if (!/^useEvent(?:Callback)?$/.test(callee.name)) return false;
22395
- const calleeReference = getRef(analysis, callee);
22396
- if (!calleeReference?.resolved) return false;
22397
- const implementation = resolveToFunction(calleeReference);
22398
- if (!implementation) return false;
22399
- const callbackParameter = getFunctionParameters(implementation)[0];
22400
- if (!callbackParameter || !isNodeOfType(callbackParameter, "Identifier")) return false;
22401
- const callbackBinding = getParameterBindingIdentity(analysis, implementation, callbackParameter);
22402
- const callbackRefDeclarators = [];
22403
- walkAst(implementation, (child) => {
22404
- if (child !== implementation && isFunctionLike$1(child)) return false;
22405
- if (!isNodeOfType(child, "VariableDeclarator")) return;
22406
- if (!isNodeOfType(child.id, "Identifier")) return;
22407
- if (!isNodeOfType(child.init, "CallExpression")) return;
22408
- if (getCallCalleeName$1(child.init) !== "useRef") return;
22409
- const refInitializer = child.init.arguments?.[0];
22410
- if (!refInitializer || getIdentifierBindingIdentity(analysis, refInitializer) !== callbackBinding) return;
22411
- callbackRefDeclarators.push(child);
22412
- });
22413
- if (callbackRefDeclarators.length === 0) return false;
22414
- return getReturnedExpressions(implementation).some((returnedExpression) => {
22415
- const returnedCall = stripParenExpression(returnedExpression);
22416
- if (!isNodeOfType(returnedCall, "CallExpression")) return false;
22417
- const returnedCalleeName = getCallCalleeName$1(returnedCall);
22418
- if (returnedCalleeName !== "useCallback" && returnedCalleeName !== "useEffectEvent") return false;
22419
- const stableCallback = returnedCall.arguments?.[0];
22420
- if (!stableCallback || !isFunctionLike$1(stableCallback)) return false;
22421
- let forwardsCallback = false;
22422
- walkAst(stableCallback, (child) => {
22423
- if (forwardsCallback) return false;
22424
- if (child !== stableCallback && isFunctionLike$1(child)) return false;
22425
- if (!isNodeOfType(child, "CallExpression")) return;
22426
- const forwardedCallee = stripParenExpression(child.callee);
22427
- if (!isNodeOfType(forwardedCallee, "MemberExpression")) return;
22428
- if (getStaticMemberName(forwardedCallee) !== "current") return;
22429
- if (!isNodeOfType(forwardedCallee.object, "Identifier")) return;
22430
- const refReference = getRef(analysis, forwardedCallee.object);
22431
- if (!callbackRefDeclarators.find((declarator) => refReference?.resolved?.defs.some((definition) => definition.node === declarator)) || !refReference?.resolved) return;
22432
- if (!refReference.resolved.references.some((candidateReference) => {
22433
- const memberExpression = candidateReference.identifier.parent;
22434
- const assignmentExpression = memberExpression?.parent;
22435
- if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
22436
- return getIdentifierBindingIdentity(analysis, assignmentExpression.right) !== callbackBinding;
22437
- })) forwardsCallback = true;
22438
- });
22439
- return forwardsCallback;
22440
- });
22441
- };
22442
- const resolveWrappedCallable = (analysis, node) => {
22443
- const candidate = stripParenExpression(node);
22444
- if (isFunctionLike$1(candidate)) return candidate;
22445
- if (isNodeOfType(candidate, "Identifier")) {
22446
- const reference = getRef(analysis, candidate);
22447
- if (!reference) return null;
22448
- if (reference.resolved?.defs.some((definition) => definition.type === "Parameter" || definition.type === "ImportBinding")) return null;
22449
- const resolved = resolveToFunction(reference);
22450
- if (resolved) return resolved;
22451
- const definitionNode = reference.resolved?.defs[0]?.node;
22452
- if (!definitionNode || !isNodeOfType(definitionNode, "VariableDeclarator")) return null;
22453
- const initializer = definitionNode.init;
22454
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return null;
22455
- if (!isReactUseEffectEventCallee(analysis, initializer.callee) && !localUseEventPreservesCallback(analysis, initializer.callee)) return null;
22456
- const callback = initializer.arguments?.[0];
22457
- return callback && isFunctionLike$1(callback) ? callback : null;
22458
- }
22459
- if (!isNodeOfType(candidate, "MemberExpression")) return null;
22460
- if (getStaticMemberName(candidate) !== "current") return null;
22461
- if (!isNodeOfType(candidate.object, "Identifier")) return null;
22462
- const reference = getRef(analysis, candidate.object);
22463
- const declarator = reference?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
22464
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
22465
- if (!isNodeOfType(declarator.init, "CallExpression")) return null;
22466
- if (getCallCalleeName$1(declarator.init) !== "useRef") return null;
22467
- const initializer = declarator.init.arguments?.[0];
22468
- if (!initializer || !isFunctionLike$1(initializer)) return null;
22469
- return Boolean(reference?.resolved?.references.some((candidateReference) => {
22470
- const member = candidateReference.identifier.parent;
22471
- const assignment = member?.parent;
22472
- return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
22473
- })) ? null : initializer;
22474
- };
22475
- const functionInvokesItself = (analysis, functionNode) => {
22476
- let invokesItself = false;
22477
- walkAst(functionNode, (child) => {
22478
- if (invokesItself) return false;
22479
- if (child !== functionNode && isFunctionLike$1(child)) return false;
22480
- if (!isNodeOfType(child, "CallExpression")) return;
22481
- if (resolveWrappedCallable(analysis, child.callee) === functionNode) {
22482
- invokesItself = true;
22483
- return false;
22484
- }
22485
- });
22486
- return invokesItself;
22487
- };
22488
- const collectIntroducedBindings = (analysis, functionNode) => {
22489
- const introducedBindings = /* @__PURE__ */ new Set();
22490
- for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
22491
- return introducedBindings;
22492
- };
22493
- const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22494
- const effectFunction = getEffectFn(analysis, effectNode);
22495
- if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
22496
- const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
22497
- const rootFrame = {
22498
- functionNode: effectFunction,
22499
- invocation: null,
22500
- isDeferred: false,
22501
- introducedBindings: /* @__PURE__ */ new Set(),
22502
- substitutions: /* @__PURE__ */ new Map()
22503
- };
22504
- const frames = [rootFrame];
22505
- walkAst(effectFunction, (child) => {
22506
- if (child !== effectFunction && isFunctionLike$1(child)) return false;
22507
- if (!isNodeOfType(child, "CallExpression")) return;
22508
- const callee = stripParenExpression(child.callee);
22509
- const calleeName = getCallCalleeName$1(child);
22510
- const memberName = getStaticMemberName(callee);
22511
- const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
22512
- const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(memberName) && isNodeOfType(callee, "MemberExpression");
22513
- const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
22514
- const callable = resolveWrappedCallable(analysis, callableNode);
22515
- if (!callable || callable.async === true || callable === effectFunction) return;
22516
- if (functionInvokesItself(analysis, callable)) return;
22517
- if (!allowWithoutInvocationEvidence && !invokedFunctionEvidence.has(callable) && !isNodeOfType(callableNode, "Identifier") && !isNodeOfType(callableNode, "MemberExpression")) return;
22518
- frames.push({
22519
- functionNode: callable,
22520
- invocation: child,
22521
- isDeferred,
22522
- introducedBindings,
22523
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
22524
- });
22525
- };
22526
- if (isFunctionLike$1(callee)) {
22527
- enqueueFrame(callee, child.arguments ?? [], false, /* @__PURE__ */ new Set());
22528
- return;
22529
- }
22530
- if (isIteratorCall && isNodeOfType(callee, "MemberExpression")) {
22531
- const collectionExpression = callee.object;
22532
- for (const argument of child.arguments ?? []) {
22533
- if (!resolveWrappedCallable(analysis, argument)) continue;
22534
- enqueueFrame(argument, [collectionExpression], false, /* @__PURE__ */ new Set(), true);
22535
- }
22536
- return;
22059
+ if (!isNodeOfType(node, "CallExpression")) return false;
22060
+ const callee = node.callee;
22061
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_REGISTRATION_METHOD_NAMES.has(callee.property.name);
22062
+ };
22063
+ const isCallArgumentOf = (parent, node) => {
22064
+ if (!isNodeOfType(parent, "CallExpression") && !isNodeOfType(parent, "NewExpression")) return false;
22065
+ return (parent.arguments ?? []).some((argument) => argument === node);
22066
+ };
22067
+ const EVENT_HANDLER_PROPERTY_PATTERN = /^on[a-z]/;
22068
+ const collectEventPropertyHandlerFunctions = (analysis, effectFn) => {
22069
+ const handlerFunctions = [];
22070
+ for (const assignment of findDownstreamNodes(effectFn, "AssignmentExpression")) {
22071
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=") continue;
22072
+ const target = assignment.left;
22073
+ if (!isNodeOfType(target, "MemberExpression") || !isNodeOfType(target.property, "Identifier") || !EVENT_HANDLER_PROPERTY_PATTERN.test(target.property.name)) continue;
22074
+ const assigned = assignment.right;
22075
+ if (isFunctionLike$1(assigned)) {
22076
+ handlerFunctions.push(assigned);
22077
+ continue;
22537
22078
  }
22538
- if (isDeferringCall) {
22539
- for (const argument of child.arguments ?? []) {
22540
- const callable = resolveWrappedCallable(analysis, argument);
22541
- if (!callable) continue;
22542
- enqueueFrame(argument, [], true, memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName) ? collectIntroducedBindings(analysis, callable) : /* @__PURE__ */ new Set(), true);
22543
- }
22544
- return;
22079
+ if (isNodeOfType(assigned, "Identifier")) {
22080
+ const assignedRef = getRef(analysis, assigned);
22081
+ const resolvedFunction = assignedRef ? resolveToFunction(assignedRef) : null;
22082
+ if (resolvedFunction) handlerFunctions.push(resolvedFunction);
22545
22083
  }
22546
- enqueueFrame(callee, child.arguments ?? [], false, /* @__PURE__ */ new Set());
22547
- });
22548
- return frames;
22549
- };
22550
- const emptyEvidence = () => ({
22551
- sourceReferences: /* @__PURE__ */ new Set(),
22552
- hasUnknownSource: false,
22553
- hasDeferredIntroducedValue: false,
22554
- readsExternalValue: false
22555
- });
22556
- const mergeEvidence = (target, source) => {
22557
- for (const reference of source.sourceReferences) target.sourceReferences.add(reference);
22558
- target.hasUnknownSource ||= source.hasUnknownSource;
22559
- target.hasDeferredIntroducedValue ||= source.hasDeferredIntroducedValue;
22560
- target.readsExternalValue ||= source.readsExternalValue;
22561
- };
22562
- const getReturnedExpressions = (functionNode) => {
22563
- if (!isFunctionLike$1(functionNode)) return [];
22564
- if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
22565
- const returnedExpressions = [];
22566
- walkAst(functionNode.body, (child) => {
22567
- if (child !== functionNode.body && isFunctionLike$1(child)) return false;
22568
- if (isNodeOfType(child, "ReturnStatement") && child.argument) returnedExpressions.push(child.argument);
22569
- });
22570
- return returnedExpressions;
22571
- };
22572
- const isOpaqueHookCall = (callExpression) => {
22573
- const calleeName = getCallCalleeName$1(callExpression);
22574
- return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
22575
- };
22576
- const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
22577
- const definitionNode = definition.node;
22578
- return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
22579
- }) === true;
22580
- const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
22581
- const node = stripParenExpression(expression);
22582
- const evidence = emptyEvidence();
22583
- if (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode)) {
22584
- evidence.readsExternalValue = true;
22585
- return evidence;
22586
- }
22587
- const root = getMemberRoot(node);
22588
- if (isNodeOfType(root, "Identifier") && STORAGE_GLOBAL_NAMES$1.has(root.name)) {
22589
- evidence.readsExternalValue = true;
22590
- return evidence;
22591
22084
  }
22592
- if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement") || isNodeOfType(node, "ThisExpression")) return evidence;
22593
- if (isNodeOfType(node, "Identifier")) {
22594
- if (node.name === "undefined" || node.name === "NaN" || node.name === "Infinity") return evidence;
22595
- const reference = getRef(analysis, node);
22596
- if (!reference) {
22597
- evidence.hasUnknownSource = true;
22598
- return evidence;
22599
- }
22600
- const bindingIdentity = reference.resolved ?? node;
22601
- if (frame.introducedBindings.has(bindingIdentity)) {
22602
- const parent = node.parent;
22603
- if (parent && isNodeOfType(parent, "SpreadElement") && parent.parent && isNodeOfType(parent.parent, "ObjectExpression")) return evidence;
22604
- evidence.hasDeferredIntroducedValue = true;
22605
- evidence.hasUnknownSource = true;
22606
- return evidence;
22607
- }
22608
- const substitution = frame.substitutions.get(bindingIdentity);
22609
- if (substitution) return collectValueEvidence(analysis, substitution.expression, substitution.frame, remainingCallFrames, visitedBindings);
22610
- if (isProp(analysis, reference) || isState(analysis, reference)) {
22611
- evidence.sourceReferences.add(reference);
22612
- if (isState(analysis, reference) && isExternallyDrivenState(analysis, reference)) evidence.readsExternalValue = true;
22613
- return evidence;
22614
- }
22615
- if (!reference.resolved || visitedBindings.has(reference.resolved)) {
22616
- evidence.hasUnknownSource = true;
22617
- return evidence;
22618
- }
22619
- visitedBindings.add(reference.resolved);
22620
- const definitions = reference.resolved.defs;
22621
- if (definitions.some((definition) => definition.type === "ImportBinding")) {
22622
- evidence.hasUnknownSource = true;
22623
- return evidence;
22624
- }
22625
- if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) {
22626
- evidence.hasUnknownSource = true;
22627
- return evidence;
22628
- }
22629
- const initializer = definitions.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init));
22630
- if (!initializer || !isNodeOfType(initializer, "VariableDeclarator") || !initializer.init) {
22631
- evidence.hasUnknownSource = true;
22632
- return evidence;
22633
- }
22634
- if (isNodeOfType(initializer.init, "CallExpression") && isOpaqueHookCall(initializer.init)) {
22635
- evidence.readsExternalValue = true;
22636
- return evidence;
22637
- }
22638
- return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
22639
- }
22640
- if (isNodeOfType(node, "MemberExpression")) {
22641
- if (isNodeOfType(node.object, "Identifier")) {
22642
- const objectReference = getRef(analysis, node.object);
22643
- if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
22644
- evidence.hasUnknownSource = true;
22645
- return evidence;
22085
+ return handlerFunctions;
22086
+ };
22087
+ const collectSubscriptionCallbackFunctions = (analysis, effectFn) => {
22088
+ const registrationCalls = [...findDownstreamNodes(effectFn, "CallExpression"), ...findDownstreamNodes(effectFn, "NewExpression")].filter((call) => isSubscriptionRegistrationCall(call));
22089
+ const callbackFunctions = collectEventPropertyHandlerFunctions(analysis, effectFn);
22090
+ for (const registrationCall of registrationCalls) {
22091
+ if (!isNodeOfType(registrationCall, "CallExpression") && !isNodeOfType(registrationCall, "NewExpression")) continue;
22092
+ for (const argument of registrationCall.arguments ?? []) {
22093
+ const argumentNode = argument;
22094
+ if (isFunctionLike$1(argumentNode)) {
22095
+ callbackFunctions.push(argumentNode);
22096
+ continue;
22097
+ }
22098
+ if (isNodeOfType(argumentNode, "Identifier")) {
22099
+ const argumentRef = getRef(analysis, argumentNode);
22100
+ const resolvedFunction = argumentRef ? resolveToFunction(argumentRef) : null;
22101
+ if (resolvedFunction) callbackFunctions.push(resolvedFunction);
22646
22102
  }
22647
22103
  }
22648
- mergeEvidence(evidence, collectValueEvidence(analysis, node.object, frame, remainingCallFrames, new Set(visitedBindings)));
22649
- if (node.computed) mergeEvidence(evidence, collectValueEvidence(analysis, node.property, frame, remainingCallFrames, new Set(visitedBindings)));
22650
- return evidence;
22651
- }
22652
- if (isNodeOfType(node, "CallExpression")) {
22653
- if (isOpaqueHookCall(node)) {
22654
- evidence.readsExternalValue = true;
22655
- return evidence;
22656
- }
22657
- const callee = stripParenExpression(node.callee);
22658
- const calleeRoot = getMemberRoot(callee);
22659
- const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
22660
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
22661
- if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
22662
- for (const argument of node.arguments ?? []) {
22663
- if (isFunctionLike$1(argument)) continue;
22664
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
22665
- }
22666
- if (isPureGlobalCall || isPureMemberTransform) return evidence;
22667
- if (isNodeOfType(callee, "MemberExpression")) {
22668
- evidence.hasUnknownSource = true;
22669
- return evidence;
22670
- }
22671
- if (remainingCallFrames <= 0) {
22672
- evidence.hasUnknownSource = true;
22673
- return evidence;
22674
- }
22675
- const callable = resolveWrappedCallable(analysis, callee);
22676
- if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
22677
- evidence.hasUnknownSource = true;
22678
- return evidence;
22679
- }
22680
- const valueFrame = {
22681
- functionNode: callable,
22682
- invocation: node,
22683
- isDeferred: false,
22684
- introducedBindings: /* @__PURE__ */ new Set(),
22685
- substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
22686
- };
22687
- const returnedExpressions = getReturnedExpressions(callable);
22688
- if (returnedExpressions.length === 0) {
22689
- evidence.hasUnknownSource = true;
22690
- return evidence;
22691
- }
22692
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
22693
- return evidence;
22694
- }
22695
- if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
22696
- evidence.hasUnknownSource = true;
22697
- return evidence;
22698
- }
22699
- const nodeRecord = node;
22700
- for (const [key, value] of Object.entries(nodeRecord)) {
22701
- if (key === "parent" || key === "type" || key === "key") continue;
22702
- if (Array.isArray(value)) for (const child of value) {
22703
- if (!child || typeof child !== "object" || !("type" in child)) continue;
22704
- mergeEvidence(evidence, collectValueEvidence(analysis, child, frame, remainingCallFrames, new Set(visitedBindings)));
22705
- }
22706
- else if (value && typeof value === "object" && "type" in value) mergeEvidence(evidence, collectValueEvidence(analysis, value, frame, remainingCallFrames, new Set(visitedBindings)));
22707
22104
  }
22708
- return evidence;
22105
+ return callbackFunctions;
22709
22106
  };
22710
- const findStateSetterReference = (analysis, callExpression) => {
22711
- if (!isNodeOfType(callExpression, "CallExpression")) return null;
22712
- const callee = stripParenExpression(callExpression.callee);
22713
- if (!isNodeOfType(callee, "Identifier")) return null;
22714
- const reference = getRef(analysis, callee);
22715
- if (!reference) return null;
22716
- if (resolveToFunction(reference)) return null;
22717
- if (isStateSetter(analysis, reference)) return reference;
22718
- return getUpstreamRefs(analysis, reference).find((upstreamReference) => isStateSetter(analysis, upstreamReference)) ?? null;
22719
- };
22720
- const isSameSimpleValue = (analysis, leftExpression, rightExpression) => {
22721
- const left = stripParenExpression(leftExpression);
22722
- const right = stripParenExpression(rightExpression);
22723
- if (left.type !== right.type) return false;
22724
- if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
22725
- const leftBinding = getIdentifierBindingIdentity(analysis, left);
22726
- const rightBinding = getIdentifierBindingIdentity(analysis, right);
22727
- if (leftBinding || rightBinding) return leftBinding !== null && leftBinding === rightBinding;
22728
- return left.name === right.name;
22107
+ const isInsideAnyFunction = (identifier, functions) => {
22108
+ let current = identifier;
22109
+ while (current) {
22110
+ if (functions.includes(current)) return true;
22111
+ current = current.parent;
22729
22112
  }
22730
- if (isNodeOfType(left, "Literal") && isNodeOfType(right, "Literal")) return left.value === right.value;
22731
- if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return left.computed === right.computed && isSameSimpleValue(analysis, left.object, right.object) && isSameSimpleValue(analysis, left.property, right.property);
22732
22113
  return false;
22733
22114
  };
22734
- const matchesStateInitializer = (analysis, callExpression, stateDeclarator) => {
22735
- if (!isNodeOfType(callExpression, "CallExpression")) return false;
22736
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return false;
22737
- if (!isNodeOfType(stateDeclarator.init, "CallExpression")) return false;
22738
- const writtenValue = callExpression.arguments?.[0];
22739
- const initializerValue = stateDeclarator.init.arguments?.[0];
22740
- if (!writtenValue || !initializerValue) return false;
22741
- const unwrappedInitializer = stripParenExpression(initializerValue);
22742
- if (isNodeOfType(unwrappedInitializer, "LogicalExpression") && (unwrappedInitializer.operator === "??" || unwrappedInitializer.operator === "||")) return isSameSimpleValue(analysis, writtenValue, unwrappedInitializer.left) || isSameSimpleValue(analysis, writtenValue, unwrappedInitializer.right);
22743
- return isSameSimpleValue(analysis, writtenValue, unwrappedInitializer);
22744
- };
22745
- const collectFrameSetterCalls = (analysis, frame) => {
22746
- const calls = [];
22747
- walkAst(frame.functionNode, (child) => {
22748
- if (child !== frame.functionNode && isFunctionLike$1(child)) return false;
22749
- if (!isNodeOfType(child, "CallExpression")) return;
22750
- const setterReference = findStateSetterReference(analysis, child);
22751
- if (setterReference) calls.push({
22752
- callExpression: child,
22753
- setterReference
22754
- });
22115
+ const isSubscriptionRegistrationArgument = (identifier) => {
22116
+ const parent = identifier.parent;
22117
+ if (!parent || !isSubscriptionRegistrationCall(parent)) return false;
22118
+ return isCallArgumentOf(parent, identifier);
22119
+ };
22120
+ const hasSubscriptionCallbackCallToSameSetter = (analysis, setterRef, effectFnRefs, effectFn) => {
22121
+ const callbackFunctions = collectSubscriptionCallbackFunctions(analysis, effectFn);
22122
+ return effectFnRefs.some((otherRef) => {
22123
+ if (otherRef === setterRef) return false;
22124
+ if (otherRef.resolved !== setterRef.resolved) return false;
22125
+ const otherIdentifier = otherRef.identifier;
22126
+ if (isSubscriptionRegistrationArgument(otherIdentifier)) return true;
22127
+ if (!getCallExpr(otherRef)) return false;
22128
+ return isInsideAnyFunction(otherIdentifier, callbackFunctions);
22755
22129
  });
22756
- return calls;
22757
22130
  };
22758
- const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
22759
- let ancestor = leftNode.parent;
22760
- while (ancestor) {
22761
- if ((isNodeOfType(ancestor, "IfStatement") || isNodeOfType(ancestor, "ConditionalExpression")) && ancestor.alternate) {
22762
- const leftIsConsequent = isAstDescendant(leftNode, ancestor.consequent);
22763
- const leftIsAlternate = isAstDescendant(leftNode, ancestor.alternate);
22764
- const rightIsConsequent = isAstDescendant(rightNode, ancestor.consequent);
22765
- const rightIsAlternate = isAstDescendant(rightNode, ancestor.alternate);
22766
- if (leftIsConsequent && rightIsAlternate || leftIsAlternate && rightIsConsequent) return true;
22131
+ const derivesFromPostMountRead = (ref) => Boolean(ref.resolved?.defs.some((def) => {
22132
+ const node = def.node;
22133
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
22134
+ return readsPostMountValue(node.init);
22135
+ }));
22136
+ const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
22137
+ "then",
22138
+ "catch",
22139
+ "finally"
22140
+ ]);
22141
+ const isPromiseFlowFunction = (fn) => {
22142
+ if (isFunctionLike$1(fn) && fn.async) return true;
22143
+ const parent = fn.parent;
22144
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
22145
+ if (!(parent.arguments ?? []).some((argument) => argument === fn)) return false;
22146
+ const callee = parent.callee;
22147
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CONTINUATION_METHOD_NAMES.has(callee.property.name);
22148
+ };
22149
+ const isPromiseFlowStateSetterCall = (analysis, ref, effectFn) => {
22150
+ if (!isStateSetterCall(analysis, ref)) return false;
22151
+ let current = ref.identifier.parent;
22152
+ let sawPromiseFlowFunction = false;
22153
+ while (current && current !== effectFn) {
22154
+ if (isFunctionLike$1(current) && !sawPromiseFlowFunction) {
22155
+ if (!isPromiseFlowFunction(current)) return false;
22156
+ sawPromiseFlowFunction = true;
22767
22157
  }
22768
- ancestor = ancestor.parent;
22158
+ current = current.parent;
22769
22159
  }
22770
- return false;
22160
+ return sawPromiseFlowFunction;
22771
22161
  };
22772
- const collectEffectStateWriteFacts = (analysis, effectNode) => {
22773
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
22774
- if (frames.length === 0) return [];
22775
- const effectHasCleanup = hasCleanup(analysis, effectNode);
22776
- const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
22777
- const facts = [];
22778
- for (const frame of frames) for (const { callExpression, setterReference } of collectFrameSetterCalls(analysis, frame)) {
22779
- if (!isNodeOfType(callExpression, "CallExpression")) continue;
22780
- if ((callExpression.arguments ?? []).length !== 1) continue;
22781
- const writtenValue = callExpression.arguments?.[0];
22782
- if (!writtenValue) continue;
22783
- const stateDeclarator = getUseStateDecl(analysis, setterReference);
22784
- if (!stateDeclarator) continue;
22785
- const remainingValueCallFrames = frame === frames[0] ? 1 : 0;
22786
- let valueEvidence;
22787
- if (isFunctionLike$1(writtenValue)) {
22788
- const updaterFrame = {
22789
- functionNode: writtenValue,
22790
- invocation: callExpression,
22791
- isDeferred: frame.isDeferred,
22792
- introducedBindings: collectIntroducedBindings(analysis, writtenValue),
22793
- substitutions: /* @__PURE__ */ new Map()
22794
- };
22795
- valueEvidence = emptyEvidence();
22796
- const returnedExpressions = getReturnedExpressions(writtenValue);
22797
- if (returnedExpressions.length === 0) valueEvidence.hasUnknownSource = true;
22798
- for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
22799
- } else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
22800
- const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
22801
- const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
22802
- const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
22803
- if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
22804
- const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
22805
- facts.push({
22806
- callExpression,
22807
- executionNode: frame.invocation ?? callExpression,
22808
- setterReference,
22809
- stateDeclarator,
22810
- sourceReferences,
22811
- isDeferred: frame.isDeferred,
22812
- isRenderKnownCopy,
22813
- matchesStateInitializer: doesMatchStateInitializer,
22814
- resetsSourceState: false
22815
- });
22816
- }
22817
- return facts.map((fact) => {
22818
- const resetsSourceState = fact.sourceReferences.filter((sourceReference) => isState(analysis, sourceReference)).map((sourceReference) => getUseStateDecl(analysis, sourceReference)).filter((declarator) => Boolean(declarator)).some((sourceDeclarator) => facts.some((candidateFact) => !candidateFact.isDeferred && candidateFact.stateDeclarator === sourceDeclarator && !areInMutuallyExclusiveBranches(fact.executionNode, candidateFact.executionNode)));
22819
- const { executionNode: _executionNode, ...publicFact } = fact;
22820
- return {
22821
- ...publicFact,
22822
- resetsSourceState,
22823
- isRenderKnownCopy: fact.isRenderKnownCopy && !cleanupManagedStateDeclarators.has(fact.stateDeclarator) && !resetsSourceState
22824
- };
22825
- });
22162
+ const isObjectUrlLifecycleEffect = (effectFn) => {
22163
+ const callMethodNames = findDownstreamNodes(effectFn, "CallExpression").map((call) => getCallMethodName(call.callee)).filter((name) => Boolean(name));
22164
+ return callMethodNames.includes("createObjectURL") && callMethodNames.includes("revokeObjectURL");
22826
22165
  };
22827
- //#endregion
22828
- //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
22829
22166
  const noAdjustStateOnPropChange = defineRule({
22830
22167
  id: "no-adjust-state-on-prop-change",
22831
22168
  title: "State synced to a prop inside an effect",
@@ -22836,13 +22173,25 @@ const noAdjustStateOnPropChange = defineRule({
22836
22173
  if (!isUseEffect(node)) return;
22837
22174
  const analysis = getProgramAnalysis(node);
22838
22175
  if (!analysis) return;
22839
- const dependencyReferences = getEffectDepsRefs(analysis, node);
22840
- if (!dependencyReferences) return;
22841
- if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
22842
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
22843
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
22176
+ const effectFnRefs = getEffectFnRefs(analysis, node);
22177
+ const depsRefs = getEffectDepsRefs(analysis, node);
22178
+ if (!effectFnRefs || !depsRefs) return;
22179
+ const effectFn = getEffectFn(analysis, node);
22180
+ if (!effectFn) return;
22181
+ if (!depsRefs.flatMap((ref) => isState(analysis, ref) ? [] : getUpstreamRefs(analysis, ref)).some((ref) => isProp(analysis, ref))) return;
22182
+ if (isObjectUrlLifecycleEffect(effectFn)) return;
22183
+ const hasAsyncStateSetter = effectFnRefs.some((ref) => isPromiseFlowStateSetterCall(analysis, ref, effectFn));
22184
+ for (const ref of effectFnRefs) {
22185
+ if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
22186
+ const callExpr = getCallExpr(ref);
22187
+ if (!callExpr) continue;
22188
+ if (readsPostMountValue(callExpr)) continue;
22189
+ if (getArgsUpstreamRefs(analysis, ref).some((argRef) => derivesFromPostMountRead(argRef))) continue;
22190
+ if (isNodeOfType(callExpr, "CallExpression") && isConstantSetterArgument(analysis, callExpr) && hasSubscriptionCallbackCallToSameSetter(analysis, ref, effectFnRefs, effectFn)) continue;
22191
+ if (hasAsyncStateSetter && isNodeOfType(callExpr, "CallExpression") && isConstantSetterArgument(analysis, callExpr)) continue;
22192
+ if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
22844
22193
  context.report({
22845
- node: fact.callExpression,
22194
+ node: callExpr,
22846
22195
  message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
22847
22196
  });
22848
22197
  }
@@ -24856,6 +24205,180 @@ const noCascadingSetState = defineRule({
24856
24205
  } })
24857
24206
  });
24858
24207
  //#endregion
24208
+ //#region src/plugin/rules/state-and-effects/utils/reads-post-mount-through-locals.ts
24209
+ const isBareRefCurrentRead = (node) => isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === "current";
24210
+ const matchesPostMountRead = (node, options) => {
24211
+ if (isPostMountGlobalRead(node)) return true;
24212
+ if (!isPostMountMemberRead(node)) return false;
24213
+ return !(options.ignoreBareRefCurrent === true && isBareRefCurrentRead(node));
24214
+ };
24215
+ const objectPatternBindsName$1 = (pattern, name) => {
24216
+ if (!isNodeOfType(pattern, "ObjectPattern")) return false;
24217
+ return (pattern.properties ?? []).some((property) => {
24218
+ if (!isNodeOfType(property, "Property")) return false;
24219
+ const bound = property.value;
24220
+ return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
24221
+ });
24222
+ };
24223
+ const findEffectLocalInitializer$1 = (effectFn, name) => {
24224
+ let initializer = null;
24225
+ walkAst(effectFn, (child) => {
24226
+ if (initializer) return false;
24227
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
24228
+ if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName$1(child.id, name)) {
24229
+ initializer = child.init;
24230
+ return false;
24231
+ }
24232
+ });
24233
+ return initializer;
24234
+ };
24235
+ const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedLocalNames = /* @__PURE__ */ new Set()) => {
24236
+ let found = false;
24237
+ walkAst(root, (child) => {
24238
+ if (found) return false;
24239
+ if (matchesPostMountRead(child, options)) {
24240
+ found = true;
24241
+ return false;
24242
+ }
24243
+ if (!isNodeOfType(child, "Identifier")) return;
24244
+ if (visitedLocalNames.has(child.name)) return;
24245
+ visitedLocalNames.add(child.name);
24246
+ const localInitializer = findEffectLocalInitializer$1(effectFn, child.name);
24247
+ if (localInitializer && readsPostMountValueThroughLocals(localInitializer, effectFn, options, visitedLocalNames)) {
24248
+ found = true;
24249
+ return false;
24250
+ }
24251
+ });
24252
+ return found;
24253
+ };
24254
+ //#endregion
24255
+ //#region src/plugin/rules/state-and-effects/utils/effect/external-state.ts
24256
+ const DEFERRING_CALLEE_NAMES = new Set([
24257
+ "setTimeout",
24258
+ "setInterval",
24259
+ "setImmediate",
24260
+ "requestAnimationFrame",
24261
+ "requestIdleCallback",
24262
+ "queueMicrotask",
24263
+ "addEventListener",
24264
+ "addListener",
24265
+ "subscribe",
24266
+ "observe",
24267
+ "watch",
24268
+ "watchPosition",
24269
+ "then",
24270
+ "catch",
24271
+ "finally",
24272
+ "on",
24273
+ "once"
24274
+ ]);
24275
+ const getCalleeName$1 = (callee) => {
24276
+ if (!callee) return null;
24277
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
24278
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
24279
+ return null;
24280
+ };
24281
+ const parentOf = (node) => node.parent ?? null;
24282
+ const argumentsInclude = (args, target) => (args ?? []).some((argument) => argument === target);
24283
+ const isDeferredCallbackPosition = (expression) => {
24284
+ const parent = parentOf(expression);
24285
+ if (!parent) return false;
24286
+ if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
24287
+ const name = getCalleeName$1(parent.callee);
24288
+ if (name && DEFERRING_CALLEE_NAMES.has(name)) return true;
24289
+ }
24290
+ if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
24291
+ const name = getCalleeName$1(parent.callee);
24292
+ if (name && (name.endsWith("Observer") || name === "Promise")) return true;
24293
+ }
24294
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
24295
+ return false;
24296
+ };
24297
+ const getHandlerDeclarator = (fn) => {
24298
+ let current = fn;
24299
+ let parent = parentOf(current);
24300
+ while (parent && isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, current)) {
24301
+ current = parent;
24302
+ parent = parentOf(current);
24303
+ }
24304
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent;
24305
+ return null;
24306
+ };
24307
+ const isNamedHandlerUsedAsDeferredCallback = (analysis, fn) => {
24308
+ const declarator = getHandlerDeclarator(fn);
24309
+ if (!declarator || !isNodeOfType(declarator.id, "Identifier")) return false;
24310
+ const name = declarator.id.name;
24311
+ for (const scope of analysis.scopeManager.scopes) {
24312
+ const variable = scope.variables.find((candidate) => candidate.name === name && candidate.defs.some((def) => def.node === declarator));
24313
+ if (!variable) continue;
24314
+ return variable.references.some((reference) => isDeferredCallbackPosition(reference.identifier));
24315
+ }
24316
+ return false;
24317
+ };
24318
+ const isDeferredCallbackFunction = (analysis, fn) => {
24319
+ if (isDeferredCallbackPosition(fn)) return true;
24320
+ return isNamedHandlerUsedAsDeferredCallback(analysis, fn);
24321
+ };
24322
+ const isInsideDeferredCallback$1 = (analysis, node, boundary) => {
24323
+ let current = parentOf(node);
24324
+ while (current && current !== boundary) {
24325
+ if (isFunctionLike$1(current) && isDeferredCallbackFunction(analysis, current)) return true;
24326
+ current = parentOf(current);
24327
+ }
24328
+ return false;
24329
+ };
24330
+ const findUseStateDeclarator = (ref) => {
24331
+ for (const def of ref.resolved?.defs ?? []) {
24332
+ const node = def.node;
24333
+ if (!isNodeOfType(node, "VariableDeclarator")) continue;
24334
+ if (!isNodeOfType(node.init, "CallExpression")) continue;
24335
+ if (!isNodeOfType(node.id, "ArrayPattern")) continue;
24336
+ return node;
24337
+ }
24338
+ return null;
24339
+ };
24340
+ const declaratorToExternallyDriven = /* @__PURE__ */ new WeakMap();
24341
+ const isExternallyDrivenState = (analysis, ref) => {
24342
+ const declarator = findUseStateDeclarator(ref);
24343
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
24344
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
24345
+ const cached = declaratorToExternallyDriven.get(declarator);
24346
+ if (cached !== void 0) return cached;
24347
+ const result = computeExternallyDriven(analysis, declarator);
24348
+ declaratorToExternallyDriven.set(declarator, result);
24349
+ return result;
24350
+ };
24351
+ const computeExternallyDriven = (analysis, declarator) => {
24352
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
24353
+ const setterElement = declarator.id.elements?.[1];
24354
+ if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
24355
+ const setterName = setterElement.name;
24356
+ let setterVariable = null;
24357
+ for (const scope of analysis.scopeManager.scopes) {
24358
+ const match = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
24359
+ if (match) {
24360
+ setterVariable = match;
24361
+ break;
24362
+ }
24363
+ }
24364
+ if (!setterVariable) return false;
24365
+ let hasDeferredCallSite = false;
24366
+ for (const setterReference of setterVariable.references) {
24367
+ const identifier = setterReference.identifier;
24368
+ const parent = parentOf(identifier);
24369
+ if (!parent) continue;
24370
+ if (isDeferredCallbackPosition(identifier)) {
24371
+ hasDeferredCallSite = true;
24372
+ continue;
24373
+ }
24374
+ if (!isNodeOfType(parent, "CallExpression")) continue;
24375
+ if (parent.callee !== identifier) continue;
24376
+ if (!isInsideDeferredCallback$1(analysis, parent, declarator)) return false;
24377
+ hasDeferredCallSite = true;
24378
+ }
24379
+ return hasDeferredCallSite;
24380
+ };
24381
+ //#endregion
24859
24382
  //#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
24860
24383
  const getUseStateDeclarator = (ref) => (ref.resolved?.defs ?? []).map((def) => def.node).find((node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "ArrayPattern")) ?? null;
24861
24384
  const isDeclaredWithin = (node, container) => {
@@ -25596,16 +25119,322 @@ const noDefaultProps = defineRule({
25596
25119
  } })
25597
25120
  });
25598
25121
  //#endregion
25122
+ //#region src/plugin/utils/is-initial-only-prop-name.ts
25123
+ const isInitialOnlyPropName = (propName) => {
25124
+ if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
25125
+ return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
25126
+ };
25127
+ //#endregion
25128
+ //#region src/plugin/rules/state-and-effects/utils/event-handler-reference.ts
25129
+ const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
25130
+ const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
25131
+ if (!isNodeOfType(node, "MemberExpression")) return null;
25132
+ if (!isNodeOfType(node.object, "Identifier")) return null;
25133
+ const propertyName = getStaticMemberPropertyName(node);
25134
+ return propertyName ? `${resolveName(node.object.name)}.${propertyName}` : null;
25135
+ };
25136
+ const isEventHandlerValue = (node, eventHandlerReferenceNames, resolveName = (name) => name) => {
25137
+ if (isInlineFunctionExpression(node)) return true;
25138
+ if (isNodeOfType(node, "Identifier")) return eventHandlerReferenceNames.has(resolveName(node.name));
25139
+ const memberReferenceName = getStaticMemberReferenceName(node, resolveName);
25140
+ return Boolean(memberReferenceName && eventHandlerReferenceNames.has(memberReferenceName));
25141
+ };
25142
+ const isIntrinsicJsxAttribute = (node) => {
25143
+ if (!isNodeOfType(node, "JSXAttribute")) return false;
25144
+ const openingElement = node.parent;
25145
+ if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
25146
+ const elementName = openingElement.name;
25147
+ if (!isNodeOfType(elementName, "JSXIdentifier")) return false;
25148
+ return /^[a-z]/.test(elementName.name);
25149
+ };
25150
+ //#endregion
25151
+ //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
25152
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
25153
+ const collectComponentPropNames = (componentFunction) => {
25154
+ const cached = componentPropNamesCache.get(componentFunction);
25155
+ if (cached) return cached;
25156
+ const propNames = /* @__PURE__ */ new Set();
25157
+ if (!isFunctionLike$1(componentFunction)) return propNames;
25158
+ const propsObjectParamNames = /* @__PURE__ */ new Set();
25159
+ for (const param of componentFunction.params ?? []) {
25160
+ collectPatternNames(param, propNames);
25161
+ if (isNodeOfType(param, "Identifier")) propsObjectParamNames.add(param.name);
25162
+ }
25163
+ const componentBody = componentFunction.body;
25164
+ if (!componentBody) return propNames;
25165
+ walkAst(componentBody, (child) => {
25166
+ if (child !== componentBody && isFunctionLike$1(child)) return false;
25167
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
25168
+ });
25169
+ componentPropNamesCache.set(componentFunction, propNames);
25170
+ return propNames;
25171
+ };
25172
+ const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
25173
+ const getOwnScopeBoundNames = (functionNode) => {
25174
+ const cached = ownScopeBoundNamesCache.get(functionNode);
25175
+ if (cached) return cached;
25176
+ const boundNames = /* @__PURE__ */ new Set();
25177
+ if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
25178
+ walkAst(functionNode, (child) => {
25179
+ if (child !== functionNode && isFunctionLike$1(child)) return false;
25180
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
25181
+ });
25182
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
25183
+ return boundNames;
25184
+ };
25185
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
25186
+ const isPropertyNamePosition = (identifier) => {
25187
+ const parent = identifier.parent;
25188
+ if (!parent) return false;
25189
+ if (isNodeOfType(parent, "MemberExpression")) return parent.property === identifier && !parent.computed;
25190
+ if (isNodeOfType(parent, "Property")) return parent.key === identifier && !parent.computed;
25191
+ return false;
25192
+ };
25193
+ const referencesIdentifierNamed = (root, identifierName) => {
25194
+ let isReferenced = false;
25195
+ walkAst(root, (child) => {
25196
+ if (isReferenced) return false;
25197
+ if (child !== root && isFunctionLike$1(child) && declaresBindingNamed(child, identifierName)) return false;
25198
+ if (isNodeOfType(child, "Identifier") && child.name === identifierName && !isPropertyNamePosition(child)) {
25199
+ isReferenced = true;
25200
+ return false;
25201
+ }
25202
+ });
25203
+ return isReferenced;
25204
+ };
25205
+ const isSetterWiredToJsxHandler = (componentFunction, setterName) => {
25206
+ let isWired = false;
25207
+ walkAst(componentFunction, (child) => {
25208
+ if (isWired) return false;
25209
+ if (child !== componentFunction && isFunctionLike$1(child) && declaresBindingNamed(child, setterName)) return false;
25210
+ if (!isNodeOfType(child, "JSXAttribute") || !child.value) return;
25211
+ const attributeName = getJsxAttributeName(child.name);
25212
+ if (!attributeName || !isEventHandlerName(attributeName)) return;
25213
+ if (referencesIdentifierNamed(child.value, setterName)) {
25214
+ isWired = true;
25215
+ return false;
25216
+ }
25217
+ });
25218
+ return isWired;
25219
+ };
25220
+ const isControlledPropMirror = (effectNode, setterCall) => {
25221
+ if (!isNodeOfType(setterCall, "CallExpression")) return false;
25222
+ if (!isNodeOfType(setterCall.callee, "Identifier")) return false;
25223
+ const setterArguments = setterCall.arguments ?? [];
25224
+ if (setterArguments.length !== 1) return false;
25225
+ const mirroredArgument = setterArguments[0];
25226
+ if (!isNodeOfType(mirroredArgument, "Identifier")) return false;
25227
+ const componentFunction = findEnclosingFunction(effectNode);
25228
+ if (!componentFunction) return false;
25229
+ if (!collectComponentPropNames(componentFunction).has(mirroredArgument.name)) return false;
25230
+ return isSetterWiredToJsxHandler(componentFunction, setterCall.callee.name);
25231
+ };
25232
+ //#endregion
25233
+ //#region src/plugin/rules/state-and-effects/utils/has-user-input-setter-writer.ts
25234
+ const HANDLER_BINDING_NAME_PATTERN = /^(on|handle)[A-Z_]/;
25235
+ const isEventHandlerPropertyKey = (property) => isNodeOfType(property, "Property") && !property.computed && isNodeOfType(property.key, "Identifier") && isEventHandlerName(property.key.name);
25236
+ const DEFERRED_CALLBACK_MEMBER_NAMES = new Set([
25237
+ "then",
25238
+ "catch",
25239
+ "finally",
25240
+ "subscribe"
25241
+ ]);
25242
+ const DEFERRED_CALLBACK_CALLEE_NAMES = new Set([
25243
+ "setTimeout",
25244
+ "setInterval",
25245
+ "requestAnimationFrame",
25246
+ "addEventListener",
25247
+ "addListener"
25248
+ ]);
25249
+ const isDeferredCallbackArgumentOf = (callExpr, child) => {
25250
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25251
+ if (!(callExpr.arguments ?? []).includes(child)) return false;
25252
+ const callee = callExpr.callee;
25253
+ if (isNodeOfType(callee, "Identifier")) return DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.name);
25254
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return DEFERRED_CALLBACK_MEMBER_NAMES.has(callee.property.name) || DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.property.name);
25255
+ return false;
25256
+ };
25257
+ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDeferredWriters) => {
25258
+ let outermostFunctionBelowComponent = null;
25259
+ let previous = identifier;
25260
+ let cursor = identifier.parent;
25261
+ while (cursor && cursor !== componentFunction) {
25262
+ if (isNodeOfType(cursor, "JSXAttribute")) {
25263
+ const attributeName = getJsxAttributeName(cursor.name);
25264
+ if (attributeName && isEventHandlerName(attributeName)) return true;
25265
+ }
25266
+ if (isEventHandlerPropertyKey(cursor)) return true;
25267
+ if (includeDeferredWriters && isDeferredCallbackArgumentOf(cursor, previous)) return true;
25268
+ if (isFunctionLike$1(cursor)) {
25269
+ outermostFunctionBelowComponent = cursor;
25270
+ if (includeDeferredWriters && cursor.async === true) return true;
25271
+ }
25272
+ previous = cursor;
25273
+ cursor = cursor.parent ?? null;
25274
+ }
25275
+ if (!outermostFunctionBelowComponent) return false;
25276
+ const bindingName = getFunctionBindingName$1(outermostFunctionBelowComponent);
25277
+ if (!bindingName) return false;
25278
+ if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
25279
+ return isSetterWiredToJsxHandler(componentFunction, bindingName);
25280
+ };
25281
+ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters = false) => {
25282
+ if (!setterRef.resolved) return false;
25283
+ const componentFunction = findEnclosingFunction(effectNode);
25284
+ if (!componentFunction) return false;
25285
+ for (const reference of setterRef.resolved.references) {
25286
+ if (reference.init) continue;
25287
+ const identifier = reference.identifier;
25288
+ if (isAstDescendant(identifier, effectNode)) continue;
25289
+ if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
25290
+ }
25291
+ return false;
25292
+ };
25293
+ //#endregion
25599
25294
  //#region src/plugin/rules/state-and-effects/no-derived-state.ts
25600
- const getStateName$1 = (stateDeclarator) => {
25601
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
25602
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
25603
- const stateBinding = stateDeclarator.id.elements?.[0];
25604
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
25605
- const setterBinding = stateDeclarator.id.elements?.[1];
25606
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
25607
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
25608
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
25295
+ const countSetterCallSites = (ref) => {
25296
+ if (!ref.resolved) return 0;
25297
+ let count = 0;
25298
+ for (const reference of ref.resolved.references) {
25299
+ const parent = reference.identifier.parent;
25300
+ if (parent && isNodeOfType(parent, "CallExpression")) count += 1;
25301
+ }
25302
+ return count;
25303
+ };
25304
+ const collectUpdaterParameterReads = (analysis, updaterFn) => {
25305
+ const reads = [];
25306
+ for (const ref of getDownstreamRefs(analysis, updaterFn.body)) if (ref.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updaterFn)) reads.push(ref.identifier);
25307
+ return reads;
25308
+ };
25309
+ const readsParameterOnlyViaObjectSpread = (parameterReads) => parameterReads.every((read) => {
25310
+ const spread = read.parent;
25311
+ if (!spread || !isNodeOfType(spread, "SpreadElement")) return false;
25312
+ const container = spread.parent;
25313
+ return Boolean(container && isNodeOfType(container, "ObjectExpression"));
25314
+ });
25315
+ const isAccumulatingFunctionalUpdater = (analysis, callExpr) => {
25316
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25317
+ const firstArgument = callExpr.arguments?.[0];
25318
+ if (!firstArgument || !isFunctionLike$1(firstArgument)) return false;
25319
+ const parameterReads = collectUpdaterParameterReads(analysis, firstArgument);
25320
+ if (parameterReads.length === 0) return false;
25321
+ return !readsParameterOnlyViaObjectSpread(parameterReads);
25322
+ };
25323
+ const deriveStateNameFromSetterName = (setterName) => {
25324
+ if (setterName.length > 3 && setterName.startsWith("set")) return setterName[3].toLowerCase() + setterName.slice(4);
25325
+ return setterName;
25326
+ };
25327
+ const getStateNameForUseStateDecl = (useStateNode) => {
25328
+ if (!useStateNode || !isNodeOfType(useStateNode, "VariableDeclarator")) return null;
25329
+ if (!isNodeOfType(useStateNode.id, "ArrayPattern")) return null;
25330
+ const elements = useStateNode.id.elements ?? [];
25331
+ const stateSlot = elements[0];
25332
+ if (stateSlot && isNodeOfType(stateSlot, "Identifier")) return stateSlot.name;
25333
+ const setterSlot = elements[1];
25334
+ if (setterSlot && isNodeOfType(setterSlot, "Identifier")) return deriveStateNameFromSetterName(setterSlot.name);
25335
+ return null;
25336
+ };
25337
+ const isUseStateCallExpression = (callExpr) => {
25338
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25339
+ const callee = callExpr.callee;
25340
+ if (isNodeOfType(callee, "Identifier")) return callee.name === "useState";
25341
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useState";
25342
+ };
25343
+ const getSetterArgsUpstreamRefs = (analysis, ref) => {
25344
+ const result = [];
25345
+ for (const upRef of getUpstreamRefs(analysis, ref)) {
25346
+ const callExpr = getCallExpr(upRef);
25347
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
25348
+ if (isUseStateCallExpression(callExpr)) continue;
25349
+ for (const argument of callExpr.arguments ?? []) for (const argRef of getDownstreamRefs(analysis, argument)) for (const innerRef of getUpstreamRefs(analysis, argRef)) result.push(innerRef);
25350
+ }
25351
+ return result;
25352
+ };
25353
+ const refResolvesToDeclarator = (ref, declarator) => Boolean(declarator && ref.resolved?.defs.some((def) => def.node === declarator));
25354
+ const isUseContextLocal = (ref) => Boolean(ref.resolved?.defs.some((def) => {
25355
+ const node = def.node;
25356
+ if (!isNodeOfType(node, "VariableDeclarator")) return false;
25357
+ if (!isNodeOfType(node.init, "CallExpression")) return false;
25358
+ const callee = node.init.callee;
25359
+ if (isNodeOfType(callee, "Identifier")) return callee.name === "useContext";
25360
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useContext";
25361
+ }));
25362
+ const isPlainValueReadExpression = (node) => {
25363
+ if (isNodeOfType(node, "Identifier")) return true;
25364
+ if (isNodeOfType(node, "MemberExpression")) {
25365
+ if (node.computed && !isNodeOfType(node.property, "Literal")) return false;
25366
+ return isPlainValueReadExpression(node.object);
25367
+ }
25368
+ return false;
25369
+ };
25370
+ const isWholeValueMirrorArgument = (callExpr) => {
25371
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25372
+ const args = callExpr.arguments ?? [];
25373
+ if (args.length !== 1) return false;
25374
+ return isPlainValueReadExpression(args[0]);
25375
+ };
25376
+ const collectEarlyReturnGuardTests = (block, beforeStatement) => {
25377
+ const tests = [];
25378
+ if (!isNodeOfType(block, "BlockStatement")) return tests;
25379
+ for (const statement of block.body ?? []) {
25380
+ if (statement === beforeStatement) break;
25381
+ if (!isNodeOfType(statement, "IfStatement")) continue;
25382
+ const consequent = statement.consequent;
25383
+ if (isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isNodeOfType(inner, "ReturnStatement"))) tests.push(statement.test);
25384
+ }
25385
+ return tests;
25386
+ };
25387
+ const collectGuardTestsOnPath = (callExpr, effectFn) => {
25388
+ const guardTests = [];
25389
+ let child = callExpr;
25390
+ let parent = callExpr.parent;
25391
+ while (parent && child !== effectFn) {
25392
+ if (isNodeOfType(parent, "IfStatement") && parent.test !== child) guardTests.push(parent.test);
25393
+ else if (isNodeOfType(parent, "ConditionalExpression") && parent.test !== child) guardTests.push(parent.test);
25394
+ else if (isNodeOfType(parent, "LogicalExpression") && parent.right === child) guardTests.push(parent.left);
25395
+ else if (isNodeOfType(parent, "BlockStatement")) guardTests.push(...collectEarlyReturnGuardTests(parent, child));
25396
+ child = parent;
25397
+ parent = parent.parent ?? null;
25398
+ }
25399
+ return guardTests;
25400
+ };
25401
+ const someGuardReadsWrittenState = (analysis, guardTests, writtenStateDecl) => guardTests.some((test) => getDownstreamRefs(analysis, test).some((testRef) => getUpstreamRefs(analysis, testRef).some((upRef) => isState(analysis, upRef) && refResolvesToDeclarator(upRef, writtenStateDecl))));
25402
+ const copiesOtherStateOrContextValue = (analysis, callExpr, writtenStateDecl) => {
25403
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25404
+ const argument = callExpr.arguments?.[0];
25405
+ if (!argument) return false;
25406
+ for (const argRef of getDownstreamRefs(analysis, argument)) {
25407
+ if (isState(analysis, argRef) && !refResolvesToDeclarator(argRef, writtenStateDecl)) return true;
25408
+ for (const upRef of getUpstreamRefs(analysis, argRef)) if (isUseContextLocal(upRef)) return true;
25409
+ }
25410
+ return false;
25411
+ };
25412
+ const effectResetsArgumentSourceState = (analysis, effectFnRefs, effectFn, flaggedRef, argsUpstreamRefs) => {
25413
+ const sourceStateDecls = /* @__PURE__ */ new Set();
25414
+ for (const argRef of argsUpstreamRefs) {
25415
+ if (!isState(analysis, argRef)) continue;
25416
+ for (const def of argRef.resolved?.defs ?? []) sourceStateDecls.add(def.node);
25417
+ }
25418
+ if (sourceStateDecls.size === 0) return false;
25419
+ for (const otherRef of effectFnRefs) {
25420
+ if (otherRef === flaggedRef) continue;
25421
+ if (!isSyncStateSetterCall(analysis, otherRef, effectFn)) continue;
25422
+ const otherDecl = getUseStateDecl(analysis, otherRef);
25423
+ if (otherDecl && sourceStateDecls.has(otherDecl)) return true;
25424
+ }
25425
+ return false;
25426
+ };
25427
+ const hasReferenceOutsideEffect = (ref, effectNode) => {
25428
+ if (!ref.resolved) return false;
25429
+ const effectRange = effectNode.range;
25430
+ if (!effectRange) return false;
25431
+ for (const reference of ref.resolved.references) {
25432
+ if (reference.init) continue;
25433
+ const identifier = reference.identifier;
25434
+ if (!identifier.range) continue;
25435
+ if (!(effectRange[0] <= identifier.range[0] && identifier.range[1] <= effectRange[1])) return true;
25436
+ }
25437
+ return false;
25609
25438
  };
25610
25439
  const noDerivedState = defineRule({
25611
25440
  id: "no-derived-state",
@@ -25617,18 +25446,124 @@ const noDerivedState = defineRule({
25617
25446
  if (!isUseEffect(node)) return;
25618
25447
  const analysis = getProgramAnalysis(node);
25619
25448
  if (!analysis) return;
25620
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
25621
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
25622
- const stateName = getStateName$1(fact.stateDeclarator);
25623
- context.report({
25624
- node: fact.callExpression,
25449
+ if (hasCleanup(analysis, node)) return;
25450
+ const effectFnRefs = getEffectFnRefs(analysis, node);
25451
+ const depsRefs = getEffectDepsRefs(analysis, node);
25452
+ if (!effectFnRefs || !depsRefs) return;
25453
+ const effectFn = getEffectFn(analysis, node);
25454
+ if (!effectFn) return;
25455
+ for (const ref of effectFnRefs) {
25456
+ if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
25457
+ const callExpr = getCallExpr(ref);
25458
+ if (!callExpr) continue;
25459
+ if (readsPostMountValueThroughLocals(callExpr, effectFn)) continue;
25460
+ const useStateNode = getUseStateDecl(analysis, ref);
25461
+ const stateName = getStateNameForUseStateDecl(useStateNode) ?? "<state>";
25462
+ const argsUpstreamRefs = getSetterArgsUpstreamRefs(analysis, ref);
25463
+ const depsUpstreamRefs = depsRefs.flatMap((depRef) => getUpstreamRefs(analysis, depRef));
25464
+ if (isInitialOnlySetterCall(callExpr)) continue;
25465
+ if (isControlledPropMirror(node, callExpr)) continue;
25466
+ if (isAccumulatingFunctionalUpdater(analysis, callExpr)) continue;
25467
+ if (effectResetsArgumentSourceState(analysis, effectFnRefs, effectFn, ref, argsUpstreamRefs)) continue;
25468
+ const guardTestsOnPath = collectGuardTestsOnPath(callExpr, effectFn);
25469
+ const upstreamSetterRef = getUpstreamRefs(analysis, ref).find((upRef) => isStateSetter(analysis, upRef)) ?? null;
25470
+ if (guardTestsOnPath.length > 0 && upstreamSetterRef !== null && hasUserInputSetterWriter(upstreamSetterRef, node, true) && (!isWholeValueMirrorArgument(callExpr) || copiesOtherStateOrContextValue(analysis, callExpr, useStateNode)) && !someGuardReadsWrittenState(analysis, guardTestsOnPath, useStateNode)) continue;
25471
+ const isSomeArgsInternal = argsUpstreamRefs.some((argRef) => isState(analysis, argRef) || isProp(analysis, argRef));
25472
+ const isValueAlwaysInSync = argsUpstreamRefs.length > 0 && argsUpstreamRefs.every((argRef) => depsUpstreamRefs.some((depRef) => argRef.resolved === depRef.resolved)) && countSetterCallSites(ref) === 1 && !hasReferenceOutsideEffect(ref, node);
25473
+ if (isSomeArgsInternal) context.report({
25474
+ node: callExpr,
25625
25475
  message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
25626
25476
  });
25477
+ else if (isValueAlwaysInSync) context.report({
25478
+ node: callExpr,
25479
+ message: `"${stateName}" is only set here from other values, so storing it costs an extra render.`
25480
+ });
25627
25481
  }
25628
25482
  } })
25629
25483
  });
25484
+ const isInitialOnlySetterCall = (callExpr) => {
25485
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
25486
+ const args = callExpr.arguments ?? [];
25487
+ if (args.length !== 1) return false;
25488
+ const arg = args[0];
25489
+ if (!isNodeOfType(arg, "Identifier")) return false;
25490
+ return isInitialOnlyPropName(arg.name);
25491
+ };
25492
+ //#endregion
25493
+ //#region src/plugin/utils/is-no-op-statement.ts
25494
+ const isNoOpStatement = (statement) => {
25495
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
25496
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
25497
+ const expression = stripParenExpression(statement.expression);
25498
+ if (isNodeOfType(expression, "Literal")) return true;
25499
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
25500
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
25501
+ return false;
25502
+ };
25503
+ //#endregion
25504
+ //#region src/plugin/utils/get-callback-statements.ts
25505
+ const getCallbackStatements = (callback) => {
25506
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
25507
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
25508
+ };
25630
25509
  //#endregion
25631
25510
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
25511
+ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE__ */ new Set()) => {
25512
+ if (!node || typeof node !== "object") return;
25513
+ if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) {
25514
+ const nestedBindingNames = new Set(localBindingNames);
25515
+ for (const parameter of node.params ?? []) if (isNodeOfType(parameter, "Identifier")) nestedBindingNames.add(parameter.name);
25516
+ collectValueIdentifierNames(node.body, into, nestedBindingNames);
25517
+ return;
25518
+ }
25519
+ if (isNodeOfType(node, "CallExpression")) {
25520
+ if (isNodeOfType(node.callee, "MemberExpression")) {
25521
+ const rootName = getRootIdentifierName(node.callee);
25522
+ if (!rootName || !BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) collectValueIdentifierNames(node.callee.object, into, localBindingNames);
25523
+ }
25524
+ for (const argument of node.arguments ?? []) collectValueIdentifierNames(argument, into, localBindingNames);
25525
+ return;
25526
+ }
25527
+ if (isNodeOfType(node, "MemberExpression")) {
25528
+ const rootName = getRootIdentifierName(node);
25529
+ if (!rootName || !BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) collectValueIdentifierNames(node.object, into, localBindingNames);
25530
+ if (node.computed) collectValueIdentifierNames(node.property, into, localBindingNames);
25531
+ return;
25532
+ }
25533
+ if (isNodeOfType(node, "Identifier")) {
25534
+ if (!localBindingNames.has(node.name)) into.push(node.name);
25535
+ return;
25536
+ }
25537
+ const nodeRecord = node;
25538
+ for (const key of Object.keys(nodeRecord)) {
25539
+ if (key === "parent" || key === "type") continue;
25540
+ const child = nodeRecord[key];
25541
+ if (Array.isArray(child)) {
25542
+ for (const item of child) if (item && typeof item === "object" && "type" in item) collectValueIdentifierNames(item, into, localBindingNames);
25543
+ } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
25544
+ }
25545
+ };
25546
+ const flattenGuardedStatements = (statements) => {
25547
+ const flattened = [];
25548
+ for (const statement of statements) {
25549
+ if (isNoOpStatement(statement)) continue;
25550
+ if (isNodeOfType(statement, "ExpressionStatement")) {
25551
+ flattened.push(statement);
25552
+ continue;
25553
+ }
25554
+ if (isNodeOfType(statement, "IfStatement")) {
25555
+ for (const branch of [statement.consequent, statement.alternate]) {
25556
+ if (!branch) continue;
25557
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
25558
+ if (flattenedBranch === null) return null;
25559
+ flattened.push(...flattenedBranch);
25560
+ }
25561
+ continue;
25562
+ }
25563
+ return null;
25564
+ }
25565
+ return flattened;
25566
+ };
25632
25567
  const noDerivedStateEffect = defineRule({
25633
25568
  id: "no-derived-state-effect",
25634
25569
  title: "Derived state stored in an effect",
@@ -25636,13 +25571,83 @@ const noDerivedStateEffect = defineRule({
25636
25571
  tags: ["test-noise"],
25637
25572
  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",
25638
25573
  create: (context) => ({ CallExpression(node) {
25639
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25640
- const analysis = getProgramAnalysis(node);
25641
- if (!analysis) return;
25642
- if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
25574
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
25575
+ const callback = getEffectCallback(node);
25576
+ if (!callback) return;
25577
+ const depsNode = node.arguments[1];
25578
+ if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
25579
+ const dependencyNames = /* @__PURE__ */ new Set();
25580
+ for (const element of depsNode.elements ?? []) {
25581
+ if (isNodeOfType(element, "Identifier")) dependencyNames.add(element.name);
25582
+ if (isNodeOfType(element, "MemberExpression")) {
25583
+ const rootName = getRootIdentifierName(element);
25584
+ if (rootName) dependencyNames.add(rootName);
25585
+ }
25586
+ }
25587
+ if (dependencyNames.size === 0) return;
25588
+ let allDepsAreInitialOnly = true;
25589
+ let sawAnyDep = false;
25590
+ for (const name of dependencyNames) {
25591
+ sawAnyDep = true;
25592
+ if (!isInitialOnlyPropName(name)) {
25593
+ allDepsAreInitialOnly = false;
25594
+ break;
25595
+ }
25596
+ }
25597
+ if (sawAnyDep && allDepsAreInitialOnly) return;
25598
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
25599
+ if (statements === null || statements.length === 0) return;
25600
+ if (!statements.every((statement) => {
25601
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
25602
+ const expression = statement.expression;
25603
+ if (!isSetterCall(expression)) return false;
25604
+ if (!isNodeOfType(expression, "CallExpression")) return false;
25605
+ if (!isNodeOfType(expression.callee, "Identifier")) return false;
25606
+ return isUseStateSetterInScope(expression, expression.callee.name);
25607
+ })) return;
25608
+ if (statements.some((statement) => {
25609
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
25610
+ return isControlledPropMirror(node, statement.expression);
25611
+ })) return;
25612
+ let allArgumentsDeriveFromDeps = true;
25613
+ let hasAnyDependencyReference = false;
25614
+ let hasExpensiveDerivation = false;
25615
+ for (const statement of statements) {
25616
+ if (!isNodeOfType(statement, "ExpressionStatement")) continue;
25617
+ if (!isNodeOfType(statement.expression, "CallExpression")) continue;
25618
+ const setStateArguments = statement.expression.arguments;
25619
+ if (!setStateArguments?.length) continue;
25620
+ const valueIdentifierNames = [];
25621
+ collectValueIdentifierNames(setStateArguments[0], valueIdentifierNames);
25622
+ walkAst(setStateArguments[0], (child) => {
25623
+ if (!isNodeOfType(child, "CallExpression")) return;
25624
+ if (isNodeOfType(child.callee, "MemberExpression")) {
25625
+ const rootName = getRootIdentifierName(child.callee);
25626
+ if (rootName && BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) return;
25627
+ hasExpensiveDerivation = true;
25628
+ return;
25629
+ }
25630
+ if (isNodeOfType(child.callee, "Identifier")) {
25631
+ const calleeName = child.callee.name;
25632
+ if (!TRIVIAL_DERIVATION_CALLEE_NAMES.has(calleeName) && !isSetterIdentifier(calleeName)) hasExpensiveDerivation = true;
25633
+ }
25634
+ });
25635
+ const nonSetterIdentifiers = valueIdentifierNames.filter((name) => !isSetterIdentifier(name));
25636
+ if (nonSetterIdentifiers.some((name) => dependencyNames.has(name))) hasAnyDependencyReference = true;
25637
+ if (nonSetterIdentifiers.some((name) => !dependencyNames.has(name))) {
25638
+ allArgumentsDeriveFromDeps = false;
25639
+ break;
25640
+ }
25641
+ }
25642
+ if (!allArgumentsDeriveFromDeps) return;
25643
+ if (hasExpensiveDerivation) hasAnyDependencyReference = true;
25644
+ let message;
25645
+ if (!hasAnyDependencyReference) message = "Your users briefly see stale state on every prop change.";
25646
+ else if (hasExpensiveDerivation) message = "You pay an extra render for state derived from other values.";
25647
+ else message = "You pay an extra render for state you can derive from other values.";
25643
25648
  context.report({
25644
25649
  node,
25645
- message: "You pay an extra render for state you can derive from other values."
25650
+ message
25646
25651
  });
25647
25652
  } })
25648
25653
  });
@@ -25780,12 +25785,6 @@ const createComponentPropStackTracker = (callbacks) => {
25780
25785
  };
25781
25786
  };
25782
25787
  //#endregion
25783
- //#region src/plugin/utils/is-initial-only-prop-name.ts
25784
- const isInitialOnlyPropName = (propName) => {
25785
- if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
25786
- return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
25787
- };
25788
- //#endregion
25789
25788
  //#region src/plugin/rules/state-and-effects/no-derived-use-state.ts
25790
25789
  const isInitialOnlySeedName = (propName) => isInitialOnlyPropName(propName) || propName === "initial" || propName === "autoFocus" || propName === "autoPlay" || propName === "startOpen" || /^initially[A-Z]/.test(propName) || /Initial([A-Z]|$)/.test(propName);
25791
25790
  const SNAPSHOT_STATE_NAME_PATTERN = /^(initial|previous|prev|preserved|saved|original|cached|snapshot|prior|debounced|deferred)([A-Z_]|$)/;
@@ -27219,23 +27218,6 @@ const noEffectChain = defineRule({
27219
27218
  }
27220
27219
  });
27221
27220
  //#endregion
27222
- //#region src/plugin/utils/is-no-op-statement.ts
27223
- const isNoOpStatement = (statement) => {
27224
- if (isNodeOfType(statement, "EmptyStatement")) return true;
27225
- if (!isNodeOfType(statement, "ExpressionStatement")) return false;
27226
- const expression = stripParenExpression(statement.expression);
27227
- if (isNodeOfType(expression, "Literal")) return true;
27228
- if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
27229
- if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
27230
- return false;
27231
- };
27232
- //#endregion
27233
- //#region src/plugin/utils/get-callback-statements.ts
27234
- const getCallbackStatements = (callback) => {
27235
- if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
27236
- return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
27237
- };
27238
- //#endregion
27239
27221
  //#region src/plugin/rules/state-and-effects/utils/find-triggered-side-effect-callee-name.ts
27240
27222
  const findTriggeredSideEffectCalleeName = (consequentNode) => {
27241
27223
  let foundCalleeName = null;
@@ -27590,186 +27572,397 @@ const noEval = defineRule({
27590
27572
  })
27591
27573
  });
27592
27574
  //#endregion
27593
- //#region src/plugin/rules/state-and-effects/utils/is-state-written-only-from-event-handlers.ts
27594
- const isEventAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
27595
- const isEventProperty = (node) => isNodeOfType(node, "Property") && !node.computed && isNodeOfType(node.key, "Identifier") && /^on[A-Z]/.test(node.key.name);
27596
- const findVariableForDefinition = (analysis, definitionNode) => {
27597
- for (const scope of analysis.scopeManager.scopes) {
27598
- const variable = scope.variables.find((candidate) => candidate.defs.some((definition) => definition.node === definitionNode));
27599
- if (variable) return variable;
27575
+ //#region src/plugin/rules/state-and-effects/no-event-handler.ts
27576
+ const SETTER_NAME_PATTERN = /^set[A-Z]/;
27577
+ const DISPATCH_NAME_PATTERN = /^dispatch$|Dispatch$/;
27578
+ const HOOK_NAME_PATTERN$2 = /^use[A-Z0-9]/;
27579
+ const isSetterLikeName = (name) => SETTER_NAME_PATTERN.test(name) || DISPATCH_NAME_PATTERN.test(name);
27580
+ const isSetterCallExpressionStatement = (node) => {
27581
+ if (!isNodeOfType(node, "ExpressionStatement")) return false;
27582
+ let expression = node.expression;
27583
+ if (expression && isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
27584
+ if (!expression) return false;
27585
+ if (isNodeOfType(expression, "CallExpression")) {
27586
+ const callee = expression.callee;
27587
+ if (isNodeOfType(callee, "Identifier")) return isSetterLikeName(callee.name);
27588
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isSetterLikeName(callee.property.name)) return true;
27589
+ return false;
27600
27590
  }
27601
- return null;
27602
- };
27603
- const findFunctionDefinitionNode = (functionNode) => {
27604
- if (isNodeOfType(functionNode, "FunctionDeclaration")) return functionNode;
27605
- let current = functionNode;
27606
- while (current) {
27607
- if (isNodeOfType(current, "VariableDeclarator")) return current;
27608
- if (isNodeOfType(current, "Program") || isFunctionLike$1(current) && current !== functionNode) return null;
27609
- current = current.parent;
27591
+ if (isNodeOfType(expression, "AssignmentExpression")) {
27592
+ const left = expression.left;
27593
+ if (isNodeOfType(left, "MemberExpression") && !left.computed && isNodeOfType(left.property, "Identifier") && left.property.name === "current" && isNodeOfType(left.object, "Identifier")) return true;
27610
27594
  }
27611
- return null;
27595
+ return false;
27612
27596
  };
27613
- const isInsideInlineEventHandler = (node, boundary) => {
27614
- let current = node;
27615
- while (current && current !== boundary) {
27616
- if (isEventAttribute(current) || isEventProperty(current)) return true;
27617
- current = current.parent;
27597
+ const REF_GUARD_SCAN_BUDGET = 50;
27598
+ const containsRefGuard = (testNode) => {
27599
+ const stack = [testNode];
27600
+ let budget = REF_GUARD_SCAN_BUDGET;
27601
+ while (stack.length > 0 && budget-- > 0) {
27602
+ const node = stack.pop();
27603
+ if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === "current" && isNodeOfType(node.object, "Identifier")) {
27604
+ const name = node.object.name;
27605
+ if (name === "ref" || name.endsWith("Ref") || name.endsWith("ref")) return true;
27606
+ }
27607
+ if (isNodeOfType(node, "LogicalExpression") || isNodeOfType(node, "BinaryExpression")) stack.push(node.left, node.right);
27608
+ else if (isNodeOfType(node, "UnaryExpression")) stack.push(node.argument);
27609
+ else if (isNodeOfType(node, "ConditionalExpression")) stack.push(node.test, node.consequent, node.alternate);
27610
+ else if (isNodeOfType(node, "MemberExpression")) stack.push(node.object);
27611
+ else if (isNodeOfType(node, "ChainExpression")) stack.push(node.expression);
27618
27612
  }
27619
27613
  return false;
27620
27614
  };
27621
- const isFunctionWiredToEventAttribute = (analysis, functionNode) => {
27622
- const definitionNode = findFunctionDefinitionNode(functionNode);
27623
- if (!definitionNode) return false;
27624
- const variable = findVariableForDefinition(analysis, definitionNode);
27625
- if (!variable) return false;
27626
- return variable.references.some((reference) => {
27627
- let current = reference.identifier;
27628
- while (current && !isFunctionLike$1(current)) {
27629
- if (isEventAttribute(current) || isEventProperty(current)) return true;
27630
- current = current.parent;
27615
+ const COMPARISON_OPERATORS$1 = new Set([
27616
+ "==",
27617
+ "===",
27618
+ "!=",
27619
+ "!==",
27620
+ "<",
27621
+ "<=",
27622
+ ">",
27623
+ ">="
27624
+ ]);
27625
+ const isLiteralLikeNode = (node) => isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateLiteral") || isNodeOfType(node, "UnaryExpression") && isNodeOfType(node.argument, "Literal");
27626
+ const isPreviousValueComparisonSide = (analysis, side) => {
27627
+ let current = side;
27628
+ for (;;) if (isNodeOfType(current, "ChainExpression")) current = current.expression;
27629
+ else if (isNodeOfType(current, "TSNonNullExpression")) current = current.expression;
27630
+ else if (isNodeOfType(current, "MemberExpression")) {
27631
+ if (!current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "current" && isNodeOfType(current.object, "Identifier")) {
27632
+ const objectRef = getRef(analysis, current.object);
27633
+ if (objectRef && isUseRefBinding(objectRef)) return true;
27631
27634
  }
27632
- return false;
27633
- });
27635
+ current = current.object;
27636
+ } else break;
27637
+ if (!isNodeOfType(current, "Identifier")) return false;
27638
+ const baseRef = getRef(analysis, current);
27639
+ return Boolean(baseRef && isOpaqueHookResultBinding(baseRef));
27640
+ };
27641
+ const containsPreviousValueComparison = (analysis, testNode) => {
27642
+ const stack = [testNode];
27643
+ let budget = REF_GUARD_SCAN_BUDGET;
27644
+ while (stack.length > 0 && budget-- > 0) {
27645
+ const node = stack.pop();
27646
+ if (isNodeOfType(node, "BinaryExpression")) {
27647
+ if (COMPARISON_OPERATORS$1.has(node.operator)) {
27648
+ const left = node.left;
27649
+ const right = node.right;
27650
+ if (isPreviousValueComparisonSide(analysis, left) && !isLiteralLikeNode(right)) return true;
27651
+ if (isPreviousValueComparisonSide(analysis, right) && !isLiteralLikeNode(left)) return true;
27652
+ }
27653
+ stack.push(node.left, node.right);
27654
+ } else if (isNodeOfType(node, "LogicalExpression")) stack.push(node.left, node.right);
27655
+ else if (isNodeOfType(node, "UnaryExpression")) stack.push(node.argument);
27656
+ else if (isNodeOfType(node, "ConditionalExpression")) stack.push(node.test, node.consequent, node.alternate);
27657
+ else if (isNodeOfType(node, "ChainExpression")) stack.push(node.expression);
27658
+ }
27659
+ return false;
27634
27660
  };
27635
- const isFunctionCalledOnlyFromHandlers = (analysis, functionNode, componentFunction) => {
27636
- const definitionNode = findFunctionDefinitionNode(functionNode);
27637
- if (!definitionNode) return false;
27638
- const variable = findVariableForDefinition(analysis, definitionNode);
27639
- if (!variable) return false;
27640
- let hasCall = false;
27641
- for (const reference of variable.references) {
27642
- const identifier = reference.identifier;
27643
- const parent = identifier.parent;
27644
- if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) continue;
27645
- hasCall = true;
27646
- if (!isInsideProvenEventHandler(analysis, parent, componentFunction, false)) return false;
27647
- }
27648
- return hasCall;
27649
- };
27650
- const isFunctionUsedOutsideHandlers = (analysis, functionNode, componentFunction) => {
27651
- const definitionNode = findFunctionDefinitionNode(functionNode);
27652
- if (!definitionNode) return true;
27653
- const variable = findVariableForDefinition(analysis, definitionNode);
27654
- if (!variable) return true;
27655
- return variable.references.some((reference) => {
27656
- if (reference.init) return false;
27657
- const identifier = reference.identifier;
27658
- if (isInsideInlineEventHandler(identifier, componentFunction)) return false;
27659
- const parent = identifier.parent;
27660
- if (parent && isNodeOfType(parent, "CallExpression") && parent.callee === identifier) {
27661
- if (findEnclosingFunction(parent) === functionNode) return false;
27662
- if (isInsideProvenEventHandler(analysis, parent, componentFunction, false)) return false;
27663
- }
27664
- return true;
27665
- });
27661
+ const TRANSPARENT_HOOK_NAMES = new Set([
27662
+ "useMemo",
27663
+ "useCallback",
27664
+ "useState",
27665
+ "useRef",
27666
+ "useDeferredValue"
27667
+ ]);
27668
+ const getHookCalleeName = (node) => {
27669
+ if (!isNodeOfType(node, "CallExpression")) return null;
27670
+ const callee = node.callee;
27671
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
27672
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
27673
+ return null;
27666
27674
  };
27667
- const isInsideProvenEventHandler = (analysis, node, componentFunction, allowOneCallFrame) => {
27668
- if (isInsideInlineEventHandler(node, componentFunction)) return true;
27669
- let current = node.parent;
27670
- while (current && current !== componentFunction) {
27671
- if (isFunctionLike$1(current)) {
27672
- if (isFunctionWiredToEventAttribute(analysis, current) && !isFunctionUsedOutsideHandlers(analysis, current, componentFunction)) return true;
27673
- return allowOneCallFrame && isFunctionCalledOnlyFromHandlers(analysis, current, componentFunction);
27675
+ const isOpaqueCustomHookInit = (init) => {
27676
+ const calleeName = getHookCalleeName(init);
27677
+ return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName) && !TRANSPARENT_HOOK_NAMES.has(calleeName);
27678
+ };
27679
+ const getDeclaratorInit = (ref) => {
27680
+ for (const def of ref.resolved?.defs ?? []) {
27681
+ const node = def.node;
27682
+ if (isNodeOfType(node, "VariableDeclarator") && node.init) return node.init;
27683
+ }
27684
+ return null;
27685
+ };
27686
+ const isOpaqueHookResultBinding = (ref) => {
27687
+ const init = getDeclaratorInit(ref);
27688
+ return Boolean(init && isOpaqueCustomHookInit(init));
27689
+ };
27690
+ const isUseRefBinding = (ref) => {
27691
+ const init = getDeclaratorInit(ref);
27692
+ return Boolean(init && getHookCalleeName(init) === "useRef");
27693
+ };
27694
+ const REACT_BUILTIN_HOOK_NAMES = new Set([
27695
+ "useState",
27696
+ "useReducer",
27697
+ "useContext",
27698
+ "useMemo",
27699
+ "useCallback",
27700
+ "useRef",
27701
+ "useDeferredValue",
27702
+ "useTransition",
27703
+ "useSyncExternalStore",
27704
+ "useImperativeHandle",
27705
+ "useLayoutEffect",
27706
+ "useEffect",
27707
+ "useInsertionEffect",
27708
+ "useId",
27709
+ "useOptimistic",
27710
+ "useActionState",
27711
+ "useDebugValue"
27712
+ ]);
27713
+ const isCustomHookReturnedBinding = (ref) => {
27714
+ const init = getDeclaratorInit(ref);
27715
+ if (!init) return false;
27716
+ const calleeName = getHookCalleeName(init);
27717
+ return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName) && !REACT_BUILTIN_HOOK_NAMES.has(calleeName);
27718
+ };
27719
+ const collectGuardUpstreamRefs = (analysis, ref, refs, visited) => {
27720
+ if (visited.has(ref)) return;
27721
+ visited.add(ref);
27722
+ refs.push(ref);
27723
+ for (const def of ref.resolved?.defs ?? []) {
27724
+ if (def.type === "ImportBinding" || def.type === "Parameter") continue;
27725
+ const defNode = def.node;
27726
+ const next = defNode.init ?? defNode.body;
27727
+ if (!next) continue;
27728
+ if (isOpaqueCustomHookInit(next)) continue;
27729
+ for (const innerRef of getDownstreamRefs(analysis, next)) {
27730
+ if (isInsideCallbackArgumentOf(innerRef.identifier, next)) continue;
27731
+ collectGuardUpstreamRefs(analysis, innerRef, refs, visited);
27674
27732
  }
27675
- current = current.parent;
27676
27733
  }
27734
+ };
27735
+ const getGuardUpstreamRefs = (analysis, ref) => {
27736
+ const refs = [];
27737
+ collectGuardUpstreamRefs(analysis, ref, refs, /* @__PURE__ */ new Set());
27738
+ return refs;
27739
+ };
27740
+ const isSideEffectFreeExit = (statement) => {
27741
+ if (isNodeOfType(statement, "ContinueStatement")) return true;
27742
+ if (isNodeOfType(statement, "BreakStatement")) return true;
27743
+ if (!isNodeOfType(statement, "ReturnStatement")) return false;
27744
+ const argument = statement.argument;
27745
+ if (!argument) return true;
27746
+ if (isNodeOfType(argument, "Literal")) return true;
27747
+ if (isNodeOfType(argument, "Identifier")) return true;
27748
+ if (isNodeOfType(argument, "UnaryExpression") && argument.operator === "void") return true;
27677
27749
  return false;
27678
27750
  };
27679
- const isSetterWriterUsage = (identifier) => {
27680
- const parent = identifier.parent;
27681
- if (!parent) return false;
27682
- if (isNodeOfType(parent, "CallExpression")) return parent.callee === identifier || (parent.arguments ?? []).includes(identifier);
27683
- let current = identifier;
27684
- while (current && !isFunctionLike$1(current)) {
27685
- if (isEventAttribute(current) || isEventProperty(current)) return true;
27686
- current = current.parent;
27751
+ const getConsequentStatements = (consequent) => {
27752
+ if (isNodeOfType(consequent, "BlockStatement")) return consequent.body ?? [];
27753
+ return [consequent];
27754
+ };
27755
+ const isControlledPropMirrorStatement = (analysis, statement, testedPropBindings) => {
27756
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
27757
+ let expression = statement.expression;
27758
+ if (expression && isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
27759
+ if (!expression || !isNodeOfType(expression, "CallExpression")) return false;
27760
+ const callee = expression.callee;
27761
+ if (!isNodeOfType(callee, "Identifier")) return false;
27762
+ const calleeRef = getRef(analysis, callee);
27763
+ if (!calleeRef || !isStateSetter(analysis, calleeRef)) return false;
27764
+ const callArguments = expression.arguments ?? [];
27765
+ if (callArguments.length !== 1) return false;
27766
+ const argument = callArguments[0];
27767
+ if (!isNodeOfType(argument, "Identifier")) return false;
27768
+ const argumentRef = getRef(analysis, argument);
27769
+ if (!argumentRef?.resolved || !isProp(analysis, argumentRef)) return false;
27770
+ return testedPropBindings.has(argumentRef.resolved);
27771
+ };
27772
+ const isControlledPropMirrorConsequent = (analysis, ifNode) => {
27773
+ const statements = getConsequentStatements(ifNode.consequent);
27774
+ if (statements.length === 0) return false;
27775
+ const testRefs = getDownstreamRefs(analysis, ifNode.test);
27776
+ if (testRefs.some((ref) => isState(analysis, ref))) return false;
27777
+ const testedPropBindings = new Set(testRefs.filter((ref) => isProp(analysis, ref)).map((ref) => ref.resolved).filter(Boolean));
27778
+ if (testedPropBindings.size === 0) return false;
27779
+ return statements.every((statement) => isControlledPropMirrorStatement(analysis, statement, testedPropBindings));
27780
+ };
27781
+ const isPureEarlyExitConsequent = (consequent) => {
27782
+ if (isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "ContinueStatement") || isNodeOfType(consequent, "BreakStatement")) return isSideEffectFreeExit(consequent);
27783
+ if (isNodeOfType(consequent, "BlockStatement")) {
27784
+ const body = consequent.body ?? [];
27785
+ if (body.length === 0) return false;
27786
+ const last = body[body.length - 1];
27787
+ if (!isSideEffectFreeExit(last)) return false;
27788
+ for (let i = 0; i < body.length - 1; i++) if (!isSetterCallExpressionStatement(body[i])) return false;
27789
+ return true;
27687
27790
  }
27688
27791
  return false;
27689
27792
  };
27690
- const findSetterVariable = (analysis, stateReference) => {
27691
- const stateDeclarator = getUseStateDecl(analysis, stateReference);
27692
- if (!stateDeclarator || !isNodeOfType(stateDeclarator, "VariableDeclarator")) return null;
27693
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return null;
27694
- const setter = stateDeclarator.id.elements?.[1];
27695
- if (!setter || !isNodeOfType(setter, "Identifier")) return null;
27696
- for (const scope of analysis.scopeManager.scopes) {
27697
- const variable = scope.variables.find((candidate) => candidate.name === setter.name && candidate.defs.some((definition) => definition.node === stateDeclarator));
27698
- if (variable) return variable;
27699
- }
27793
+ const isEffectLocalBinding = (ref, effectFn) => Boolean(ref.resolved && ref.resolved.defs.length > 0 && ref.resolved.defs.every((def) => isAstDescendant(def.node, effectFn)));
27794
+ const isEffectLocalWriteStatement = (analysis, statement, effectFn) => {
27795
+ if (isSideEffectFreeExit(statement)) return true;
27796
+ if (isNodeOfType(statement, "VariableDeclaration")) return true;
27797
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
27798
+ const expression = statement.expression;
27799
+ if (!isNodeOfType(expression, "AssignmentExpression")) return false;
27800
+ const left = expression.left;
27801
+ if (!isNodeOfType(left, "Identifier")) return false;
27802
+ const leftRef = getRef(analysis, left);
27803
+ return Boolean(leftRef && isEffectLocalBinding(leftRef, effectFn));
27804
+ };
27805
+ const isLocalComputationConsequent = (analysis, ifNode, effectFn) => {
27806
+ const statements = getConsequentStatements(ifNode.consequent);
27807
+ if (statements.length === 0) return false;
27808
+ return statements.every((statement) => isEffectLocalWriteStatement(analysis, statement, effectFn));
27809
+ };
27810
+ const isEffectLocalOnlyTest = (analysis, ifNode, effectFn) => {
27811
+ const testRefs = getDownstreamRefs(analysis, ifNode.test);
27812
+ if (testRefs.length === 0) return false;
27813
+ return testRefs.every((ref) => isEffectLocalBinding(ref, effectFn));
27814
+ };
27815
+ const isDepsListedCallbackInvocationConsequent = (analysis, ifNode, effectNode) => {
27816
+ const statements = getConsequentStatements(ifNode.consequent);
27817
+ if (statements.length !== 1) return false;
27818
+ const statement = statements[0];
27819
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
27820
+ let expression = statement.expression;
27821
+ if (isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
27822
+ if (!isNodeOfType(expression, "CallExpression")) return false;
27823
+ if ((expression.arguments ?? []).length !== 0) return false;
27824
+ const callee = expression.callee;
27825
+ if (!isNodeOfType(callee, "Identifier")) return false;
27826
+ const calleeRef = getRef(analysis, callee);
27827
+ if (!calleeRef) return false;
27828
+ const init = getDeclaratorInit(calleeRef);
27829
+ if (!init || getHookCalleeName(init) !== "useCallback") return false;
27830
+ const deps = effectNode.arguments?.[1];
27831
+ if (!deps || !isNodeOfType(deps, "ArrayExpression")) return false;
27832
+ return (deps.elements ?? []).some((element) => isNodeOfType(element, "Identifier") && element.name === callee.name);
27833
+ };
27834
+ const IMPERATIVE_ROOT_GLOBAL_NAMES = new Set([
27835
+ "window",
27836
+ "document",
27837
+ "globalThis"
27838
+ ]);
27839
+ const PARENT_CALLBACK_NAME_PATTERN = /^on[A-Z]/;
27840
+ const DATA_ITERATION_METHOD_NAMES = new Set([
27841
+ "forEach",
27842
+ "map",
27843
+ "filter",
27844
+ "reduce",
27845
+ "flatMap",
27846
+ "find",
27847
+ "some",
27848
+ "every",
27849
+ "push",
27850
+ "pop",
27851
+ "shift",
27852
+ "unshift",
27853
+ "splice",
27854
+ "sort",
27855
+ "reverse",
27856
+ "concat"
27857
+ ]);
27858
+ const getImperativeTargetChain = (target) => {
27859
+ let current = target;
27860
+ let passesThroughRefCurrent = false;
27861
+ let memberDepth = 0;
27862
+ for (;;) if (isNodeOfType(current, "ChainExpression")) current = current.expression;
27863
+ else if (isNodeOfType(current, "TSNonNullExpression")) current = current.expression;
27864
+ else if (isNodeOfType(current, "MemberExpression")) {
27865
+ if (!current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "current") passesThroughRefCurrent = true;
27866
+ memberDepth += 1;
27867
+ current = current.object;
27868
+ } else if (isNodeOfType(current, "CallExpression")) current = current.callee;
27869
+ else break;
27870
+ return {
27871
+ baseIdentifier: isNodeOfType(current, "Identifier") ? current : null,
27872
+ passesThroughRefCurrent,
27873
+ memberDepth
27874
+ };
27875
+ };
27876
+ const getInvokedMemberName = (callee) => {
27877
+ let current = callee;
27878
+ if (isNodeOfType(current, "ChainExpression")) current = current.expression;
27879
+ if (isNodeOfType(current, "MemberExpression") && isNodeOfType(current.property, "Identifier")) return current.property.name;
27700
27880
  return null;
27701
27881
  };
27702
- const isStateWrittenOnlyFromEventHandlers = (analysis, stateReference) => {
27703
- const setterVariable = findSetterVariable(analysis, stateReference);
27704
- if (!setterVariable) return false;
27705
- const stateDeclarator = getUseStateDecl(analysis, stateReference);
27706
- if (!stateDeclarator) return false;
27707
- const componentFunction = findEnclosingFunction(stateDeclarator);
27708
- if (!componentFunction) return false;
27709
- let hasWriter = false;
27710
- for (const reference of setterVariable.references) {
27711
- const identifier = reference.identifier;
27712
- if (!isSetterWriterUsage(identifier)) continue;
27713
- hasWriter = true;
27714
- if (!isInsideProvenEventHandler(analysis, identifier, componentFunction, true)) return false;
27882
+ const isCustomHookInstanceBinding = (ref) => Boolean(ref.resolved?.defs.some((def) => {
27883
+ const node = def.node;
27884
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
27885
+ const calleeName = getHookCalleeName(node.init);
27886
+ return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName);
27887
+ }));
27888
+ const isImperativeSyncStatement = (analysis, statement, testedBindings) => {
27889
+ if (isSideEffectFreeExit(statement)) return true;
27890
+ if (isNodeOfType(statement, "IfStatement")) {
27891
+ if (statement.alternate) return false;
27892
+ const innerTestedBindings = new Set(testedBindings);
27893
+ for (const testRef of getDownstreamRefs(analysis, statement.test)) if (testRef.resolved) innerTestedBindings.add(testRef.resolved);
27894
+ const innerStatements = getConsequentStatements(statement.consequent);
27895
+ if (innerStatements.length === 0) return false;
27896
+ return innerStatements.every((innerStatement) => isImperativeSyncStatement(analysis, innerStatement, innerTestedBindings));
27715
27897
  }
27716
- return hasWriter;
27898
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
27899
+ let expression = statement.expression;
27900
+ if (isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
27901
+ let target;
27902
+ let invokedName = null;
27903
+ if (isNodeOfType(expression, "CallExpression")) {
27904
+ target = expression.callee;
27905
+ invokedName = getInvokedMemberName(target);
27906
+ } else if (isNodeOfType(expression, "AssignmentExpression")) target = expression.left;
27907
+ else return false;
27908
+ const chain = getImperativeTargetChain(target);
27909
+ if (!chain.baseIdentifier) return false;
27910
+ if (chain.memberDepth === 0) {
27911
+ if (!isNodeOfType(expression, "CallExpression")) return false;
27912
+ const calleeRef = getRef(analysis, chain.baseIdentifier);
27913
+ if (!calleeRef?.resolved) return false;
27914
+ if (isState(analysis, calleeRef) || isStateSetter(analysis, calleeRef)) return false;
27915
+ if (PARENT_CALLBACK_NAME_PATTERN.test(chain.baseIdentifier.name)) return false;
27916
+ return isCustomHookReturnedBinding(calleeRef);
27917
+ }
27918
+ const baseName = chain.baseIdentifier.name;
27919
+ if (IMPERATIVE_ROOT_GLOBAL_NAMES.has(baseName)) return true;
27920
+ if (chain.passesThroughRefCurrent) return true;
27921
+ if (baseName === "ref" || baseName.endsWith("Ref") || baseName.endsWith("ref")) return true;
27922
+ const baseRef = getRef(analysis, chain.baseIdentifier);
27923
+ if (!baseRef?.resolved) return false;
27924
+ if (isState(analysis, baseRef) || isStateSetter(analysis, baseRef)) return false;
27925
+ if (isCustomHookInstanceBinding(baseRef)) return true;
27926
+ if (invokedName && PARENT_CALLBACK_NAME_PATTERN.test(invokedName)) return false;
27927
+ if (invokedName && DATA_ITERATION_METHOD_NAMES.has(invokedName)) return false;
27928
+ if (!testedBindings.has(baseRef.resolved)) return false;
27929
+ if (!isCustomHookParameter(baseRef) && isWholePropsObjectReference(analysis, baseRef)) return false;
27930
+ return true;
27717
27931
  };
27718
- //#endregion
27719
- //#region src/plugin/rules/state-and-effects/no-event-handler.ts
27720
- const collectGuardStateReferences = (analysis, testExpression) => {
27721
- const stateReferences = [];
27722
- const seenBindings = /* @__PURE__ */ new Set();
27723
- for (const directReference of getDownstreamRefs(analysis, testExpression)) for (const reference of getUpstreamRefs(analysis, directReference)) {
27724
- if (!isState(analysis, reference)) continue;
27725
- const binding = reference.resolved ?? reference.identifier;
27726
- if (seenBindings.has(binding)) continue;
27727
- seenBindings.add(binding);
27728
- stateReferences.push(reference);
27729
- }
27730
- return stateReferences;
27731
- };
27732
- const consequentHasTransferableWork = (analysis, consequent) => {
27733
- if (findTriggeredSideEffectCalleeName(consequent)) return true;
27734
- if (getDownstreamRefs(analysis, consequent).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return true;
27735
- let hasTransferableWork = false;
27736
- walkAst(consequent, (child) => {
27737
- if (hasTransferableWork) return false;
27738
- if (child !== consequent && isFunctionLike$1(child)) return false;
27739
- if (!isNodeOfType(child, "CallExpression")) return;
27740
- if (isNodeOfType(child.callee, "Identifier")) {
27741
- const calleeReference = getRef(analysis, child.callee);
27742
- if (calleeReference && isStateSetterCall(analysis, calleeReference)) return;
27743
- if (calleeReference?.resolved?.defs.some((definition) => {
27744
- const definitionNode = definition.node;
27745
- if (!isNodeOfType(definitionNode, "VariableDeclarator")) return false;
27746
- if (!isNodeOfType(definitionNode.init, "CallExpression")) return false;
27747
- const hookCallee = definitionNode.init.callee;
27748
- return isNodeOfType(hookCallee, "Identifier") && hookCallee.name !== "useReducer" && /^use[A-Z0-9]/.test(hookCallee.name);
27749
- })) return;
27750
- } else if (!isNodeOfType(child.callee, "MemberExpression") || !isNodeOfType(child.callee.property, "Identifier") || child.callee.property.name !== "setAttribute") return;
27751
- hasTransferableWork = true;
27752
- return false;
27753
- });
27754
- return hasTransferableWork;
27932
+ const isImperativeSyncConsequent = (analysis, ifNode) => {
27933
+ const statements = getConsequentStatements(ifNode.consequent);
27934
+ if (statements.length === 0) return false;
27935
+ if (statements.every((statement) => isSideEffectFreeExit(statement))) return false;
27936
+ const testedBindings = /* @__PURE__ */ new Set();
27937
+ for (const testRef of getDownstreamRefs(analysis, ifNode.test)) if (testRef.resolved) testedBindings.add(testRef.resolved);
27938
+ return statements.every((statement) => isImperativeSyncStatement(analysis, statement, testedBindings));
27755
27939
  };
27756
- const guardHasOtherReactiveSource = (analysis, testExpression, handlerStateDeclarator) => getDownstreamRefs(analysis, testExpression).some((directReference) => getUpstreamRefs(analysis, directReference).some((reference) => {
27757
- if (isProp(analysis, reference)) return true;
27758
- if (!isState(analysis, reference)) return false;
27759
- return getUseStateDecl(analysis, reference) !== handlerStateDeclarator;
27760
- }));
27761
- const consequentHasAdditionalReactiveGuard = (analysis, consequent, handlerStateDeclarator) => {
27762
- let hasAdditionalGuard = false;
27763
- walkAst(consequent, (child) => {
27764
- if (hasAdditionalGuard) return false;
27765
- if (child !== consequent && isFunctionLike$1(child)) return false;
27766
- if (!isNodeOfType(child, "IfStatement")) return;
27767
- if (guardHasOtherReactiveSource(analysis, child.test, handlerStateDeclarator)) {
27768
- hasAdditionalGuard = true;
27769
- return false;
27770
- }
27771
- });
27772
- return hasAdditionalGuard;
27940
+ const isSetterInvocationUsage = (identifier) => {
27941
+ const parent = identifier.parent;
27942
+ if (!parent) return false;
27943
+ if (isNodeOfType(parent, "CallExpression")) {
27944
+ if (parent.callee === identifier) return true;
27945
+ return (parent.arguments ?? []).some((argument) => argument === identifier);
27946
+ }
27947
+ return isNodeOfType(parent, "JSXExpressionContainer");
27948
+ };
27949
+ const isStateSetterUsedOutsideEffect = (analysis, stateRef, effectNode) => {
27950
+ const declarator = stateRef.resolved?.defs.map((def) => def.node).find((defNode) => isNodeOfType(defNode, "VariableDeclarator") && isNodeOfType(defNode.id, "ArrayPattern"));
27951
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
27952
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
27953
+ const setterElement = declarator.id.elements?.[1];
27954
+ if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
27955
+ const setterName = setterElement.name;
27956
+ for (const scope of analysis.scopeManager.scopes) {
27957
+ const setterVariable = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
27958
+ if (!setterVariable) continue;
27959
+ return setterVariable.references.some((reference) => {
27960
+ const identifier = reference.identifier;
27961
+ if (isAstDescendant(identifier, effectNode)) return false;
27962
+ return isSetterInvocationUsage(identifier);
27963
+ });
27964
+ }
27965
+ return false;
27773
27966
  };
27774
27967
  const noEventHandler = defineRule({
27775
27968
  id: "no-event-handler",
@@ -27779,30 +27972,58 @@ const noEventHandler = defineRule({
27779
27972
  recommendation: "Run the side effect in the event handler that triggers it, instead of watching its state from a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers",
27780
27973
  create: (context) => ({ CallExpression(node) {
27781
27974
  if (!isUseEffect(node)) return;
27975
+ const depsArgument = node.arguments?.[1];
27976
+ const isMountOnlyEffect = Boolean(depsArgument && isNodeOfType(depsArgument, "ArrayExpression") && (depsArgument.elements ?? []).length === 0);
27782
27977
  const analysis = getProgramAnalysis(node);
27783
- if (!analysis || hasCleanup(analysis, node)) return;
27784
- const frames = collectBoundedEffectExecutionFrames(analysis, node);
27785
- for (const frame of frames) {
27786
- let reported = false;
27787
- walkAst(frame.functionNode, (child) => {
27788
- if (reported) return false;
27789
- if (child !== frame.functionNode && isFunctionLike$1(child)) return false;
27790
- if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
27791
- if (!consequentHasTransferableWork(analysis, child.consequent)) return;
27792
- const stateReferences = collectGuardStateReferences(analysis, child.test);
27793
- const handlerStateDeclarators = new Set(stateReferences.filter((reference) => isStateWrittenOnlyFromEventHandlers(analysis, reference)).map((reference) => getUseStateDecl(analysis, reference)).filter((declarator) => Boolean(declarator)));
27794
- if (handlerStateDeclarators.size !== 1) return;
27795
- const handlerStateDeclarator = [...handlerStateDeclarators][0];
27796
- if (!handlerStateDeclarator) return;
27797
- if (guardHasOtherReactiveSource(analysis, child.test, handlerStateDeclarator) || consequentHasAdditionalReactiveGuard(analysis, child.consequent, handlerStateDeclarator)) return;
27798
- context.report({
27799
- node,
27800
- message: "Faking an event handler with state plus a useEffect costs an extra render & runs late."
27801
- });
27802
- reported = true;
27803
- return false;
27978
+ if (!analysis) return;
27979
+ if (hasCleanup(analysis, node)) return;
27980
+ if (!getEffectFnRefs(analysis, node)) return;
27981
+ const effectFn = getEffectFn(analysis, node);
27982
+ if (!effectFn) return;
27983
+ const ifTestRefEntries = findDownstreamNodes(node, "IfStatement").filter((ifNode) => isNodeOfType(ifNode, "IfStatement") && !ifNode.alternate && !isPureEarlyExitConsequent(ifNode.consequent) && !isControlledPropMirrorConsequent(analysis, ifNode) && !containsRefGuard(ifNode.test) && !containsPreviousValueComparison(analysis, ifNode.test) && !isLocalComputationConsequent(analysis, ifNode, effectFn) && !isEffectLocalOnlyTest(analysis, ifNode, effectFn) && !isDepsListedCallbackInvocationConsequent(analysis, ifNode, node) && !isImperativeSyncConsequent(analysis, ifNode)).flatMap((ifNode) => {
27984
+ if (!isNodeOfType(ifNode, "IfStatement")) return [];
27985
+ const directTestRefs = getDownstreamRefs(analysis, ifNode.test);
27986
+ const isAsyncDataGated = directTestRefs.some((ref) => isOpaqueHookResultBinding(ref));
27987
+ return directTestRefs.filter((ref) => !(isState(analysis, ref) && isExternallyDrivenState(analysis, ref))).flatMap((ref) => getGuardUpstreamRefs(analysis, ref)).map((ref) => ({
27988
+ ref,
27989
+ isAsyncDataGated
27990
+ }));
27991
+ });
27992
+ const seenBindings = /* @__PURE__ */ new Map();
27993
+ const seenIdentifiers = /* @__PURE__ */ new Set();
27994
+ const dedupedEntries = [];
27995
+ for (const entry of ifTestRefEntries) {
27996
+ const identifier = entry.ref.identifier;
27997
+ if (!identifier) continue;
27998
+ const resolved = entry.ref.resolved;
27999
+ if (resolved) {
28000
+ const seen = seenBindings.get(resolved);
28001
+ if (seen) {
28002
+ if (!entry.isAsyncDataGated) seen.isAsyncDataGated = false;
28003
+ continue;
28004
+ }
28005
+ seenBindings.set(resolved, entry);
28006
+ }
28007
+ if (seenIdentifiers.has(identifier)) continue;
28008
+ seenIdentifiers.add(identifier);
28009
+ dedupedEntries.push(entry);
28010
+ }
28011
+ const dedupedRefs = dedupedEntries.map((entry) => entry.ref);
28012
+ for (const ref of dedupedRefs) if (isState(analysis, ref)) {
28013
+ if (isExternallyDrivenState(analysis, ref)) continue;
28014
+ if (isMountOnlyEffect && !isStateSetterUsedOutsideEffect(analysis, ref, node)) continue;
28015
+ context.report({
28016
+ node: ref.identifier,
28017
+ message: "Faking an event handler with state plus a useEffect costs an extra render & runs late."
28018
+ });
28019
+ }
28020
+ for (const entry of dedupedEntries) if (isProp(analysis, entry.ref)) {
28021
+ if (isMountOnlyEffect) continue;
28022
+ if (entry.isAsyncDataGated) continue;
28023
+ context.report({
28024
+ node: entry.ref.identifier,
28025
+ message: "Faking an event handler with a prop plus a useEffect costs an extra render & runs late."
27804
28026
  });
27805
- if (reported) return;
27806
28027
  }
27807
28028
  } })
27808
28029
  });
@@ -28410,6 +28631,46 @@ const collectFunctionLikeLocalNames = (componentBody) => {
28410
28631
  return functionLikeLocalNames;
28411
28632
  };
28412
28633
  //#endregion
28634
+ //#region src/plugin/rules/state-and-effects/utils/collect-handler-binding-names.ts
28635
+ const collectHandlerBindingNames = (componentBody) => {
28636
+ const handlerNames = /* @__PURE__ */ new Set();
28637
+ walkAst(componentBody, (child) => {
28638
+ if (!isNodeOfType(child, "JSXAttribute")) return;
28639
+ if (!isNodeOfType(child.name, "JSXIdentifier")) return;
28640
+ if (!/^on[A-Z]/.test(child.name.name)) return;
28641
+ if (!isNodeOfType(child.value, "JSXExpressionContainer")) return;
28642
+ const expression = child.value.expression;
28643
+ if (isNodeOfType(expression, "Identifier")) handlerNames.add(expression.name);
28644
+ });
28645
+ return handlerNames;
28646
+ };
28647
+ //#endregion
28648
+ //#region src/plugin/rules/state-and-effects/utils/is-inside-event-handler.ts
28649
+ const isInsideEventHandler = (node, handlerBindingNames) => {
28650
+ let cursor = node.parent ?? null;
28651
+ while (cursor) {
28652
+ if (isNodeOfType(cursor, "ArrowFunctionExpression") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "FunctionDeclaration")) {
28653
+ let outer = cursor.parent ?? null;
28654
+ while (outer) {
28655
+ if (isNodeOfType(outer, "JSXAttribute")) {
28656
+ const attrName = isNodeOfType(outer.name, "JSXIdentifier") ? outer.name.name : null;
28657
+ if (attrName && /^on[A-Z]/.test(attrName)) return true;
28658
+ return false;
28659
+ }
28660
+ if (isNodeOfType(outer, "VariableDeclarator")) {
28661
+ const declaredName = isNodeOfType(outer.id, "Identifier") ? outer.id.name : null;
28662
+ return Boolean(declaredName && handlerBindingNames.has(declaredName));
28663
+ }
28664
+ if (isNodeOfType(outer, "Program")) return false;
28665
+ outer = outer.parent ?? null;
28666
+ }
28667
+ return false;
28668
+ }
28669
+ cursor = cursor.parent ?? null;
28670
+ }
28671
+ return false;
28672
+ };
28673
+ //#endregion
28413
28674
  //#region src/plugin/rules/state-and-effects/no-event-trigger-state.ts
28414
28675
  const SENTINEL_IDENTIFIER_NAMES = new Set([
28415
28676
  "undefined",
@@ -28436,6 +28697,23 @@ const getTriggerGuardRootName = (testNode) => {
28436
28697
  if (isNodeOfType(testNode, "UnaryExpression") && testNode.operator === "!") return getTriggerGuardRootName(testNode.argument);
28437
28698
  return null;
28438
28699
  };
28700
+ const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
28701
+ const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
28702
+ const settersWithAnyCall = /* @__PURE__ */ new Set();
28703
+ const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
28704
+ walkAst(componentBody, (child) => {
28705
+ if (!isNodeOfType(child, "CallExpression")) return;
28706
+ if (!isNodeOfType(child.callee, "Identifier")) return;
28707
+ const setterName = child.callee.name;
28708
+ if (!setterNames.has(setterName)) return;
28709
+ settersWithAnyCall.add(setterName);
28710
+ if (settersWithNonHandlerCall.has(setterName)) return;
28711
+ if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
28712
+ });
28713
+ const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
28714
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
28715
+ return handlerOnlyWriteStateNames;
28716
+ };
28439
28717
  const noEventTriggerState = defineRule({
28440
28718
  id: "no-event-trigger-state",
28441
28719
  title: "State exists only to trigger an effect",
@@ -28447,9 +28725,8 @@ const noEventTriggerState = defineRule({
28447
28725
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
28448
28726
  const useStateBindings = collectUseStateBindings(componentBody);
28449
28727
  if (useStateBindings.length === 0) return;
28450
- const analysis = getProgramAnalysis(componentBody);
28451
- if (!analysis) return;
28452
- const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
28728
+ const handlerOnlyWriteStateNames = collectHandlerOnlyWriteStateNames(componentBody, useStateBindings, collectHandlerBindingNames(componentBody));
28729
+ if (handlerOnlyWriteStateNames.size === 0) return;
28453
28730
  const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
28454
28731
  const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
28455
28732
  const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
@@ -28462,9 +28739,7 @@ const noEventTriggerState = defineRule({
28462
28739
  if ((depsNode.elements?.length ?? 0) !== 1) return;
28463
28740
  const depElement = depsNode.elements[0];
28464
28741
  if (!isNodeOfType(depElement, "Identifier")) return;
28465
- if (!localStateNames.has(depElement.name)) return;
28466
- const dependencyReference = getRef(analysis, depElement);
28467
- if (!dependencyReference || !isState(analysis, dependencyReference) || !isStateWrittenOnlyFromEventHandlers(analysis, dependencyReference)) return;
28742
+ if (!handlerOnlyWriteStateNames.has(depElement.name)) return;
28468
28743
  if (renderReachableNames.has(depElement.name)) return;
28469
28744
  const callback = getEffectCallback(effectCall);
28470
28745
  if (!callback) return;
@@ -29361,12 +29636,264 @@ const noIndeterminateAttribute = defineRule({
29361
29636
  }
29362
29637
  });
29363
29638
  //#endregion
29639
+ //#region src/plugin/utils/contains-non-deterministic-source.ts
29640
+ const NON_DETERMINISTIC_MEMBER_CALLS = new Set([
29641
+ "Math.random",
29642
+ "Date.now",
29643
+ "performance.now",
29644
+ "crypto.randomUUID",
29645
+ "crypto.getRandomValues"
29646
+ ]);
29647
+ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
29648
+ "nanoid",
29649
+ "uuid",
29650
+ "cuid",
29651
+ "ulid",
29652
+ "createId"
29653
+ ]);
29654
+ const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
29655
+ const containsNonDeterministicSource = (root) => {
29656
+ let found = false;
29657
+ walkAst(root, (child) => {
29658
+ if (found) return false;
29659
+ if (isFunctionLike$1(child)) return false;
29660
+ if (isZeroArgDateConstruction(child)) {
29661
+ found = true;
29662
+ return false;
29663
+ }
29664
+ if (!isNodeOfType(child, "CallExpression")) return;
29665
+ const callee = child.callee;
29666
+ if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
29667
+ found = true;
29668
+ return false;
29669
+ }
29670
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && NON_DETERMINISTIC_MEMBER_CALLS.has(`${callee.object.name}.${callee.property.name}`)) {
29671
+ found = true;
29672
+ return false;
29673
+ }
29674
+ });
29675
+ return found;
29676
+ };
29677
+ //#endregion
29364
29678
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
29365
- const getStateName = (stateDeclarator) => {
29366
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
29367
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
29368
- const stateBinding = stateDeclarator.id.elements?.[0] ?? stateDeclarator.id.elements?.[1];
29369
- return stateBinding && isNodeOfType(stateBinding, "Identifier") ? stateBinding.name : "<state>";
29679
+ const MEASUREMENT_GLOBAL_NAMES = new Set([
29680
+ "window",
29681
+ "document",
29682
+ "navigator"
29683
+ ]);
29684
+ const objectPatternBindsName = (pattern, name) => {
29685
+ if (!isNodeOfType(pattern, "ObjectPattern")) return false;
29686
+ return (pattern.properties ?? []).some((property) => {
29687
+ if (!isNodeOfType(property, "Property")) return false;
29688
+ const bound = property.value;
29689
+ return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
29690
+ });
29691
+ };
29692
+ const findEffectLocalInitializer = (effectFn, name) => {
29693
+ let initializer = null;
29694
+ walkAst(effectFn, (child) => {
29695
+ if (initializer) return false;
29696
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
29697
+ if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName(child.id, name)) {
29698
+ initializer = child.init;
29699
+ return false;
29700
+ }
29701
+ });
29702
+ return initializer;
29703
+ };
29704
+ const isMeasurementGlobalValueRead = (identifier) => {
29705
+ const memberParent = identifier.parent;
29706
+ if (!isNodeOfType(memberParent, "MemberExpression") || memberParent.object !== identifier) return false;
29707
+ if (isNodeOfType(memberParent.property, "Identifier") && DOM_QUERY_MEMBER_NAMES.has(memberParent.property.name)) {
29708
+ const callGrandparent = memberParent.parent;
29709
+ return Boolean(callGrandparent && isNodeOfType(callGrandparent, "CallExpression") && callGrandparent.callee === memberParent);
29710
+ }
29711
+ return true;
29712
+ };
29713
+ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames = /* @__PURE__ */ new Set()) => {
29714
+ let found = false;
29715
+ walkAst(argument, (child) => {
29716
+ if (found) return false;
29717
+ if (isMeasurementMemberRead(child)) {
29718
+ found = true;
29719
+ return false;
29720
+ }
29721
+ if (!isNodeOfType(child, "Identifier")) return;
29722
+ if (isPostMountGlobalRead(child) && MEASUREMENT_GLOBAL_NAMES.has(child.name) && isMeasurementGlobalValueRead(child)) {
29723
+ found = true;
29724
+ return false;
29725
+ }
29726
+ if (visitedLocalNames.has(child.name)) return;
29727
+ visitedLocalNames.add(child.name);
29728
+ const localInitializer = findEffectLocalInitializer(effectFn, child.name);
29729
+ if (localInitializer && argumentReadsPostMountMeasurement(localInitializer, effectFn, visitedLocalNames)) {
29730
+ found = true;
29731
+ return false;
29732
+ }
29733
+ });
29734
+ return found;
29735
+ };
29736
+ const isResourceLikeInitializer = (initializer) => {
29737
+ if (isNodeOfType(initializer, "AwaitExpression")) return isResourceLikeInitializer(initializer.argument);
29738
+ return isNodeOfType(initializer, "NewExpression") || isNodeOfType(initializer, "CallExpression");
29739
+ };
29740
+ const collectArgumentSourceLocalNames = (argument, effectFn, sourceLocalNames = /* @__PURE__ */ new Set()) => {
29741
+ walkAst(argument, (child) => {
29742
+ if (!isNodeOfType(child, "Identifier")) return;
29743
+ if (sourceLocalNames.has(child.name)) return;
29744
+ const localInitializer = findEffectLocalInitializer(effectFn, child.name);
29745
+ if (!localInitializer || !isResourceLikeInitializer(localInitializer)) return;
29746
+ sourceLocalNames.add(child.name);
29747
+ collectArgumentSourceLocalNames(localInitializer, effectFn, sourceLocalNames);
29748
+ });
29749
+ return sourceLocalNames;
29750
+ };
29751
+ const isFunctionExpressionLike$2 = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression");
29752
+ const findCleanupFunction = (effectFn) => {
29753
+ if (!isNodeOfType(effectFn, "ArrowFunctionExpression") && !isNodeOfType(effectFn, "FunctionExpression")) return null;
29754
+ const body = effectFn.body;
29755
+ if (!isNodeOfType(body, "BlockStatement")) return null;
29756
+ let cleanupFunction = null;
29757
+ walkAst(body, (child) => {
29758
+ if (cleanupFunction) return false;
29759
+ if (isNodeOfType(child, "ReturnStatement")) {
29760
+ if (child.argument && isFunctionExpressionLike$2(child.argument)) cleanupFunction = child.argument;
29761
+ return false;
29762
+ }
29763
+ if (child !== body && isFunctionExpressionLike$2(child)) return false;
29764
+ if (isNodeOfType(child, "FunctionDeclaration")) return false;
29765
+ });
29766
+ return cleanupFunction;
29767
+ };
29768
+ const cleanupDisposesArgumentSource = (argument, effectFn) => {
29769
+ const cleanupFunction = findCleanupFunction(effectFn);
29770
+ if (!cleanupFunction) return false;
29771
+ const sourceLocalNames = collectArgumentSourceLocalNames(argument, effectFn);
29772
+ if (sourceLocalNames.size === 0) return false;
29773
+ let referencesSource = false;
29774
+ walkAst(cleanupFunction, (child) => {
29775
+ if (referencesSource) return false;
29776
+ if (isNodeOfType(child, "Identifier") && sourceLocalNames.has(child.name)) {
29777
+ referencesSource = true;
29778
+ return false;
29779
+ }
29780
+ });
29781
+ return referencesSource;
29782
+ };
29783
+ const cleanupResetsSameSetter = (effectFn, setterName) => {
29784
+ const cleanupFunction = findCleanupFunction(effectFn);
29785
+ if (!cleanupFunction) return false;
29786
+ let resets = false;
29787
+ walkAst(cleanupFunction, (child) => {
29788
+ if (resets) return false;
29789
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
29790
+ resets = true;
29791
+ return false;
29792
+ }
29793
+ });
29794
+ return resets;
29795
+ };
29796
+ const TYPEOF_BROWSER_GLOBAL_NAMES = new Set(["window", "document"]);
29797
+ const initializerHasTypeofBrowserGlobalCheck = (useStateDecl) => {
29798
+ if (!isNodeOfType(useStateDecl, "VariableDeclarator")) return false;
29799
+ if (!isNodeOfType(useStateDecl.init, "CallExpression")) return false;
29800
+ const initialArgument = useStateDecl.init.arguments?.[0];
29801
+ if (!initialArgument) return false;
29802
+ let found = false;
29803
+ walkAst(initialArgument, (child) => {
29804
+ if (found) return false;
29805
+ if (isNodeOfType(child, "UnaryExpression") && child.operator === "typeof" && isNodeOfType(child.argument, "Identifier") && TYPEOF_BROWSER_GLOBAL_NAMES.has(child.argument.name)) {
29806
+ found = true;
29807
+ return false;
29808
+ }
29809
+ });
29810
+ return found;
29811
+ };
29812
+ const HANDLER_HELPER_NAME_PATTERN = /^(on|handle)[A-Z_]/;
29813
+ const isSameValueExpression = (leftNode, rightNode) => {
29814
+ const left = stripParenExpression(leftNode);
29815
+ const right = stripParenExpression(rightNode);
29816
+ if (left.type !== right.type) return false;
29817
+ if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) return left.name === right.name;
29818
+ if (isNodeOfType(left, "Literal") && isNodeOfType(right, "Literal")) return left.value === right.value;
29819
+ if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) {
29820
+ if (left.computed !== right.computed) return false;
29821
+ return isSameValueExpression(left.property, right.property) && isSameValueExpression(left.object, right.object);
29822
+ }
29823
+ if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
29824
+ const leftArguments = left.arguments ?? [];
29825
+ const rightArguments = right.arguments ?? [];
29826
+ if (leftArguments.length !== rightArguments.length) return false;
29827
+ if (!isSameValueExpression(left.callee, right.callee)) return false;
29828
+ return leftArguments.every((leftArgument, argumentIndex) => isSameValueExpression(leftArgument, rightArguments[argumentIndex]));
29829
+ }
29830
+ if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && isSameValueExpression(left.argument, right.argument);
29831
+ return false;
29832
+ };
29833
+ const unwrapLazyInitializer = (initializer) => {
29834
+ if (!isNodeOfType(initializer, "ArrowFunctionExpression")) return initializer;
29835
+ const body = initializer.body;
29836
+ if (!isNodeOfType(body, "BlockStatement")) return body;
29837
+ const statements = body.body ?? [];
29838
+ if (statements.length === 1 && isNodeOfType(statements[0], "ReturnStatement")) {
29839
+ const returned = statements[0].argument;
29840
+ if (returned) return returned;
29841
+ }
29842
+ return initializer;
29843
+ };
29844
+ const collectInitializerValueExpressions = (useStateDecl) => {
29845
+ if (!isNodeOfType(useStateDecl, "VariableDeclarator")) return [];
29846
+ if (!isNodeOfType(useStateDecl.init, "CallExpression")) return [];
29847
+ const initialArgument = useStateDecl.init.arguments?.[0];
29848
+ if (!initialArgument) return [];
29849
+ const expressions = [];
29850
+ const expand = (node) => {
29851
+ expressions.push(node);
29852
+ if (isNodeOfType(node, "LogicalExpression") && (node.operator === "??" || node.operator === "||")) {
29853
+ expand(node.left);
29854
+ expand(node.right);
29855
+ }
29856
+ };
29857
+ expand(unwrapLazyInitializer(initialArgument));
29858
+ return expressions;
29859
+ };
29860
+ const isUseStateWithoutArgument = (useStateDecl) => isNodeOfType(useStateDecl, "VariableDeclarator") && isNodeOfType(useStateDecl.init, "CallExpression") && (useStateDecl.init.arguments ?? []).length === 0;
29861
+ const isUndefinedExpression = (node) => {
29862
+ const unwrapped = stripParenExpression(node);
29863
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === "undefined";
29864
+ return isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "void";
29865
+ };
29866
+ const isSameValueAsInitializer = (callExpr, useStateDecl) => {
29867
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
29868
+ const args = callExpr.arguments ?? [];
29869
+ if (args.length !== 1) return false;
29870
+ const setterArgument = args[0];
29871
+ if (isUseStateWithoutArgument(useStateDecl) && isUndefinedExpression(setterArgument)) return true;
29872
+ return collectInitializerValueExpressions(useStateDecl).some((initializerValue) => isSameValueExpression(setterArgument, initializerValue));
29873
+ };
29874
+ const collectInnerStateSetterCalls = (analysis, helperFn) => {
29875
+ const innerCalls = [];
29876
+ const helperBody = helperFn.body;
29877
+ if (!helperBody) return innerCalls;
29878
+ walkAst(helperBody, (child) => {
29879
+ if (!isNodeOfType(child, "Identifier")) return;
29880
+ const innerRef = getRef(analysis, child);
29881
+ if (!innerRef || !isStateSetter(analysis, innerRef)) return;
29882
+ const innerCallExpr = getCallExpr(innerRef);
29883
+ if (!innerCallExpr) return;
29884
+ innerCalls.push({
29885
+ ref: innerRef,
29886
+ callExpr: innerCallExpr,
29887
+ isSyncWithinFunction: isSynchronous(child, helperFn)
29888
+ });
29889
+ });
29890
+ return innerCalls;
29891
+ };
29892
+ const isNodeWithinRange = (inner, outer) => {
29893
+ const innerRange = inner.range;
29894
+ const outerRange = outer.range;
29895
+ if (!innerRange || !outerRange) return false;
29896
+ return outerRange[0] <= innerRange[0] && innerRange[1] <= outerRange[1];
29370
29897
  };
29371
29898
  const noInitializeState = defineRule({
29372
29899
  id: "no-initialize-state",
@@ -29376,15 +29903,53 @@ const noInitializeState = defineRule({
29376
29903
  recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
29377
29904
  create: (context) => ({ CallExpression(node) {
29378
29905
  if (!isUseEffect(node)) return;
29379
- const dependencies = node.arguments?.[1];
29380
- if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
29381
29906
  const analysis = getProgramAnalysis(node);
29382
29907
  if (!analysis) return;
29383
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
29384
- if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
29385
- const stateName = getStateName(fact.stateDeclarator);
29908
+ const effectFnRefs = getEffectFnRefs(analysis, node);
29909
+ const depsRefs = getEffectDepsRefs(analysis, node);
29910
+ if (!effectFnRefs || !depsRefs) return;
29911
+ const effectFn = getEffectFn(analysis, node);
29912
+ if (!effectFn) return;
29913
+ if (!(depsRefs.filter((ref) => !isStateSetter(analysis, ref)).length === 0)) return;
29914
+ for (const ref of effectFnRefs) {
29915
+ if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
29916
+ const callExpr = getCallExpr(ref);
29917
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
29918
+ if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
29919
+ if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
29920
+ if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
29921
+ if (!isStateSetter(analysis, ref)) {
29922
+ const helperFn = resolveToFunction(ref);
29923
+ if (helperFn) {
29924
+ const helperName = getFunctionBindingName$1(helperFn);
29925
+ if (helperName && HANDLER_HELPER_NAME_PATTERN.test(helperName)) continue;
29926
+ }
29927
+ if (helperFn) {
29928
+ const innerSetterCalls = collectInnerStateSetterCalls(analysis, helperFn);
29929
+ if (innerSetterCalls.length > 0) {
29930
+ const syncInnerCalls = innerSetterCalls.filter((innerCall) => innerCall.isSyncWithinFunction);
29931
+ if (syncInnerCalls.length === 0) continue;
29932
+ const measurementLookupFn = isNodeWithinRange(helperFn, effectFn) ? effectFn : helperFn;
29933
+ if (syncInnerCalls.every((innerCall) => {
29934
+ if (!isNodeOfType(innerCall.callExpr, "CallExpression")) return false;
29935
+ const innerUseStateDecl = getUseStateDecl(analysis, innerCall.ref);
29936
+ if (innerUseStateDecl && isSameValueAsInitializer(innerCall.callExpr, innerUseStateDecl)) return true;
29937
+ return (innerCall.callExpr.arguments ?? []).some((argument) => Boolean(argument) && (containsNonDeterministicSource(argument) || argumentReadsPostMountMeasurement(argument, measurementLookupFn)));
29938
+ })) continue;
29939
+ }
29940
+ }
29941
+ }
29942
+ const useStateDecl = getUseStateDecl(analysis, ref);
29943
+ if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
29944
+ if (isSameValueAsInitializer(callExpr, useStateDecl)) continue;
29945
+ if (initializerHasTypeofBrowserGlobalCheck(useStateDecl)) continue;
29946
+ if (isNodeOfType(callExpr.callee, "Identifier") && cleanupResetsSameSetter(effectFn, callExpr.callee.name)) continue;
29947
+ if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
29948
+ const elements = useStateDecl.id.elements ?? [];
29949
+ const stateBinding = elements[0] ?? elements[1];
29950
+ const stateName = stateBinding && isNodeOfType(stateBinding, "Identifier") ? stateBinding.name : "<state>";
29386
29951
  context.report({
29387
- node: fact.callExpression,
29952
+ node: callExpr,
29388
29953
  message: `Your users see an extra render with empty "${stateName}" because a useEffect sets its starting value.`
29389
29954
  });
29390
29955
  }
@@ -32445,19 +33010,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
32445
33010
  "deserialize"
32446
33011
  ]);
32447
33012
  //#endregion
32448
- //#region src/plugin/utils/get-call-method-name.ts
32449
- /**
32450
- * Returns the static method name of a call's callee when it's a
32451
- * non-computed MemberExpression (`obj.method` → `"method"`), or
32452
- * `null` otherwise. Used by `no-pass-data-to-parent` and
32453
- * `no-pass-live-state-to-parent` to match the receiver-bound
32454
- * method-call shape against an allow/block list.
32455
- */
32456
- const getCallMethodName = (callee) => {
32457
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
32458
- return null;
32459
- };
32460
- //#endregion
32461
33013
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
32462
33014
  const isUseStateIdentifier = (identifier) => {
32463
33015
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -38448,7 +39000,7 @@ const containsVnodeFactoryCall = (root) => {
38448
39000
  };
38449
39001
  const isComponentLikeFunction = (functionNode) => {
38450
39002
  const bindingName = getFunctionBindingName$1(functionNode);
38451
- if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$3.test(bindingName))) return true;
39003
+ if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$4.test(bindingName))) return true;
38452
39004
  const body = "body" in functionNode ? functionNode.body : null;
38453
39005
  if (!body || !isAstNode(body)) return false;
38454
39006
  return containsJsxElement(body) || containsVnodeFactoryCall(body);
@@ -41864,46 +42416,6 @@ const requireRenderReturn = defineRule({
41864
42416
  }
41865
42417
  });
41866
42418
  //#endregion
41867
- //#region src/plugin/rules/state-and-effects/utils/collect-handler-binding-names.ts
41868
- const collectHandlerBindingNames = (componentBody) => {
41869
- const handlerNames = /* @__PURE__ */ new Set();
41870
- walkAst(componentBody, (child) => {
41871
- if (!isNodeOfType(child, "JSXAttribute")) return;
41872
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
41873
- if (!/^on[A-Z]/.test(child.name.name)) return;
41874
- if (!isNodeOfType(child.value, "JSXExpressionContainer")) return;
41875
- const expression = child.value.expression;
41876
- if (isNodeOfType(expression, "Identifier")) handlerNames.add(expression.name);
41877
- });
41878
- return handlerNames;
41879
- };
41880
- //#endregion
41881
- //#region src/plugin/rules/state-and-effects/utils/is-inside-event-handler.ts
41882
- const isInsideEventHandler = (node, handlerBindingNames) => {
41883
- let cursor = node.parent ?? null;
41884
- while (cursor) {
41885
- if (isNodeOfType(cursor, "ArrowFunctionExpression") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "FunctionDeclaration")) {
41886
- let outer = cursor.parent ?? null;
41887
- while (outer) {
41888
- if (isNodeOfType(outer, "JSXAttribute")) {
41889
- const attrName = isNodeOfType(outer.name, "JSXIdentifier") ? outer.name.name : null;
41890
- if (attrName && /^on[A-Z]/.test(attrName)) return true;
41891
- return false;
41892
- }
41893
- if (isNodeOfType(outer, "VariableDeclarator")) {
41894
- const declaredName = isNodeOfType(outer.id, "Identifier") ? outer.id.name : null;
41895
- return Boolean(declaredName && handlerBindingNames.has(declaredName));
41896
- }
41897
- if (isNodeOfType(outer, "Program")) return false;
41898
- outer = outer.parent ?? null;
41899
- }
41900
- return false;
41901
- }
41902
- cursor = cursor.parent ?? null;
41903
- }
41904
- return false;
41905
- };
41906
- //#endregion
41907
42419
  //#region src/plugin/rules/state-and-effects/rerender-defer-reads-hook.ts
41908
42420
  const DEFERRABLE_HOOK_NAMES = new Set([
41909
42421
  "useSearchParams",
@@ -51397,7 +51909,7 @@ const tanstackStartNoNavigateInRender = defineRule({
51397
51909
  if (!isNodeOfType(callParent, "CallExpression")) return false;
51398
51910
  if (callParent.callee === functionNode) return false;
51399
51911
  if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
51400
- return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$3.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
51912
+ return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$4.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
51401
51913
  };
51402
51914
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
51403
51915
  const isEventHandlerNamedProperty = (node) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
@@ -51425,11 +51937,11 @@ const tanstackStartNoNavigateInRender = defineRule({
51425
51937
  if (isNodeOfType(parent, "ReturnStatement")) {
51426
51938
  const outerFunction = findEnclosingFunction(parent);
51427
51939
  const hookName = outerFunction ? getFunctionBindingName$1(outerFunction) : null;
51428
- return Boolean(hookName && HOOK_NAME_PATTERN$3.test(hookName));
51940
+ return Boolean(hookName && HOOK_NAME_PATTERN$4.test(hookName));
51429
51941
  }
51430
51942
  if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
51431
51943
  const hookName = getFunctionBindingName$1(parent);
51432
- return Boolean(hookName && HOOK_NAME_PATTERN$3.test(hookName));
51944
+ return Boolean(hookName && HOOK_NAME_PATTERN$4.test(hookName));
51433
51945
  }
51434
51946
  return false;
51435
51947
  };