eslint-plugin-react-x 5.19.0 → 5.20.0

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 +241 -138
  2. package/package.json +17 -17
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@ import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
4
4
  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
+ import { P, isMatching, match } from "ts-pattern";
7
8
  import { isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget, resolveObjectType } from "@eslint-react/var";
8
9
  import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
9
10
  import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
10
11
  import { findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
11
12
  import { compare } from "compare-versions";
12
- import { P, isMatching, match } from "ts-pattern";
13
13
  import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
14
14
  import { unionConstituents } from "ts-api-utils";
15
15
  import "typescript";
@@ -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.19.0";
148
+ var version = "5.20.0";
149
149
 
150
150
  //#endregion
151
151
  //#region src/utils/create-rule.ts
@@ -1143,6 +1143,97 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
1143
1143
  "unshift"
1144
1144
  ]);
1145
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
+ /** Resolve a direct call target, following simple function aliases. */
1165
+ function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
1166
+ const expression = Extract.unwrap(node);
1167
+ if (Check.isFunction(expression)) return expression;
1168
+ if (!Check.isIdentifier(expression) || seen.has(expression)) return null;
1169
+ seen.add(expression);
1170
+ const resolved = resolve(context, expression);
1171
+ if (resolved == null) return null;
1172
+ return resolveToFunction(context, resolved, seen);
1173
+ }
1174
+
1175
+ //#endregion
1176
+ //#region src/rules/globals/collect.ts
1177
+ function createGlobalsCollector() {
1178
+ const facts = {
1179
+ callEdges: [],
1180
+ methodCalls: [],
1181
+ writes: []
1182
+ };
1183
+ function getEnclosingFunction(node) {
1184
+ return Traverse.findParent(node, Check.isFunction);
1185
+ }
1186
+ function pushWrite(node, target) {
1187
+ const enclosingFunction = getEnclosingFunction(node);
1188
+ if (enclosingFunction == null) return;
1189
+ facts.writes.push({
1190
+ enclosingFunction,
1191
+ node,
1192
+ target
1193
+ });
1194
+ }
1195
+ return {
1196
+ facts,
1197
+ visitor: {
1198
+ AssignmentExpression(node) {
1199
+ for (const target of getAssignmentTargets(node.left)) pushWrite(node, target);
1200
+ },
1201
+ CallExpression(node) {
1202
+ const caller = getEnclosingFunction(node);
1203
+ if (caller != null) facts.callEdges.push({
1204
+ callee: node.callee,
1205
+ caller
1206
+ });
1207
+ const callee = Extract.unwrap(node.callee);
1208
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
1209
+ const method = Extract.getCalleeName(node);
1210
+ if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
1211
+ if (caller == null) return;
1212
+ facts.methodCalls.push({
1213
+ enclosingFunction: caller,
1214
+ method,
1215
+ node,
1216
+ receiver: callee.object
1217
+ });
1218
+ },
1219
+ UnaryExpression(node) {
1220
+ if (node.operator !== "delete") return;
1221
+ const argument = Extract.unwrap(node.argument);
1222
+ if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
1223
+ pushWrite(node, argument);
1224
+ },
1225
+ UpdateExpression(node) {
1226
+ const argument = Extract.unwrap(node.argument);
1227
+ if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
1228
+ pushWrite(node, argument);
1229
+ }
1230
+ }
1231
+ };
1232
+ }
1233
+
1234
+ //#endregion
1235
+ //#region src/rules/globals/origins.ts
1236
+ /**
1146
1237
  * Return whether an identifier is an unresolved global or is declared in the
1147
1238
  * global/module scope.
1148
1239
  */
@@ -1174,34 +1265,85 @@ function resolveGlobalOrigin(context, node, seen = /* @__PURE__ */ new Set()) {
1174
1265
  if (initializer.type !== AST_NODE_TYPES.Identifier && initializer.type !== AST_NODE_TYPES.MemberExpression) return null;
1175
1266
  return resolveGlobalOrigin(context, initializer, seen);
1176
1267
  }
