eslint-plugin-react-x 5.14.1 → 5.14.5

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 +303 -202
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import * as core from "@eslint-react/core";
5
5
  import { merge } from "@eslint-react/eslint";
6
6
  import { JsxDetectionHint, findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
7
7
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
8
+ import { computeObjectType, isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget } 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
- import { computeObjectType, isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
11
11
  import { P, isMatching, match } from "ts-pattern";
12
12
  import { compare } from "compare-versions";
13
13
  import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
@@ -144,7 +144,7 @@ const rules$6 = {
144
144
  //#endregion
145
145
  //#region package.json
146
146
  var name$6 = "eslint-plugin-react-x";
147
- var version = "5.14.1";
147
+ var version = "5.14.5";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -1157,6 +1157,67 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
1157
1157
  "splice",
1158
1158
  "unshift"
1159
1159
  ]);
1160
+ /**
1161
+ * Return whether an identifier is an unresolved global or is declared in the
1162
+ * global/module scope.
1163
+ */
1164
+ function isGlobalVariable(context, node) {
1165
+ const variable = findVariable(context.sourceCode.getScope(node), node);
1166
+ if (variable == null || variable.defs.length === 0) return true;
1167
+ return variable.scope.type === ScopeType.global || variable.scope.type === ScopeType.module;
1168
+ }
1169
+ /**
1170
+ * Resolve an object expression to the global/module binding it aliases.
1171
+ *
1172
+ * This intentionally follows only stable `const alias = value` declarations.
1173
+ * It gives the rule useful Alias effects without pretending to perform full
1174
+ * control-flow analysis for reassigned locals.
1175
+ */
1176
+ function resolveGlobalOrigin(context, node, seen = /* @__PURE__ */ new Set()) {
1177
+ const expression = Extract.unwrap(node);
1178
+ if (seen.has(expression)) return null;
1179
+ seen.add(expression);
1180
+ if (expression.type === AST_NODE_TYPES.MemberExpression) return resolveGlobalOrigin(context, expression.object, seen);
1181
+ if (expression.type !== AST_NODE_TYPES.Identifier) return null;
1182
+ if (isGlobalVariable(context, expression)) return expression;
1183
+ const variable = findVariable(context.sourceCode.getScope(expression), expression);
1184
+ const definition = variable?.defs.length === 1 ? variable.defs[0] : null;
1185
+ if (definition?.type !== DefinitionType.Variable) return null;
1186
+ if (definition.node.id.type !== AST_NODE_TYPES.Identifier || definition.node.init == null) return null;
1187
+ if (definition.node.parent.kind !== "const") return null;
1188
+ const initializer = Extract.unwrap(definition.node.init);
1189
+ if (initializer.type !== AST_NODE_TYPES.Identifier && initializer.type !== AST_NODE_TYPES.MemberExpression) return null;
1190
+ return resolveGlobalOrigin(context, initializer, seen);
1191
+ }
1192
+ /**
1193
+ * Collect every write target in an assignment, including destructuring
1194
+ * patterns such as `[local, globalValue] = source`.
1195
+ */
1196
+ function getAssignmentTargets(node) {
1197
+ const target = Extract.unwrap(node);
1198
+ switch (target.type) {
1199
+ case AST_NODE_TYPES.Identifier:
1200
+ case AST_NODE_TYPES.MemberExpression: return [target];
1201
+ case AST_NODE_TYPES.ArrayPattern: return target.elements.flatMap((element) => element == null ? [] : getAssignmentTargets(element));
1202
+ case AST_NODE_TYPES.AssignmentPattern: return getAssignmentTargets(target.left);
1203
+ case AST_NODE_TYPES.ObjectPattern: return target.properties.flatMap((property) => {
1204
+ if (property.type === AST_NODE_TYPES.RestElement) return getAssignmentTargets(property.argument);
1205
+ return getAssignmentTargets(property.value);
1206
+ });
1207
+ case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
1208
+ default: return [];
1209
+ }
1210
+ }
1211
+ /** Resolve a direct call target, following simple function aliases. */
1212
+ function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
1213
+ const expression = Extract.unwrap(node);
1214
+ if (Check.isFunction(expression)) return expression;
1215
+ if (expression.type !== AST_NODE_TYPES.Identifier || seen.has(expression)) return null;
1216
+ seen.add(expression);
1217
+ const resolved = resolve(context, expression);
1218
+ if (resolved == null) return null;
1219
+ return resolveToFunction(context, resolved, seen);
1220
+ }
1160
1221
 
