eslint-plugin-react-x 5.20.4 → 5.20.6

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 +343 -83
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as core from "@eslint-react/core";
5
5
  import { merge } from "@eslint-react/eslint";
6
6
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
7
7
  import { P, isMatching, match } from "ts-pattern";
8
- import { isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget, resolveObjectType } from "@eslint-react/var";
8
+ import { isAssignmentTargetEqual, isInitializedFromReact, resolveEnclosingAssignmentTarget, resolveObjectType, resolveOrigin } from "@eslint-react/var";
9
9
  import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
10
10
  import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
11
11
  import { findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
@@ -145,7 +145,7 @@ const rules$6 = {
145
145
  //#endregion
146
146
  //#region package.json
147
147
  var name$6 = "eslint-plugin-react-x";
148
- var version = "5.20.4";
148
+ var version = "5.20.6";
149
149
 
150
150
  //#endregion
151
151
  //#region src/utils/create-rule.ts
@@ -1142,32 +1142,13 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
1142
1142
  "splice",
1143
1143
  "unshift"
1144
1144
  ]);
1145
- /**
1146
- * Collect every write target in an assignment, including destructuring
1147
- * patterns such as `[local, globalValue] = source`.
1148
- */
1149
- function getAssignmentTargets(node) {
1150
- const target = Extract.unwrap(node);
1151
- switch (target.type) {
1152
- case AST_NODE_TYPES.Identifier:
1153
- case AST_NODE_TYPES.MemberExpression: return [target];
1154
- case AST_NODE_TYPES.ArrayPattern: return target.elements.flatMap((element) => element == null ? [] : getAssignmentTargets(element));
1155
- case AST_NODE_TYPES.AssignmentPattern: return getAssignmentTargets(target.left);
1156
- case AST_NODE_TYPES.ObjectPattern: return target.properties.flatMap((property) => {
1157
- if (property.type === AST_NODE_TYPES.RestElement) return getAssignmentTargets(property.argument);
1158
- return getAssignmentTargets(property.value);
1159
- });
1160
- case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
1161
- default: return [];
1162
- }
1163
- }
1164
1145
  /** Resolve a direct call target, following simple function aliases. */
1165
1146
  function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
1166
1147
  const expression = Extract.unwrap(node);
1167
1148
  if (Check.isFunction(expression)) return expression;
1168
1149
  if (!Check.isIdentifier(expression) || seen.has(expression)) return null;
1169
1150
  seen.add(expression);
1170
- const resolved = resolve(context, expression);
1151
+ const resolved = resolveOrigin(context, expression);
1171
1152
  if (resolved == null) return null;
1172
1153
  return resolveToFunction(context, resolved, seen);
1173
1154
  }