1268
+
1269
+ //#endregion
1270
+ //#region src/rules/globals/effects.ts
1177
1271
  /**
1178
- * Collect every write target in an assignment, including destructuring
1179
- * patterns such as `[local, globalValue] = source`.
1272
+ * Classify the collected writes and mutating method calls, keeping only the
1273
+ * ones that reach a global/module binding, grouped by the function that
1274
+ * performs them.
1180
1275
  */
1181
- function getAssignmentTargets(node) {
1182
- const target = Extract.unwrap(node);
1183
- switch (target.type) {
1184
- case AST_NODE_TYPES.Identifier:
1185
- case AST_NODE_TYPES.MemberExpression: return [target];
1186
- case AST_NODE_TYPES.ArrayPattern: return target.elements.flatMap((element) => element == null ? [] : getAssignmentTargets(element));
1187
- case AST_NODE_TYPES.AssignmentPattern: return getAssignmentTargets(target.left);
1188
- case AST_NODE_TYPES.ObjectPattern: return target.properties.flatMap((property) => {
1189
- if (property.type === AST_NODE_TYPES.RestElement) return getAssignmentTargets(property.argument);
1190
- return getAssignmentTargets(property.value);
1276
+ function inferGlobalMutations(context, facts) {
1277
+ const directEffects = /* @__PURE__ */ new Map();
1278
+ function pushEffect(enclosingFunction, effect) {
1279
+ const effects = directEffects.get(enclosingFunction) ?? [];
1280
+ effects.push(effect);
1281
+ directEffects.set(enclosingFunction, effects);
1282
+ }
1283
+ for (const write of facts.writes) {
1284
+ if (Check.isIdentifier(write.target)) {
1285
+ if (!isGlobalVariable(context, write.target)) continue;
1286
+ pushEffect(write.enclosingFunction, {
1287
+ kind: "global",
1288
+ name: write.target.name,
1289
+ method: null,
1290
+ node: write.node
1291
+ });
1292
+ continue;
1293
+ }
1294
+ if (resolveGlobalOrigin(context, write.target.object) == null) continue;
1295
+ pushEffect(write.enclosingFunction, {
1296
+ kind: "property",
1297
+ name: context.sourceCode.getText(write.target),
1298
+ method: null,
1299
+ node: write.node
1191
1300
  });
1192
- case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
1193
- default: return [];
1194
1301
  }
1302
+ for (const call of facts.methodCalls) {
1303
+ const origin = resolveGlobalOrigin(context, call.receiver);
1304
+ if (origin == null) continue;
1305
+ pushEffect(call.enclosingFunction, {
1306
+ kind: "method",
1307
+ name: origin.name,
1308
+ method: call.method,
1309
+ node: call.node
1310
+ });
1311
+ }
1312
+ return directEffects;
1195
1313
  }
1196
- /** Resolve a direct call target, following simple function aliases. */
1197
- function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
1198
- const expression = Extract.unwrap(node);
1199
- if (Check.isFunction(expression)) return expression;
1200
- if (!Check.isIdentifier(expression) || seen.has(expression)) return null;
1201
- seen.add(expression);
1202
- const resolved = resolve(context, expression);
1203
- if (resolved == null) return null;
1204
- return resolveToFunction(context, resolved, seen);
1314
+ /** Resolve the collected call edges into a function-to-function call graph. */
1315
+ function inferCallGraph(context, callEdges) {
1316
+ const callGraph = /* @__PURE__ */ new Map();
1317
+ for (const edge of callEdges) {
1318
+ const callee = resolveToFunction(context, edge.callee);
1319
+ if (callee == null) continue;
1320
+ const callees = callGraph.get(edge.caller) ?? /* @__PURE__ */ new Set();
1321
+ callees.add(callee);
1322
+ callGraph.set(edge.caller, callees);
1323
+ }
1324
+ return callGraph;
1325
+ }
1326
+ /**
1327
+ * Like the SPEC's function signatures, these summaries keep creation of an
1328
+ * effect separate from applying it in a component or hook render: walk the
1329
+ * call graph from each render function and gather every reachable effect once.
1330
+ */
1331
+ function collectReachableEffects(renderFunctions, directEffects, callGraph) {
1332
+ const visited = /* @__PURE__ */ new Set();
1333
+ const reported = /* @__PURE__ */ new Set();
1334
+ const reachable = [];
1335
+ function applyFunctionEffects(func) {
1336
+ if (visited.has(func)) return;
1337
+ visited.add(func);
1338
+ for (const effect of directEffects.get(func) ?? []) {
1339
+ if (reported.has(effect)) continue;
1340
+ reported.add(effect);
1341
+ reachable.push(effect);
1342
+ }
1343
+ for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
1344
+ }
1345
+ for (const func of renderFunctions) applyFunctionEffects(func);
1346
+ return reachable;
1205
1347
  }
1206
1348
 
1207
1349
  //#endregion
@@ -1223,87 +1365,25 @@ var globals_default = createRule({
1223
1365
  defaultOptions: []
1224
1366
  });
1225
1367
  function create$49(context) {
1226
- const hc = core.getHookCollector(context);
1227
- const fc = core.getFunctionComponentCollector(context);
1228
- const directEffects = /* @__PURE__ */ new Map();
1229
- const callGraph = /* @__PURE__ */ new Map();
1230
- function getEnclosingFunction(node) {
1231
- return Traverse.findParent(node, Check.isFunction);
1232
- }
1233
- function recordEffect(node, messageId, data) {
1234
- const enclosing = getEnclosingFunction(node);
1235
- if (enclosing == null) return;
1236
- const effects = directEffects.get(enclosing) ?? [];
1237
- effects.push({
1238
- data,
1239
- messageId,
1240
- node
1241
- });
1242
- directEffects.set(enclosing, effects);
1243
- }
1244
- function recordWrite(node, target) {
1245
- if (Check.isIdentifier(target)) {
1246
- if (!isGlobalVariable(context, target)) return;
1247
- recordEffect(node, "mutatingGlobal", { name: target.name });
1248
- return;
1249
- }
1250
- if (resolveGlobalOrigin(context, target.object) == null) return;
1251
- recordEffect(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(target) });
1252
- }
1253
- function recordCallEdge(node) {
1254
- const caller = getEnclosingFunction(node);
1255
- if (caller == null) return;
1256
- const callee = resolveToFunction(context, node.callee);
1257
- if (callee == null) return;
1258
- const callees = callGraph.get(caller) ?? /* @__PURE__ */ new Set();
1259
- callees.add(callee);
1260
- callGraph.set(caller, callees);
1261
- }
1262
- return merge(hc.visitor, fc.visitor, {
1263
- AssignmentExpression(node) {
1264
- for (const target of getAssignmentTargets(node.left)) recordWrite(node, target);
1265
- },
1266
- CallExpression(node) {
1267
- recordCallEdge(node);
1268
- const callee = Extract.unwrap(node.callee);
1269
- if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
1270
- const method = Extract.getCalleeName(node);
1271
- if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
1272
- const origin = resolveGlobalOrigin(context, callee.object);
1273
- if (origin == null) return;
1274
- recordEffect(node, "mutatingGlobalArrayMethod", {
1275
- name: origin.name,
1276
- method
1368
+ const hooks = core.getHookCollector(context);
1369
+ const comps = core.getFunctionComponentCollector(context);
1370
+ const globs = createGlobalsCollector();
1371
+ return merge(hooks.visitor, comps.visitor, globs.visitor, { "Program:exit"(program) {
1372
+ const renderFunctions = [...comps.api.getAllComponents(program), ...hooks.api.getAllHooks(program)].map(({ node }) => node);
1373
+ const directEffects = inferGlobalMutations(context, globs.facts);
1374
+ const callGraph = inferCallGraph(context, globs.facts.callEdges);
1375
+ for (const effect of collectReachableEffects(renderFunctions, directEffects, callGraph)) {
1376
+ const data = effect.method == null ? { name: effect.name } : {
1377
+ name: effect.name,
1378
+ method: effect.method
1379
+ };
1380
+ context.report({
1381
+ data,
1382
+ messageId: match(effect).with({ kind: "global" }, () => "mutatingGlobal").with({ kind: "method" }, () => "mutatingGlobalArrayMethod").with({ kind: "property" }, () => "mutatingGlobalProperty").exhaustive(),
1383
+ node: effect.node
1277
1384
  });
1278
- },
1279
- "Program:exit"(program) {
1280
- const renderFunctions = [...fc.api.getAllComponents(program), ...hc.api.getAllHooks(program)];
1281
- const visited = /* @__PURE__ */ new Set();
1282
- const reported = /* @__PURE__ */ new Set();
1283
- function applyFunctionEffects(func) {
1284
- if (visited.has(func)) return;
1285
- visited.add(func);
1286
- for (const effect of directEffects.get(func) ?? []) {
1287
- if (reported.has(effect)) continue;
1288
- reported.add(effect);
1289
- context.report(effect);
1290
- }
1291
- for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
1292
- }
1293
- for (const { node } of renderFunctions) applyFunctionEffects(node);
1294
- },
1295
- UnaryExpression(node) {
1296
- if (node.operator !== "delete") return;
1297
- const argument = Extract.unwrap(node.argument);
1298
- if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
1299
- recordWrite(node, argument);
1300
- },
1301
- UpdateExpression(node) {
1302
- const argument = Extract.unwrap(node.argument);
1303
- if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
1304
- recordWrite(node, argument);
1305
1385
  }
1306
- });
1386
+ } });
1307
1387
  }
1308
1388
 
1309
1389
  //#endregion
@@ -1314,7 +1394,7 @@ function create$49(context) {
1314
1394
  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
1315
1395
  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
1316
1396
  */
1317
- const MUTATING_METHODS = /* @__PURE__ */ new Set([
1397
+ const KNOWN_MUTATING_METHODS = /* @__PURE__ */ new Set([
1318
1398
  "add",
1319
1399
  "clear",
1320
1400
  "copyWithin",
@@ -1330,9 +1410,9 @@ const MUTATING_METHODS = /* @__PURE__ */ new Set([
1330
1410
  "unshift"
1331
1411
  ]);
1332
1412
  /**
1333
- * Known navigation hooks.
1413
+ * Known mutating hooks.
1334
1414
  */
1335
- const NAVIGATION_HOOKS = /* @__PURE__ */ new Set([
1415
+ const KNOWN_MUTATING_HOOKS = /* @__PURE__ */ new Set([
1336
1416
  "useHistory",
1337
1417
  "useNavigate",
1338
1418
  "useNavigation",
@@ -1346,6 +1426,20 @@ function isNodeWithin(node, ancestor) {
1346
1426
  }
1347
1427
  return false;
1348
1428
  }
1429
+ function isComponentPropsDefinition(def, components) {
1430
+ if (def.type !== DefinitionType.Parameter) return false;
1431
+ const fn = def.node;
1432
+ if (!Check.isFunction(fn)) return false;
1433
+ const firstParam = fn.params.at(0);
1434
+ if (firstParam == null || !isNodeWithin(def.name, firstParam)) return false;
1435
+ return components.includes(fn);
1436
+ }
1437
+ function getStateHookName(context, init) {
1438
+ const { additionalStateHooks } = getSettingsFromContext(context);
1439
+ if (core.isUseStateLikeCall(init, additionalStateHooks)) return Extract.getCalleeName(init) ?? "useState";
1440
+ if (core.isUseReducerCall(context, init)) return "useReducer";
1441
+ return null;
1442
+ }
1349
1443
  function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set()) {
1350
1444
  const expr = Extract.unwrap(node);
1351
1445
  if (Check.isFunction(expr)) return expr;
@@ -1390,7 +1484,7 @@ function isInitializedFromUseRef(context, node) {
1390
1484
  function isKnownNonMutatingMethodCall(context, node) {
1391
1485
  const callee = Extract.unwrap(node.callee);
1392
1486
  return Check.isExpression(callee) && isInitializedFromCall(context, callee, (init) => {
1393
- return NAVIGATION_HOOKS.values().some((hook) => core.isAPICall(hook)(context, init));
1487
+ return KNOWN_MUTATING_HOOKS.values().some((hook) => core.isAPICall(hook)(context, init));
1394
1488
  });
1395
1489
  }
1396
1490
  function isRefLikeChain(context, node) {
@@ -1435,7 +1529,7 @@ function createImmutabilityCollector() {
1435
1529
  const callee = Extract.unwrap(node.callee);
1436
1530
  if (callee.type === AST_NODE_TYPES.MemberExpression) {
1437
1531
  const method = Extract.getCalleeName(node);
1438
- if (method != null && MUTATING_METHODS.has(method)) {
1532
+ if (method != null && KNOWN_MUTATING_METHODS.has(method)) {
1439
1533
  const root = Extract.getIdentifierAt(callee.object, 0);
1440
1534
  if (root != null) pushMutation("value", node, callee.object, root);
1441
1535
  }
@@ -1484,42 +1578,43 @@ function createImmutabilityCollector() {
1484
1578
 
1485
1579
  //#endregion
1486
1580
  //#region src/rules/immutability/origins.ts
1487
- function isComponentPropsDefinition(context, def) {
1488
- if (def.type !== DefinitionType.Parameter) return false;
1489
- const fn = def.node;
1490
- if (!Check.isFunction(fn)) return false;
1491
- const firstParam = fn.params.at(0);
1492
- if (firstParam == null || !isNodeWithin(def.name, firstParam)) return false;
1493
- return core.isFunctionComponentDefinition(context, fn, core.DEFAULT_COMPONENT_DETECTION_HINT);
1494
- }
1495
- function getStateHookName(context, init) {
1496
- const { additionalStateHooks } = getSettingsFromContext(context);
1497
- if (core.isUseStateLikeCall(init, additionalStateHooks)) return Extract.getCalleeName(init) ?? "useState";
1498
- if (core.isUseReducerCall(context, init)) return "useReducer";
1499
- return null;
1500
- }
1501
1581
  /**
1502
1582
  * Classify whether a variable ultimately holds a value that must be treated as
1503
1583
  * immutable: a component's props, a state value returned from `useState`-like or
1504
- * `useReducer` calls, or a shallow copy (spread literal) of either.
1584
+ * `useReducer` calls, a shallow copy (spread literal) of either, or a `for...of`
1585
+ * iterator variable whose iterated collection resolves to one of those.
1505
1586
  * @param context The rule context.
1506
1587
  * @param variable The variable to classify.
1507
- * @param seen Variables already visited during spread recursion.
1588
+ * @param components The confirmed function component nodes in the file.
1589
+ * @param seen Variables already visited during spread/iterator recursion.
1508
1590
  * @returns The frozen origin, or `null` when the variable is not derived from one.
1509
1591
  */
1510
- function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
1592
+ function classifyFrozenOrigin(context, variable, components, seen = /* @__PURE__ */ new Set()) {
1511
1593
  if (seen.has(variable)) return null;
1512
1594
  seen.add(variable);
1513
1595
  const origin = resolveVariableOrigin(context, variable);
1514
1596
  const def = origin.defs.length === 1 ? origin.defs[0] : null;
1515
1597
  if (def == null) return null;
1516
- if (isComponentPropsDefinition(context, def)) return {
1598
+ if (isComponentPropsDefinition(def, components)) return {
1517
1599
  kind: "props",
1518
1600
  name: origin.name
1519
1601
  };
1520
1602
  if (def.type !== DefinitionType.Variable) return null;
1521
- const init = def.node.init == null ? null : Extract.unwrap(def.node.init);
1522
- if (init == null) return null;
1603
+ if (def.node.init == null) {
1604
+ const loop = def.node.parent.parent;
1605
+ if (loop.type !== AST_NODE_TYPES.ForOfStatement || loop.left !== def.node.parent) return null;
1606
+ const root = Extract.getIdentifierAt(loop.right, 0);
1607
+ if (root == null) return null;
1608
+ const source = findVariable(context.sourceCode.getScope(root), root);
1609
+ if (source == null) return null;
1610
+ const inner = classifyFrozenOrigin(context, source, components, seen);
1611
+ return inner == null ? null : {
1612
+ kind: "iterator",
1613
+ name: origin.name,
1614
+ original: inner.name
1615
+ };
1616
+ }
1617
+ const init = Extract.unwrap(def.node.init);
1523
1618
  switch (init.type) {
1524
1619
  case AST_NODE_TYPES.CallExpression: {
1525
1620
  const hook = getStateHookName(context, init);
@@ -1542,7 +1637,7 @@ function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set(
1542
1637
  if (!Check.isIdentifier(argument)) continue;
1543
1638
  const source = findVariable(context.sourceCode.getScope(argument), argument);
1544
1639
  if (source == null) continue;
1545
- const inner = classifyFrozenOrigin(context, source, seen);
1640
+ const inner = classifyFrozenOrigin(context, source, components, seen);
1546
1641
  if (inner != null) return {
1547
1642
  kind: "shallow-copy",
1548
1643
  name: origin.name,
@@ -1591,7 +1686,7 @@ function getMutatedObject(mutation) {
1591
1686
  if (mutation.node.type === AST_NODE_TYPES.CallExpression) return target;
1592
1687
  return target.type === AST_NODE_TYPES.MemberExpression ? Extract.unwrap(target.object) : target;
1593
1688
  }
1594
- function inferDirectMutations(context, mutations) {
1689
+ function inferDirectMutations(context, mutations, components) {
1595
1690
  const directMutations = [];
1596
1691
  for (const mutation of mutations) {
1597
1692
  if (mutation.kind !== "value") continue;
@@ -1599,7 +1694,7 @@ function inferDirectMutations(context, mutations) {
1599
1694
  if (isRefMutation(context, mutation)) continue;
1600
1695
  const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
1601
1696
  if (variable == null) continue;
1602
- const origin = classifyFrozenOrigin(context, variable);
1697
+ const origin = classifyFrozenOrigin(context, variable, components);
1603
1698
  if (origin == null) continue;
1604
1699
  switch (origin.kind) {
1605
1700
  case "props":
@@ -1623,6 +1718,12 @@ function inferDirectMutations(context, mutations) {
1623
1718
  detail: `It is a shallow copy of '${origin.original}'; mutating nested values through it mutates '${origin.original}' in place.`,
1624
1719
  node: mutation.node
1625
1720
  });
1721
+ break;
1722
+ case "iterator": directMutations.push({
1723
+ name: origin.name,
1724
+ detail: `It is an element of '${origin.original}' and must be treated as immutable.`,
1725
+ node: mutation.node
1726
+ });
1626
1727
  }
1627
1728
  }
1628
1729
  return directMutations;
@@ -1648,17 +1749,18 @@ var immutability_default = createRule({
1648
1749
  });
1649
1750
  function create$48(context) {
1650
1751
  const hooks = core.getHookCollector(context);
1651
- const collector = createImmutabilityCollector();
1652
- return merge(hooks.visitor, collector.visitor, { "Program:exit"(program) {
1653
- for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) collector.facts.sinks.push({
1752
+ const comps = core.getFunctionComponentCollector(context);
1753
+ const immut = createImmutabilityCollector();
1754
+ return merge(hooks.visitor, comps.visitor, immut.visitor, { "Program:exit"(program) {
1755
+ for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) immut.facts.sinks.push({
1654
1756
  kind: "hook-return",
1655
1757
  expression
1656
1758
  });
1657
1759
  const reportedMutations = /* @__PURE__ */ new Set();
1658
- const mutableFunctions = inferMutableFunctions(context, collector.facts.mutations);
1760
+ const mutableFunctions = inferMutableFunctions(context, immut.facts.mutations);
1659
1761
  if (mutableFunctions.size > 0) {
1660
1762
  const reportedSinks = /* @__PURE__ */ new Set();
1661
- for (const sink of collector.facts.sinks) {
1763
+ for (const sink of immut.facts.sinks) {
1662
1764
  const expression = sink.expression;
1663
1765
  if (reportedSinks.has(expression)) continue;
1664
1766
  const fn = resolveToFunctionNode(context, expression);
@@ -1679,7 +1781,8 @@ function create$48(context) {
1679
1781
  });
1680
1782
  }
1681
1783
  }
1682
- for (const mutation of inferDirectMutations(context, collector.facts.mutations)) {
1784
+ const funcs = comps.api.getAllComponents(program).map((comp) => comp.node);
1785
+ for (const mutation of inferDirectMutations(context, immut.facts.mutations, funcs)) {
1683
1786
  if (reportedMutations.has(mutation.node)) continue;
1684
1787
  reportedMutations.add(mutation.node);
1685
1788
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.19.0",
3
+ "version": "5.20.0",
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,17 +36,17 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@eslint-react/ast": "5.19.0",
40
- "@eslint-react/core": "5.19.0",
41
- "@eslint-react/eslint": "5.19.0",
42
- "@eslint-react/jsx": "5.19.0",
43
- "@eslint-react/shared": "5.19.0",
44
- "@eslint-react/var": "5.19.0",
45
- "@typescript-eslint/scope-manager": "^8.69.0",
46
- "@typescript-eslint/type-utils": "^8.69.0",
47
- "@typescript-eslint/types": "^8.69.0",
48
- "@typescript-eslint/typescript-estree": "^8.69.0",
49
- "@typescript-eslint/utils": "^8.69.0",
39
+ "@eslint-react/ast": "5.20.0",
40
+ "@eslint-react/core": "5.20.0",
41
+ "@eslint-react/eslint": "5.20.0",
42
+ "@eslint-react/jsx": "5.20.0",
43
+ "@eslint-react/shared": "5.20.0",
44
+ "@eslint-react/var": "5.20.0",
45
+ "@typescript-eslint/scope-manager": "^8.70.0",
46
+ "@typescript-eslint/type-utils": "^8.70.0",
47
+ "@typescript-eslint/types": "^8.70.0",
48
+ "@typescript-eslint/typescript-estree": "^8.70.0",
49
+ "@typescript-eslint/utils": "^8.70.0",
50
50
  "compare-versions": "^6.1.1",
51
51
  "string-ts": "^2.3.1",
52
52
  "ts-api-utils": "^2.5.0",
@@ -55,15 +55,15 @@
55
55
  "devDependencies": {
56
56
  "@local/configs": "0.0.0",
57
57
  "@local/eff": "0.0.0",
58
- "@types/react": "^19.2.18",
59
- "@types/react-dom": "^19.2.7",
58
+ "@types/react": "^19.3.0",
59
+ "@types/react-dom": "^19.3.0",
60
60
  "dedent": "^1.7.2",
61
61
  "eslint": "^10.10.0",
62
- "react": "^19.2.8",
63
- "react-dom": "^19.2.8",
62
+ "react": "^19.3.0",
63
+ "react-dom": "^19.3.0",
64
64
  "tsdown": "^0.23.0",
65
65
  "tsl": "^1.0.30",
66
- "tsl-dx": "^0.13.3",
66
+ "tsl-dx": "^0.13.5",
67
67
  "typescript": "6.0.3"
68
68
  },
69
69
  "peerDependencies": {