1161
1222
  //#endregion
1162
1223
  //#region src/rules/globals/globals.ts
@@ -1179,101 +1240,83 @@ var globals_default = createRule({
1179
1240
  function create$49(context) {
1180
1241
  const hc = core.getHookCollector(context);
1181
1242
  const fc = core.getFunctionComponentCollector(context);
1182
- const violations = [];
1183
- /**
1184
- * Return true when `id` refers to a variable defined in the global or module
1185
- * scope (i.e. outside any function), or when it has no known definition.
1186
- */
1187
- function isGlobalOrModuleVariable(id) {
1188
- const variable = findVariable(context.sourceCode.getScope(id), id);
1189
- if (variable == null) return true;
1190
- if (variable.defs.length === 0) return true;
1191
- const scopeType = variable.scope.type;
1192
- return scopeType === ScopeType.global || scopeType === ScopeType.module;
1193
- }
1194
- /**
1195
- * Return the nearest enclosing function for `node`.
1196
- */
1243
+ const directEffects = /* @__PURE__ */ new Map();
1244
+ const callGraph = /* @__PURE__ */ new Map();
1197
1245
  function getEnclosingFunction(node) {
1198
1246
  return Traverse.findParent(node, Check.isFunction);
1199
1247
  }
1200
- /**
1201
- * Record a violation that will be filtered at Program:exit.
1202
- */
1203
- function recordViolation(node, messageId, data) {
1204
- const func = getEnclosingFunction(node);
1205
- if (func == null) return;
1206
- violations.push({
1248
+ function recordEffect(node, messageId, data) {
1249
+ const enclosing = getEnclosingFunction(node);
1250
+ if (enclosing == null) return;
1251
+ const effects = directEffects.get(enclosing) ?? [];
1252
+ effects.push({
1207
1253
  data,
1208
- func,
1209
1254
  messageId,
1210
1255
  node
1211
1256
  });
1257
+ directEffects.set(enclosing, effects);
1258
+ }
1259
+ function recordWrite(node, target) {
1260
+ if (target.type === AST_NODE_TYPES.Identifier) {
1261
+ if (!isGlobalVariable(context, target)) return;
1262
+ recordEffect(node, "mutatingGlobal", { name: target.name });
1263
+ return;
1264
+ }
1265
+ if (resolveGlobalOrigin(context, target.object) == null) return;
1266
+ recordEffect(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(target) });
1267
+ }
1268
+ function recordCallEdge(node) {
1269
+ const caller = getEnclosingFunction(node);
1270
+ if (caller == null) return;
1271
+ const callee = resolveToFunction(context, node.callee);
1272
+ if (callee == null) return;
1273
+ const callees = callGraph.get(caller) ?? /* @__PURE__ */ new Set();
1274
+ callees.add(callee);
1275
+ callGraph.set(caller, callees);
1212
1276
  }
1213
1277
  return merge(hc.visitor, fc.visitor, {
1214
- /**
1215
- * Detect `renderCount++`, `renderCount--`
1216
- */
1217
- UpdateExpression(node) {
1218
- const arg = Extract.unwrap(node.argument);
1219
- if (arg.type === AST_NODE_TYPES.Identifier) {
1220
- if (!isGlobalOrModuleVariable(arg)) return;
1221
- recordViolation(node, "mutatingGlobal", { name: arg.name });
1222
- return;
1223
- }
1224
- if (arg.type === AST_NODE_TYPES.MemberExpression) {
1225
- const rootId = Extract.getRootIdentifier(arg);
1226
- if (rootId == null) return;
1227
- if (!isGlobalOrModuleVariable(rootId)) return;
1228
- recordViolation(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(arg) });
1229
- }
1230
- },
1231
- /**
1232
- * Detect `renderCount = 1`, `window.currentUser = id`, `cache[id] = value`
1233
- */
1234
1278
  AssignmentExpression(node) {
1235
- const left = Extract.unwrap(node.left);
1236
- if (left.type === AST_NODE_TYPES.Identifier) {
1237
- if (!isGlobalOrModuleVariable(left)) return;
1238
- recordViolation(node, "mutatingGlobal", { name: left.name });
1239
- return;
1240
- }
1241
- if (left.type === AST_NODE_TYPES.MemberExpression) {
1242
- const rootId = Extract.getRootIdentifier(left);
1243
- if (rootId == null) return;
1244
- if (!isGlobalOrModuleVariable(rootId)) return;
1245
- recordViolation(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(left) });
1246
- }
1279
+ for (const target of getAssignmentTargets(node.left)) recordWrite(node, target);
1247
1280
  },
1248
- /**
1249
- * Detect `events.push(event)`, `cache.sort()`, etc.
1250
- */
1251
1281
  CallExpression(node) {
1282
+ recordCallEdge(node);
1252
1283
  const callee = Extract.unwrap(node.callee);
1253
1284
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
1254
- const { object, property } = callee;
1255
- if (property.type !== AST_NODE_TYPES.Identifier) return;
1256
- if (!MUTATING_ARRAY_METHODS.has(property.name)) return;
1257
- const rootId = Extract.getRootIdentifier(object);
1258
- if (rootId == null) return;
1259
- if (!isGlobalOrModuleVariable(rootId)) return;
1260
- recordViolation(node, "mutatingGlobalArrayMethod", {
1261
- name: rootId.name,
1262
- method: property.name
1285
+ const method = Extract.getPropertyName(callee.property);
1286
+ if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
1287
+ const origin = resolveGlobalOrigin(context, callee.object);
1288
+ if (origin == null) return;
1289
+ recordEffect(node, "mutatingGlobalArrayMethod", {
1290
+ name: origin.name,
1291
+ method
1263
1292
  });
1264
1293
  },
1265
- "Program:exit"(node) {
1266
- const comps = fc.api.getAllComponents(node);
1267
- const hooks = hc.api.getAllHooks(node);
1268
- const funcs = [...comps, ...hooks];
1269
- for (const { data, func, messageId, node } of violations) {
1270
- if (!funcs.some((f) => f.node === func)) continue;
1271
- context.report({
1272
- data,
1273
- messageId,
1274
- node
1275
- });
1294
+ "Program:exit"(program) {
1295
+ const renderFunctions = [...fc.api.getAllComponents(program), ...hc.api.getAllHooks(program)];
1296
+ const visited = /* @__PURE__ */ new Set();
1297
+ const reported = /* @__PURE__ */ new Set();
1298
+ function applyFunctionEffects(func) {
1299
+ if (visited.has(func)) return;
1300
+ visited.add(func);
1301
+ for (const effect of directEffects.get(func) ?? []) {
1302
+ if (reported.has(effect)) continue;
1303
+ reported.add(effect);
1304
+ context.report(effect);
1305
+ }
1306
+ for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
1276
1307
  }
1308
+ for (const { node } of renderFunctions) applyFunctionEffects(node);
1309
+ },
1310
+ UnaryExpression(node) {
1311
+ if (node.operator !== "delete") return;
1312
+ const argument = Extract.unwrap(node.argument);
1313
+ if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
1314
+ recordWrite(node, argument);
1315
+ },
1316
+ UpdateExpression(node) {
1317
+ const argument = Extract.unwrap(node.argument);
1318
+ if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
1319
+ recordWrite(node, argument);
1277
1320
  }
1278
1321
  });
1279
1322
  }
@@ -1327,6 +1370,24 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
1327
1370
  return resolveToFunctionNode(context, resolved, seen);
1328
1371
  }
1329
1372
  /**
1373
+ * Follow identifier-only variable-declarator aliases to their originating
1374
+ * binding. Assignment aliases are intentionally left for the value-flow phase.
1375
+ * @param context The ESLint rule context.
1376
+ * @param variable The binding whose initializer aliases should be followed.
1377
+ * @param seen Bindings already visited, to guard against cycles.
1378
+ */
1379
+ function resolveVariableOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
1380
+ if (seen.has(variable)) return variable;
1381
+ seen.add(variable);
1382
+ const definition = variable.defs.length === 1 ? variable.defs[0] : null;
1383
+ if (definition?.type !== DefinitionType.Variable || definition.node.init == null) return variable;
1384
+ const initializer = Extract.unwrap(definition.node.init);
1385
+ if (initializer.type !== AST_NODE_TYPES.Identifier) return variable;
1386
+ const source = findVariable(context.sourceCode.getScope(initializer), initializer);
1387
+ if (source == null) return variable;
1388
+ return resolveVariableOrigin(context, source, seen);
1389
+ }
1390
+ /**
1330
1391
  * Check if a name is ref-like ("ref" or ends with "Ref").
1331
1392
  * Refs are mutable by design and exempted from immutability checks.
1332
1393
  * @param name The identifier name to check.
@@ -1349,20 +1410,28 @@ function hasRefLikeNameInChain(node) {
1349
1410
  return false;
1350
1411
  }
1351
1412
  /**
1352
- * Check if the root identifier of a member-expression chain (or the
1353
- * identifier itself) is initialized directly from a `useRef()` call, e.g.
1354
- * `const mounted = useRef(false); mounted.current = true;`.
1355
- *
1356
- * This catches refs regardless of naming convention, complementing
1357
- * {@link hasRefLikeNameInChain}.
1413
+ * Check if the root identifier of a member-expression chain is initialized
1414
+ * from a `useRef()` call, following variable-declarator aliases.
1358
1415
  * @param context The ESLint rule context.
1359
1416
  * @param node The AST node to inspect (an identifier or member-expression chain).
1360
1417
  */
1361
1418
  function isInitializedFromUseRef(context, node) {
1362
- const rootId = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
1363
- if (rootId == null) return false;
1364
- const initNode = resolve(context, rootId);
1365
- return initNode != null && initNode.type === AST_NODE_TYPES.CallExpression && core.isUseRefCall(context, initNode);
1419
+ const root = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
1420
+ if (root == null) return false;
1421
+ const variable = findVariable(context.sourceCode.getScope(root), root);
1422
+ if (variable == null) return false;
1423
+ return isVariableInitializedFromUseRef(context, variable, /* @__PURE__ */ new Set());
1424
+ }
1425
+ function isVariableInitializedFromUseRef(context, variable, seen) {
1426
+ if (seen.has(variable)) return false;
1427
+ seen.add(variable);
1428
+ const definition = variable.defs.length === 1 ? variable.defs[0] : null;
1429
+ if (definition?.type !== DefinitionType.Variable || definition.node.init == null) return false;
1430
+ const initializer = Extract.unwrap(definition.node.init);
1431
+ if (initializer.type === AST_NODE_TYPES.CallExpression) return core.isUseRefCall(context, initializer);
1432
+ if (initializer.type !== AST_NODE_TYPES.Identifier) return false;
1433
+ const source = findVariable(context.sourceCode.getScope(initializer), initializer);
1434
+ return source != null && isVariableInitializedFromUseRef(context, source, seen);
1366
1435
  }
1367
1436
  /**
1368
1437
  * Check if a mutated expression chain should be exempt from immutability
@@ -1376,6 +1445,122 @@ function isRefLikeChain(context, node) {
1376
1445
  return hasRefLikeNameInChain(node) || isInitializedFromUseRef(context, node);
1377
1446
  }
1378
1447
 
1448
+ //#endregion
1449
+ //#region src/rules/immutability/collect.ts
1450
+ function createImmutabilityCollector() {
1451
+ const facts = {
1452
+ mutations: [],
1453
+ sinks: []
1454
+ };
1455
+ function pushMutation(kind, node, target, root) {
1456
+ const enclosingFunction = Traverse.findParent(node, Check.isFunction);
1457
+ if (enclosingFunction == null) return;
1458
+ facts.mutations.push({
1459
+ kind,
1460
+ enclosingFunction,
1461
+ node,
1462
+ root,
1463
+ target
1464
+ });
1465
+ }
1466
+ return {
1467
+ facts,
1468
+ visitor: {
1469
+ AssignmentExpression(node) {
1470
+ const target = Extract.unwrap(node.left);
1471
+ switch (target.type) {
1472
+ case AST_NODE_TYPES.Identifier:
1473
+ pushMutation("binding", node, target, target);
1474
+ return;
1475
+ case AST_NODE_TYPES.MemberExpression: {
1476
+ const root = Extract.getRootIdentifier(target);
1477
+ if (root != null) pushMutation("value", node, target, root);
1478
+ return;
1479
+ }
1480
+ }
1481
+ },
1482
+ CallExpression(node) {
1483
+ const callee = Extract.unwrap(node.callee);
1484
+ if (callee.type === AST_NODE_TYPES.MemberExpression) {
1485
+ const property = Extract.getPropertyName(callee.property);
1486
+ if (property != null && MUTATING_METHODS.has(property)) {
1487
+ const root = Extract.getRootIdentifier(callee.object);
1488
+ if (root != null) pushMutation("value", node, callee.object, root);
1489
+ }
1490
+ }
1491
+ if (!core.isHookCall(node)) return;
1492
+ for (const argument of node.arguments) {
1493
+ if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
1494
+ facts.sinks.push({
1495
+ kind: "hook-argument",
1496
+ expression: argument
1497
+ });
1498
+ }
1499
+ },
1500
+ JSXAttribute(node) {
1501
+ if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1502
+ const expression = node.value.expression;
1503
+ if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return;
1504
+ facts.sinks.push({
1505
+ kind: "jsx-prop",
1506
+ expression
1507
+ });
1508
+ },
1509
+ UnaryExpression(node) {
1510
+ if (node.operator !== "delete") return;
1511
+ const target = Extract.unwrap(node.argument);
1512
+ if (target.type !== AST_NODE_TYPES.MemberExpression) return;
1513
+ const root = Extract.getRootIdentifier(target);
1514
+ if (root != null) pushMutation("value", node, target, root);
1515
+ },
1516
+ UpdateExpression(node) {
1517
+ const target = Extract.unwrap(node.argument);
1518
+ switch (target.type) {
1519
+ case AST_NODE_TYPES.Identifier:
1520
+ pushMutation("binding", node, target, target);
1521
+ return;
1522
+ case AST_NODE_TYPES.MemberExpression: {
1523
+ const root = Extract.getRootIdentifier(target);
1524
+ if (root != null) pushMutation("value", node, target, root);
1525
+ return;
1526
+ }
1527
+ }
1528
+ }
1529
+ }
1530
+ };
1531
+ }
1532
+
1533
+ //#endregion
1534
+ //#region src/rules/immutability/effects.ts
1535
+ function isGlobalOrModuleVariable(variable) {
1536
+ return variable.defs.length === 0 || variable.scope.type === ScopeType.global || variable.scope.type === ScopeType.module;
1537
+ }
1538
+ function isRefMutation(context, mutation) {
1539
+ if (mutation.target.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(mutation.target.name);
1540
+ return isRefLikeChain(context, mutation.target);
1541
+ }
1542
+ function inferMutableFunctions(context, mutations) {
1543
+ const mutableFunctions = /* @__PURE__ */ new Map();
1544
+ for (const mutation of mutations) {
1545
+ if (isRefMutation(context, mutation)) continue;
1546
+ const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
1547
+ if (variable == null) continue;
1548
+ const origin = mutation.kind === "binding" ? variable : resolveVariableOrigin(context, variable);
1549
+ if (isGlobalOrModuleVariable(origin)) continue;
1550
+ const declaration = origin.identifiers.at(0) ?? null;
1551
+ let current = mutation.enclosingFunction;
1552
+ while (current != null) {
1553
+ if (declaration != null && isNodeWithin(declaration, current)) break;
1554
+ if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
1555
+ name: origin.name,
1556
+ node: mutation.node
1557
+ });
1558
+ current = Traverse.findParent(current, Check.isFunction);
1559
+ }
1560
+ }
1561
+ return mutableFunctions;
1562
+ }
1563
+
1379
1564
  //#endregion
1380
1565
  //#region src/rules/immutability/immutability.ts
1381
1566
  const RULE_NAME$48 = "immutability";
@@ -1394,120 +1579,36 @@ var immutability_default = createRule({
1394
1579
  defaultOptions: []
1395
1580
  });
1396
1581
  function create$48(context) {
1397
- const hc = core.getHookCollector(context);
1398
- const mutationSites = [];
1399
- const sinkCandidates = [];
1400
- function pushMutationSite(node, root) {
1401
- const enclosing = Traverse.findParent(node, Check.isFunction);
1402
- if (enclosing == null) return;
1403
- mutationSites.push({
1404
- enclosing,
1405
- node,
1406
- root
1582
+ const hooks = core.getHookCollector(context);
1583
+ const collector = createImmutabilityCollector();
1584
+ return merge(hooks.visitor, collector.visitor, { "Program:exit"(program) {
1585
+ for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) collector.facts.sinks.push({
1586
+ kind: "hook-return",
1587
+ expression
1407
1588
  });
1408
- }
1409
- return merge(hc.visitor, {
1410
- AssignmentExpression(node) {
1411
- const left = Extract.unwrap(node.left);
1412
- switch (left.type) {
1413
- case AST_NODE_TYPES.Identifier:
1414
- if (isRefLikeName$1(left.name)) return;
1415
- pushMutationSite(node, left);
1416
- return;
1417
- case AST_NODE_TYPES.MemberExpression: {
1418
- if (isRefLikeChain(context, left)) return;
1419
- const rootId = Extract.getRootIdentifier(left);
1420
- if (rootId == null) return;
1421
- pushMutationSite(node, rootId);
1422
- return;
1423
- }
1424
- }
1425
- },
1426
- CallExpression(node) {
1427
- const callee = Extract.unwrap(node.callee);
1428
- if (callee.type === AST_NODE_TYPES.MemberExpression) {
1429
- const propName = Extract.getPropertyName(callee.property);
1430
- if (propName != null && MUTATING_METHODS.has(propName) && !isRefLikeChain(context, callee.object)) {
1431
- const rootId = Extract.getRootIdentifier(callee.object);
1432
- if (rootId != null) pushMutationSite(node, rootId);
1433
- }
1434
- }
1435
- if (core.isHookCall(node)) for (const arg of node.arguments) {
1436
- if (arg.type === AST_NODE_TYPES.SpreadElement) continue;
1437
- sinkCandidates.push(arg);
1438
- }
1439
- },
1440
- JSXAttribute(node) {
1441
- if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1442
- const expr = node.value.expression;
1443
- if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return;
1444
- sinkCandidates.push(expr);
1445
- },
1446
- "Program:exit"(program) {
1447
- const mutableFunctions = /* @__PURE__ */ new Map();
1448
- for (const { enclosing, node, root } of mutationSites) {
1449
- const variable = findVariable(context.sourceCode.getScope(root), root);
1450
- if (variable == null) continue;
1451
- const declId = variable.identifiers.at(0) ?? null;
1452
- let current = enclosing;
1453
- while (current != null) {
1454
- if (declId != null && isNodeWithin(declId, current)) break;
1455
- if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
1456
- name: variable.name,
1457
- node
1458
- });
1459
- current = Traverse.findParent(current, Check.isFunction);
1460
- }
1461
- }
1462
- if (mutableFunctions.size === 0) return;
1463
- const reported = /* @__PURE__ */ new Set();
1464
- function checkSink(expr) {
1465
- if (expr == null || reported.has(expr)) return;
1466
- const fn = resolveToFunctionNode(context, expr);
1467
- if (fn == null) return;
1468
- const mutation = mutableFunctions.get(fn);
1469
- if (mutation == null) return;
1470
- reported.add(expr);
1471
- context.report({
1472
- data: { name: mutation.name },
1473
- messageId: "default",
1474
- node: expr
1475
- });
1476
- context.report({
1477
- data: { name: mutation.name },
1478
- messageId: "mutates",
1479
- node: mutation.node
1480
- });
1481
- }
1482
- for (const candidate of sinkCandidates) checkSink(candidate);
1483
- for (const hook of hc.api.getAllHooks(program)) for (const ret of hook.rets) checkSink(ret);
1484
- },
1485
- UnaryExpression(node) {
1486
- if (node.operator !== "delete") return;
1487
- const arg = Extract.unwrap(node.argument);
1488
- if (arg.type !== AST_NODE_TYPES.MemberExpression) return;
1489
- if (isRefLikeChain(context, arg)) return;
1490
- const rootId = Extract.getRootIdentifier(arg);
1491
- if (rootId == null) return;
1492
- pushMutationSite(node, rootId);
1493
- },
1494
- UpdateExpression(node) {
1495
- const arg = Extract.unwrap(node.argument);
1496
- switch (arg.type) {
1497
- case AST_NODE_TYPES.Identifier:
1498
- if (isRefLikeName$1(arg.name)) return;
1499
- pushMutationSite(node, arg);
1500
- return;
1501
- case AST_NODE_TYPES.MemberExpression: {
1502
- if (isRefLikeChain(context, arg)) return;
1503
- const rootId = Extract.getRootIdentifier(arg);
1504
- if (rootId == null) return;
1505
- pushMutationSite(node, rootId);
1506
- return;
1507
- }
1508
- }
1589
+ const mutableFunctions = inferMutableFunctions(context, collector.facts.mutations);
1590
+ if (mutableFunctions.size === 0) return;
1591
+ const reported = /* @__PURE__ */ new Set();
1592
+ for (const sink of collector.facts.sinks) {
1593
+ const expression = sink.expression;
1594
+ if (reported.has(expression)) continue;
1595
+ const fn = resolveToFunctionNode(context, expression);
1596
+ if (fn == null) continue;
1597
+ const mutation = mutableFunctions.get(fn);
1598
+ if (mutation == null) continue;
1599
+ reported.add(expression);
1600
+ context.report({
1601
+ data: { name: mutation.name },
1602
+ messageId: "default",
1603
+ node: expression
1604
+ });
1605
+ context.report({
1606
+ data: { name: mutation.name },
1607
+ messageId: "mutates",
1608
+ node: mutation.node
1609
+ });
1509
1610
  }
1510
- });
1611
+ } });
1511
1612
  }
1512
1613
 
1513
1614
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.14.1",
3
+ "version": "5.14.5",
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",
@@ -45,12 +45,12 @@
45
45
  "string-ts": "^2.3.1",
46
46
  "ts-api-utils": "^2.5.0",
47
47
  "ts-pattern": "^5.9.0",
48
- "@eslint-react/ast": "5.14.1",
49
- "@eslint-react/jsx": "5.14.1",
50
- "@eslint-react/core": "5.14.1",
51
- "@eslint-react/eslint": "5.14.1",
52
- "@eslint-react/shared": "5.14.1",
53
- "@eslint-react/var": "5.14.1"
48
+ "@eslint-react/eslint": "5.14.5",
49
+ "@eslint-react/ast": "5.14.5",
50
+ "@eslint-react/core": "5.14.5",
51
+ "@eslint-react/jsx": "5.14.5",
52
+ "@eslint-react/shared": "5.14.5",
53
+ "@eslint-react/var": "5.14.5"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",