@@ -1196,7 +1177,7 @@ function createFactCollector$2() {
1196
1177
  facts,
1197
1178
  visitor: {
1198
1179
  AssignmentExpression(node) {
1199
- for (const target of getAssignmentTargets(node.left)) pushWrite(node, target);
1180
+ for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
1200
1181
  },
1201
1182
  CallExpression(node) {
1202
1183
  const caller = getEnclosingFunction(node);
@@ -1216,6 +1197,14 @@ function createFactCollector$2() {
1216
1197
  receiver: callee.object
1217
1198
  });
1218
1199
  },
1200
+ ForInStatement(node) {
1201
+ if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
1202
+ for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
1203
+ },
1204
+ ForOfStatement(node) {
1205
+ if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
1206
+ for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
1207
+ },
1219
1208
  UnaryExpression(node) {
1220
1209
  if (node.operator !== "delete") return;
1221
1210
  const argument = Extract.unwrap(node.argument);
@@ -1445,7 +1434,7 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
1445
1434
  if (Check.isFunction(expr)) return expr;
1446
1435
  if (!Check.isIdentifier(expr) || seen.has(expr)) return null;
1447
1436
  seen.add(expr);
1448
- const resolved = resolve(context, expr);
1437
+ const resolved = resolveOrigin(context, expr);
1449
1438
  return resolved == null ? null : resolveToFunctionNode(context, resolved, seen);
1450
1439
  }
1451
1440
  function resolveVariableOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
@@ -1514,19 +1503,18 @@ function createFactCollector$1() {
1514
1503
  if (root == null || !Check.isIdentifier(root)) return;
1515
1504
  pushMutation("value", node, target, root);
1516
1505
  }
1506
+ function pushTargetMutation(node, target) {
1507
+ if (Check.isIdentifier(target)) {
1508
+ pushMutation("binding", node, target, target);
1509
+ return;
1510
+ }
1511
+ pushMemberMutation(node, target);
1512
+ }
1517
1513
  return {
1518
1514
  facts,
1519
1515
  visitor: {
1520
1516
  AssignmentExpression(node) {
1521
- const target = Extract.unwrap(node.left);
1522
- switch (target.type) {
1523
- case AST_NODE_TYPES.Identifier:
1524
- pushMutation("binding", node, target, target);
1525
- return;
1526
- case AST_NODE_TYPES.MemberExpression:
1527
- pushMemberMutation(node, target);
1528
- return;
1529
- }
1517
+ for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
1530
1518
  },
1531
1519
  CallExpression(node) {
1532
1520
  const callee = Extract.unwrap(node.callee);
@@ -1543,6 +1531,14 @@ function createFactCollector$1() {
1543
1531
  });
1544
1532
  }
1545
1533
  },
1534
+ ForInStatement(node) {
1535
+ if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
1536
+ for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
1537
+ },
1538
+ ForOfStatement(node) {
1539
+ if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
1540
+ for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
1541
+ },
1546
1542
  JSXAttribute(node) {
1547
1543
  if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1548
1544
  const expression = node.value.expression;
@@ -4871,8 +4867,159 @@ const IMPURE_CTORS = /* @__PURE__ */ new Set([
4871
4867
  "XMLHttpRequest"
4872
4868
  ]);
4873
4869
  /**
4874
- * Recursively resolve an identifier to the root builtin global object name.
4875
- * Follows simple assignment chains like `const M = Math` or `const w = window`.
4870
+ * Known pure functions
4871
+ */
4872
+ const PURE_FUNCS = /* @__PURE__ */ new Map([
4873
+ ["Array", /* @__PURE__ */ new Set([
4874
+ "from",
4875
+ "isArray",
4876
+ "of"
4877
+ ])],
4878
+ ["Atomics", /* @__PURE__ */ new Set(["isLockFree"])],
4879
+ ["BigInt", /* @__PURE__ */ new Set(["asIntN", "asUintN"])],
4880
+ ["Date", /* @__PURE__ */ new Set(["parse", "UTC"])],
4881
+ ["globalThis", /* @__PURE__ */ new Set([
4882
+ "atob",
4883
+ "Boolean",
4884
+ "btoa",
4885
+ "decodeURI",
4886
+ "decodeURIComponent",
4887
+ "encodeURI",
4888
+ "encodeURIComponent",
4889
+ "escape",
4890
+ "isFinite",
4891
+ "isNaN",
4892
+ "Number",
4893
+ "parseFloat",
4894
+ "parseInt",
4895
+ "String",
4896
+ "unescape"
4897
+ ])],
4898
+ ["JSON", /* @__PURE__ */ new Set(["parse", "stringify"])],
4899
+ ["Math", /* @__PURE__ */ new Set([
4900
+ "abs",
4901
+ "acos",
4902
+ "acosh",
4903
+ "asin",
4904
+ "asinh",
4905
+ "atan",
4906
+ "atan2",
4907
+ "atanh",
4908
+ "cbrt",
4909
+ "ceil",
4910
+ "clz32",
4911
+ "cos",
4912
+ "cosh",
4913
+ "exp",
4914
+ "expm1",
4915
+ "floor",
4916
+ "fround",
4917
+ "hypot",
4918
+ "imul",
4919
+ "log",
4920
+ "log1p",
4921
+ "log2",
4922
+ "log10",
4923
+ "max",
4924
+ "min",
4925
+ "pow",
4926
+ "round",
4927
+ "sign",
4928
+ "sin",
4929
+ "sinh",
4930
+ "sqrt",
4931
+ "tan",
4932
+ "tanh",
4933
+ "trunc"
4934
+ ])],
4935
+ ["Number", /* @__PURE__ */ new Set([
4936
+ "isFinite",
4937
+ "isInteger",
4938
+ "isNaN",
4939
+ "isSafeInteger",
4940
+ "parseFloat",
4941
+ "parseInt"
4942
+ ])],
4943
+ ["Object", /* @__PURE__ */ new Set([
4944
+ "entries",
4945
+ "fromEntries",
4946
+ "getOwnPropertyDescriptor",
4947
+ "getOwnPropertyDescriptors",
4948
+ "getOwnPropertyNames",
4949
+ "getOwnPropertySymbols",
4950
+ "getPrototypeOf",
4951
+ "groupBy",
4952
+ "hasOwn",
4953
+ "is",
4954
+ "isExtensible",
4955
+ "isFrozen",
4956
+ "isSealed",
4957
+ "keys",
4958
+ "values"
4959
+ ])],
4960
+ ["Reflect", /* @__PURE__ */ new Set([
4961
+ "apply",
4962
+ "construct",
4963
+ "get",
4964
+ "getOwnPropertyDescriptor",
4965
+ "getPrototypeOf",
4966
+ "has",
4967
+ "isExtensible",
4968
+ "ownKeys"
4969
+ ])],
4970
+ ["String", /* @__PURE__ */ new Set([
4971
+ "fromCharCode",
4972
+ "fromCodePoint",
4973
+ "raw"
4974
+ ])]
4975
+ ]);
4976
+ function isCatalogObject(name) {
4977
+ return IMPURE_FUNCS.has(name) || PURE_FUNCS.has(name);
4978
+ }
4979
+ /**
4980
+ * Find the property key under which `name` is bound in an object destructuring
4981
+ * pattern, e.g. `"random"` for `random` in `const { random } = Math` or for
4982
+ * `r` in `const { random: r } = Math`.
4983
+ */
4984
+ function getDestructuredPropertyName(pattern, name) {
4985
+ for (const property of pattern.properties) {
4986
+ if (property.type !== AST_NODE_TYPES.Property) continue;
4987
+ if ((property.value.type === AST_NODE_TYPES.AssignmentPattern ? property.value.left : property.value) !== name) continue;
4988
+ return Extract.getPropertyName(property, "max");
4989
+ }
4990
+ return null;
4991
+ }
4992
+ /**
4993
+ * Resolve a member expression to a builtin object name and property, e.g.
4994
+ * `window.Math.random` resolves to `{ object: "Math", property: "random" }`.
4995
+ * Intermediate segments are followed only while they name known catalog
4996
+ * objects (`window`, `Math`, ...), so unknown-global chains like
4997
+ * `foo.Math.random` do not resolve.
4998
+ */
4999
+ function resolveBuiltinMember(context, node, seen = /* @__PURE__ */ new Set()) {
5000
+ const chain = Extract.getMemberChain(node);
5001
+ const rootId = chain.at(0);
5002
+ if (rootId == null || !Check.isIdentifier(rootId)) return null;
5003
+ const segments = chain.slice(1);
5004
+ if (segments.length === 0 || !segments.every((segment) => Check.isIdentifier(segment))) return null;
5005
+ const resolved = resolveBuiltinObjectName(context, rootId, seen);
5006
+ if (resolved == null) return null;
5007
+ let object = resolved.property ?? resolved.object;
5008
+ const names = segments.map((segment) => segment.name);
5009
+ if (isCatalogObject(object)) for (const name of names.slice(0, -1)) {
5010
+ if (!isCatalogObject(name)) break;
5011
+ object = name;
5012
+ }
5013
+ return {
5014
+ object,
5015
+ property: names.at(-1) ?? null
5016
+ };
5017
+ }
5018
+ /**
5019
+ * Recursively resolve an identifier to a builtin global object name and
5020
+ * optional property. Follows simple assignment chains like `const M = Math`,
5021
+ * member-function aliases like `const random = Math.random`, and destructured
5022
+ * aliases like `const { random } = Math`.
4876
5023
  * Returns `null` if the identifier is locally defined (parameter, import, function declaration, etc.)
4877
5024
  * or resolves to a non-builtin source.
4878
5025
  * @param context - The rule context.
@@ -4884,17 +5031,33 @@ function resolveBuiltinObjectName(context, node, seen = /* @__PURE__ */ new Set(
4884
5031
  seen.add(node.name);
4885
5032
  const scope = context.sourceCode.getScope(node);
4886
5033
  const variable = findVariable(scope, node);
4887
- if (variable == null) return node.name;
5034
+ if (variable == null) return {
5035
+ object: node.name,
5036
+ property: null
5037
+ };
4888
5038
  const def = variable.defs[0];
4889
- if (def == null) return node.name;
4890
- if (def.type === DefinitionType.ImplicitGlobalVariable) return node.name;
5039
+ if (def == null) return {
5040
+ object: node.name,
5041
+ property: null
5042
+ };
5043
+ if (def.type === DefinitionType.ImplicitGlobalVariable) return {
5044
+ object: node.name,
5045
+ property: null
5046
+ };
4891
5047
  if (def.type === DefinitionType.Variable && def.node.init != null) {
4892
5048
  const init = Extract.unwrap(def.node.init);
4893
- if (Check.isIdentifier(init)) return resolveBuiltinObjectName(context, init, seen);
4894
- if (init.type === AST_NODE_TYPES.MemberExpression) {
4895
- const rootId = Extract.getMemberChain(init).at(0);
4896
- if (rootId != null && Check.isIdentifier(rootId)) return resolveBuiltinObjectName(context, rootId, seen);
5049
+ const resolved = Check.isIdentifier(init) ? resolveBuiltinObjectName(context, init, seen) : init.type === AST_NODE_TYPES.MemberExpression ? resolveBuiltinMember(context, init, seen) : null;
5050
+ if (resolved == null) return null;
5051
+ if (def.node.id.type === AST_NODE_TYPES.ObjectPattern) {
5052
+ const key = getDestructuredPropertyName(def.node.id, def.name);
5053
+ if (key == null) return null;
5054
+ return {
5055
+ object: resolved.property ?? resolved.object,
5056
+ property: key
5057
+ };
4897
5058
  }
5059
+ if (def.node.id.type === AST_NODE_TYPES.ArrayPattern) return null;
5060
+ return resolved;
4898
5061
  }
4899
5062
  return null;
4900
5063
  }
@@ -4923,10 +5086,9 @@ function create$7(context) {
4923
5086
  const expr = Extract.unwrap(node.callee);
4924
5087
  switch (true) {
4925
5088
  case Check.isIdentifier(expr): {
4926
- const builtinName = resolveBuiltinObjectName(context, expr);
4927
- if (builtinName == null) return;
4928
- const globalThisImpure = IMPURE_FUNCS.get("globalThis");
4929
- if (globalThisImpure == null || !globalThisImpure.has(builtinName)) return;
5089
+ const resolved = resolveBuiltinObjectName(context, expr);
5090
+ if (resolved == null) return;
5091
+ if (!(resolved.property != null ? IMPURE_FUNCS.get(resolved.object)?.has(resolved.property) ?? false : IMPURE_FUNCS.get("globalThis")?.has(resolved.object) ?? false)) return;
4930
5092
  const func = Traverse.findParent(node, Check.isFunction);
4931
5093
  if (func == null) return;
4932
5094
  cEntries.push({
@@ -4936,13 +5098,9 @@ function create$7(context) {
4936
5098
  break;
4937
5099
  }
4938
5100
  case expr.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(expr.property): {
4939
- const rootId = Extract.getMemberChain(expr.object).at(0);
4940
- if (rootId == null || !Check.isIdentifier(rootId)) return;
4941
- const objectName = resolveBuiltinObjectName(context, rootId);
4942
- if (objectName == null) return;
4943
- const propertyName = expr.property.name;
4944
- const objectImpure = IMPURE_FUNCS.get(objectName);
4945
- if (objectImpure == null || !objectImpure.has(propertyName)) return;
5101
+ const resolved = resolveBuiltinMember(context, expr);
5102
+ if (resolved == null || resolved.property == null) return;
5103
+ if (!IMPURE_FUNCS.get(resolved.object)?.has(resolved.property)) return;
4946
5104
  const func = Traverse.findParent(node, Check.isFunction);
4947
5105
  if (func == null) return;
4948
5106
  cEntries.push({
@@ -4956,10 +5114,11 @@ function create$7(context) {
4956
5114
  NewExpression(node) {
4957
5115
  const expr = Extract.unwrap(node.callee);
4958
5116
  if (!Check.isIdentifier(expr)) return;
4959
- const builtinName = resolveBuiltinObjectName(context, expr);
4960
- if (builtinName == null) return;
4961
- if (!IMPURE_CTORS.has(builtinName)) return;
4962
- if (builtinName === "Date" && node.arguments.length > 0) return;
5117
+ const resolved = resolveBuiltinObjectName(context, expr);
5118
+ if (resolved == null) return;
5119
+ const ctorName = resolved.property != null && isCatalogObject(resolved.object) ? resolved.property : resolved.object;
5120
+ if (!IMPURE_CTORS.has(ctorName)) return;
5121
+ if (ctorName === "Date" && node.arguments.length > 0) return;
4963
5122
  const func = Traverse.findParent(node, Check.isFunction);
4964
5123
  if (func == null) return;
4965
5124
  nEntries.push({
@@ -5029,14 +5188,47 @@ function isReachedThroughFunctions(node, boundary, reached) {
5029
5188
  function getRefAccess(node) {
5030
5189
  let parent = node.parent;
5031
5190
  while (Check.isTypeExpression(parent)) parent = parent.parent;
5032
- const isDirectWrite = parent.type === AST_NODE_TYPES.AssignmentExpression ? parent.left === node || Extract.unwrap(parent.left) === node : parent.type === AST_NODE_TYPES.UpdateExpression ? parent.argument === node || Extract.unwrap(parent.argument) === node : false;
5191
+ const isDirectWrite = parent.type === AST_NODE_TYPES.AssignmentExpression ? parent.left === node || Extract.unwrap(parent.left) === node : parent.type === AST_NODE_TYPES.UpdateExpression ? parent.argument === node || Extract.unwrap(parent.argument) === node : parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "delete" ? parent.argument === node || Extract.unwrap(parent.argument) === node : false;
5033
5192
  return {
5034
5193
  isInitializationWrite: parent.type === AST_NODE_TYPES.AssignmentExpression && parent.operator === "=" && (parent.left === node || Extract.unwrap(parent.left) === node),
5035
- isWrite: isDirectWrite || isNestedRefCurrentWrite(node),
5194
+ isWrite: isDirectWrite || isNestedRefCurrentWrite(node) || isPatternWriteTarget(node),
5036
5195
  node
5037
5196
  };
5038
5197
  }
5039
5198
  /**
5199
+ * Check whether `node` (a `ref.current` MemberExpression) is assigned through a
5200
+ * destructuring pattern or a for-in/of loop target, e.g.
5201
+ * `({ a: ref.current } = value)`, `[ref.current] = value`,
5202
+ * `({ a: ref.current.x } = value)`, or `for (ref.current of items)`. Defaults
5203
+ * and computed keys inside patterns are reads, not writes.
5204
+ */
5205
+ function isPatternWriteTarget(node) {
5206
+ let child = node;
5207
+ let current = node.parent;
5208
+ while (true) {
5209
+ switch (current.type) {
5210
+ case AST_NODE_TYPES.MemberExpression:
5211
+ if (current.object !== child && Extract.unwrap(current.object) !== child) return false;
5212
+ break;
5213
+ case AST_NODE_TYPES.Property:
5214
+ if (current.value !== child) return false;
5215
+ break;
5216
+ case AST_NODE_TYPES.ObjectPattern:
5217
+ case AST_NODE_TYPES.ArrayPattern:
5218
+ case AST_NODE_TYPES.RestElement: break;
5219
+ case AST_NODE_TYPES.AssignmentPattern:
5220
+ if (current.left !== child) return false;
5221
+ break;
5222
+ case AST_NODE_TYPES.AssignmentExpression: return current.operator === "=" && current.left === child;
5223
+ case AST_NODE_TYPES.ForInStatement:
5224
+ case AST_NODE_TYPES.ForOfStatement: return current.left === child;
5225
+ default: return false;
5226
+ }
5227
+ child = current;
5228
+ current = current.parent;
5229
+ }
5230
+ }
5231
+ /**
5040
5232
  * Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
5041
5233
  * a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
5042
5234
  */
@@ -5054,6 +5246,7 @@ function isNestedRefCurrentWrite(node) {
5054
5246
  if (!sawNesting) return false;
5055
5247
  if (parent.type === AST_NODE_TYPES.AssignmentExpression) return parent.left === outer || Extract.unwrap(parent.left) === outer;
5056
5248
  if (parent.type === AST_NODE_TYPES.UpdateExpression) return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
5249
+ if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "delete") return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
5057
5250
  return false;
5058
5251
  }
5059
5252
  }
@@ -5137,15 +5330,17 @@ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
5137
5330
  /**
5138
5331
  * Collect the raw facts the refs rule reasons about: identifier/member/function
5139
5332
  * bindings with their source positions, JSX `ref` attribute targets, call
5140
- * expressions, and every `.current` member access. Facts outside any function
5141
- * are never reachable during render and are not collected. All inference is
5142
- * deferred to `Program:exit`.
5333
+ * expressions, ref pass sites (calls, constructor calls, tagged templates), and
5334
+ * every `.current` member access. Facts outside any function are never
5335
+ * reachable during render and are not collected. All inference is deferred to
5336
+ * `Program:exit`.
5143
5337
  */
5144
5338
  function createFactCollector() {
5145
5339
  const facts = {
5146
5340
  bindings: [],
5147
5341
  callEdges: [],
5148
5342
  jsxRefs: [],
5343
+ passSites: [],
5149
5344
  refAccesses: []
5150
5345
  };
5151
5346
  return {
@@ -5180,6 +5375,10 @@ function createFactCollector() {
5180
5375
  caller,
5181
5376
  node
5182
5377
  });
5378
+ facts.passSites.push({
5379
+ caller,
5380
+ node
5381
+ });
5183
5382
  },
5184
5383
  FunctionDeclaration(node) {
5185
5384
  if (node.id != null) facts.bindings.push({
@@ -5204,6 +5403,22 @@ function createFactCollector() {
5204
5403
  enclosingFunction
5205
5404
  });
5206
5405
  },
5406
+ NewExpression(node) {
5407
+ const caller = Traverse.findParent(node, Check.isFunction);
5408
+ if (caller == null) return;
5409
+ facts.passSites.push({
5410
+ caller,
5411
+ node
5412
+ });
5413
+ },
5414
+ TaggedTemplateExpression(node) {
5415
+ const caller = Traverse.findParent(node, Check.isFunction);
5416
+ if (caller == null) return;
5417
+ facts.passSites.push({
5418
+ caller,
5419
+ node
5420
+ });
5421
+ },
5207
5422
  VariableDeclarator(node) {
5208
5423
  if (!Check.isIdentifier(node.id) || node.init == null) return;
5209
5424
  facts.bindings.push({
@@ -5322,21 +5537,43 @@ function inferRefViolations(accesses, boundaries, reachability, resolver) {
5322
5537
  return violations;
5323
5538
  }
5324
5539
  /**
5540
+ * Get the statically known name of a pass-site callee, whether it is a plain
5541
+ * call, a constructor call, or a tagged template.
5542
+ */
5543
+ function getPassSiteCalleeName(node) {
5544
+ const callee = Extract.unwrap(node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.tag : node.callee);
5545
+ if (Check.isIdentifier(callee)) return callee.name;
5546
+ if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && Check.isIdentifier(callee.property)) return callee.property.name;
5547
+ return null;
5548
+ }
5549
+ /**
5550
+ * The `mergeRefs` exemption is name-based but alias-aware: a simple variable
5551
+ * alias of a `mergeRefs` binding keeps the exemption.
5552
+ */
5553
+ function isMergeRefsCallee(callee, calleeName, position, resolver) {
5554
+ if (calleeName === "mergeRefs") return true;
5555
+ if (callee.type !== AST_NODE_TYPES.Identifier) return false;
5556
+ const variable = resolver.getVariable(callee);
5557
+ return variable != null && resolver.resolveAliasTarget(variable, position).name === "mergeRefs";
5558
+ }
5559
+ /**
5325
5560
  * Passing a ref to an unknown function can expose its value during render.
5326
5561
  * Hook callbacks, mergeRefs, and the existing render-prop compatibility case
5327
5562
  * remain exempt.
5328
5563
  */
5329
- function inferRefPassViolations(callEdges, boundaries, reachability, resolver) {
5564
+ function inferRefPassViolations(passSites, boundaries, reachability, resolver) {
5330
5565
  const violations = [];
5331
- for (const edge of callEdges) {
5332
- const boundary = getBoundaryOf(edge.caller, boundaries);
5333
- if (boundary == null || !isReachedDuringRender(edge.node, boundary, reachability)) continue;
5334
- const call = edge.node;
5335
- const callee = Extract.unwrap(call.callee);
5336
- const calleeName = Extract.getCalleeName(call);
5337
- const callArguments = call.arguments;
5338
- if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
5339
- for (const argument of callArguments) {
5566
+ for (const site of passSites) {
5567
+ const boundary = getBoundaryOf(site.caller, boundaries);
5568
+ if (boundary == null || !isReachedDuringRender(site.node, boundary, reachability)) continue;
5569
+ const node = site.node;
5570
+ const callee = Extract.unwrap(node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.tag : node.callee);
5571
+ const calleeName = getPassSiteCalleeName(node);
5572
+ const args = node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.quasi.expressions : node.arguments;
5573
+ if (node.type === AST_NODE_TYPES.CallExpression && core.isHookCall(node)) continue;
5574
+ if (isMergeRefsCallee(callee, calleeName, node.range[0], resolver)) continue;
5575
+ if (calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
5576
+ for (const argument of args) {
5340
5577
  if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
5341
5578
  const value = Extract.unwrap(argument);
5342
5579
  if (!Check.isIdentifier(value)) continue;
@@ -5450,6 +5687,18 @@ function createBindingResolver(context, facts) {
5450
5687
  case "unknown": return null;
5451
5688
  }
5452
5689
  }
5690
+ /**
5691
+ * Follow simple variable-to-variable aliases to the terminal variable, e.g.
5692
+ * `const combine = mergeRefs` resolves to the `mergeRefs` variable itself.
5693
+ * Position-aware, so a reassigned alias resolves to its latest source.
5694
+ */
5695
+ function resolveAliasTarget(variable, position, seen = /* @__PURE__ */ new Set()) {
5696
+ if (seen.has(variable)) return variable;
5697
+ seen.add(variable);
5698
+ const event = getLatestValue(bindings.get(variable), position);
5699
+ if (event != null && event.value.kind === "variable") return resolveAliasTarget(event.value.variable, event.position, seen);
5700
+ return variable;
5701
+ }
5453
5702
  function resolveCallable(node, position) {
5454
5703
  const callee = Extract.unwrap(node);
5455
5704
  if (isFunctionExpressionLike(callee)) return callee;
@@ -5499,6 +5748,7 @@ function createBindingResolver(context, facts) {
5499
5748
  getNullBranch,
5500
5749
  getRefTarget,
5501
5750
  getVariable,
5751
+ resolveAliasTarget,
5502
5752
  resolveCallable,
5503
5753
  resolveRef
5504
5754
  };
@@ -5540,7 +5790,7 @@ function create$6(context) {
5540
5790
  const resolver = createBindingResolver(context, facts.facts);
5541
5791
  const callGraph = inferCallGraph(boundaries, facts.facts.callEdges, resolver.resolveCallable);
5542
5792
  const reachability = collectReachableFunctions(boundaries, callGraph);
5543
- const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.callEdges, boundaries, reachability, resolver)];
5793
+ const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.passSites, boundaries, reachability, resolver)];
5544
5794
  for (const violation of violations) context.report({
5545
5795
  messageId: match(violation).with({ kind: "duplicate-init" }, () => "duplicateRefInit").with({ kind: "pass" }, () => "refPassedToFunction").with({ kind: "read" }, () => "readDuringRender").with({ kind: "write" }, () => "writeDuringRender").exhaustive(),
5546
5796
  node: violation.node
@@ -7887,7 +8137,7 @@ function create$5(context) {
7887
8137
  }
7888
8138
  }
7889
8139
  function isIdFromUseStateCall(id, at) {
7890
- const initNode = resolve(context, id);
8140
+ const initNode = resolveOrigin(context, id);
7891
8141
  if (initNode == null) return false;
7892
8142
  if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
7893
8143
  if (!isUseStateCall(initNode)) return false;
@@ -8012,7 +8262,7 @@ function create$5(context) {
8012
8262
  const getSetStateCalls = (context, id) => {
8013
8263
  const variable = findVariable(context.sourceCode.getScope(id), id);
8014
8264
  if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
8015
- const node = resolve(context, id);
8265
+ const node = resolveOrigin(context, id);
8016
8266
  switch (node?.type) {
8017
8267
  case AST_NODE_TYPES.ArrowFunctionExpression:
8018
8268
  case AST_NODE_TYPES.FunctionDeclaration:
@@ -8112,7 +8362,7 @@ function create$4(context) {
8112
8362
  return core.isUseStateLikeCall(node, additionalStateHooks);
8113
8363
  }
8114
8364
  function isIdFromUseStateCall(topLevelId, at) {
8115
- const initNode = resolve(context, topLevelId);
8365
+ const initNode = resolveOrigin(context, topLevelId);
8116
8366
  if (initNode == null) return false;
8117
8367
  if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
8118
8368
  if (!isUseStateCall(initNode)) return false;
@@ -8473,18 +8723,28 @@ function create$1(context) {
8473
8723
  function validateNoOuterVariableReassignment(callback) {
8474
8724
  const violations = [];
8475
8725
  if (callback.body == null) return violations;
8476
- simpleTraverse(callback.body, { enter(node) {
8477
- if (node.type !== AST_NODE_TYPES.AssignmentExpression) return;
8478
- const left = Extract.unwrap(node.left);
8479
- if (!Check.isIdentifier(left)) return;
8726
+ function checkWriteTarget(node, target) {
8727
+ if (!Check.isIdentifier(target)) return;
8480
8728
  if (Traverse.findParent(node, Check.isFunction, (n) => n === callback) != null) return;
8481
- const scope = context.sourceCode.getScope(left);
8482
- const variable = findVariable(scope, left);
8729
+ const scope = context.sourceCode.getScope(target);
8730
+ const variable = findVariable(scope, target);
8483
8731
  if (variable != null && variable.defs.length > 0 && isDeclaredInsideCallback(variable, callback)) return;
8484
8732
  violations.push({
8485
8733
  messageId: "noReassigningOuterVariables",
8486
- node: left
8734
+ node: target
8487
8735
  });
8736
+ }
8737
+ simpleTraverse(callback.body, { enter(node) {
8738
+ switch (node.type) {
8739
+ case AST_NODE_TYPES.AssignmentExpression:
8740
+ for (const target of Extract.getAssignmentTargets(node.left)) checkWriteTarget(node, target);
8741
+ return;
8742
+ case AST_NODE_TYPES.ForInStatement:
8743
+ case AST_NODE_TYPES.ForOfStatement:
8744
+ if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
8745
+ for (const target of Extract.getAssignmentTargets(node.left)) checkWriteTarget(node, target);
8746
+ return;
8747
+ }
8488
8748
  } });
8489
8749
  return violations;
8490
8750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.20.4",
3
+ "version": "5.20.6",
4
4
  "description": "A set of composable ESLint rules for libraries and frameworks that use React as a UI runtime.",
5
5
  "keywords": [
6
6
  "react",
@@ -36,12 +36,12 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@eslint-react/ast": "5.20.4",
40
- "@eslint-react/core": "5.20.4",
41
- "@eslint-react/eslint": "5.20.4",
42
- "@eslint-react/jsx": "5.20.4",
43
- "@eslint-react/shared": "5.20.4",
44
- "@eslint-react/var": "5.20.4",
39
+ "@eslint-react/ast": "5.20.6",
40
+ "@eslint-react/core": "5.20.6",
41
+ "@eslint-react/eslint": "5.20.6",
42
+ "@eslint-react/jsx": "5.20.6",
43
+ "@eslint-react/shared": "5.20.6",
44
+ "@eslint-react/var": "5.20.6",
45
45
  "@typescript-eslint/scope-manager": "^8.70.0",
46
46
  "@typescript-eslint/type-utils": "^8.70.0",
47
47
  "@typescript-eslint/types": "^8.70.0",
@@ -63,7 +63,7 @@
63
63
  "react-dom": "^19.3.0",
64
64
  "tsdown": "^0.23.0",
65
65
  "tsl": "^1.0.30",
66
- "tsl-dx": "^0.13.5",
66
+ "tsl-dx": "^0.13.6",
67
67
  "typescript": "6.0.3"
68
68
  },
69
69
  "peerDependencies": {