eslint-plugin-react-x 5.14.2 → 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 +181 -123
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -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.2";
147
+ var version = "5.14.5";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -1370,6 +1370,24 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
1370
1370
  return resolveToFunctionNode(context, resolved, seen);
1371
1371
  }
1372
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
+ /**
1373
1391
  * Check if a name is ref-like ("ref" or ends with "Ref").
1374
1392
  * Refs are mutable by design and exempted from immutability checks.
1375
1393
  * @param name The identifier name to check.
@@ -1392,20 +1410,28 @@ function hasRefLikeNameInChain(node) {
1392
1410
  return false;
1393
1411
  }
1394
1412
  /**
1395
- * Check if the root identifier of a member-expression chain (or the
1396
- * identifier itself) is initialized directly from a `useRef()` call, e.g.
1397
- * `const mounted = useRef(false); mounted.current = true;`.
1398
- *
1399
- * This catches refs regardless of naming convention, complementing
1400
- * {@link hasRefLikeNameInChain}.
1413
+ * Check if the root identifier of a member-expression chain is initialized
1414
+ * from a `useRef()` call, following variable-declarator aliases.
1401
1415
  * @param context The ESLint rule context.
1402
1416
  * @param node The AST node to inspect (an identifier or member-expression chain).
1403
1417
  */
1404
1418
  function isInitializedFromUseRef(context, node) {
1405
- const rootId = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
1406
- if (rootId == null) return false;
1407
- const initNode = resolve(context, rootId);
1408
- 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);
1409
1435
  }
1410
1436
  /**
1411
1437
  * Check if a mutated expression chain should be exempt from immutability
@@ -1419,6 +1445,122 @@ function isRefLikeChain(context, node) {
1419
1445
  return hasRefLikeNameInChain(node) || isInitializedFromUseRef(context, node);
1420
1446
  }
1421
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
+
1422
1564
  //#endregion
1423
1565
  //#region src/rules/immutability/immutability.ts
1424
1566
  const RULE_NAME$48 = "immutability";
@@ -1437,120 +1579,36 @@ var immutability_default = createRule({
1437
1579
  defaultOptions: []
1438
1580
  });
1439
1581
  function create$48(context) {
1440
- const hc = core.getHookCollector(context);
1441
- const mutationSites = [];
1442
- const sinkCandidates = [];
1443
- function pushMutationSite(node, root) {
1444
- const enclosing = Traverse.findParent(node, Check.isFunction);
1445
- if (enclosing == null) return;
1446
- mutationSites.push({
1447
- enclosing,
1448
- node,
1449
- 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
1450
1588
  });
1451
- }
1452
- return merge(hc.visitor, {
1453
- AssignmentExpression(node) {
1454
- const left = Extract.unwrap(node.left);
1455
- switch (left.type) {
1456
- case AST_NODE_TYPES.Identifier:
1457
- if (isRefLikeName$1(left.name)) return;
1458
- pushMutationSite(node, left);
1459
- return;
1460
- case AST_NODE_TYPES.MemberExpression: {
1461
- if (isRefLikeChain(context, left)) return;
1462
- const rootId = Extract.getRootIdentifier(left);
1463
- if (rootId == null) return;
1464
- pushMutationSite(node, rootId);
1465
- return;
1466
- }
1467
- }
1468
- },
1469
- CallExpression(node) {
1470
- const callee = Extract.unwrap(node.callee);
1471
- if (callee.type === AST_NODE_TYPES.MemberExpression) {
1472
- const propName = Extract.getPropertyName(callee.property);
1473
- if (propName != null && MUTATING_METHODS.has(propName) && !isRefLikeChain(context, callee.object)) {
1474
- const rootId = Extract.getRootIdentifier(callee.object);
1475
- if (rootId != null) pushMutationSite(node, rootId);
1476
- }
1477
- }
1478
- if (core.isHookCall(node)) for (const arg of node.arguments) {
1479
- if (arg.type === AST_NODE_TYPES.SpreadElement) continue;
1480
- sinkCandidates.push(arg);
1481
- }
1482
- },
1483
- JSXAttribute(node) {
1484
- if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1485
- const expr = node.value.expression;
1486
- if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return;
1487
- sinkCandidates.push(expr);
1488
- },
1489
- "Program:exit"(program) {
1490
- const mutableFunctions = /* @__PURE__ */ new Map();
1491
- for (const { enclosing, node, root } of mutationSites) {
1492
- const variable = findVariable(context.sourceCode.getScope(root), root);
1493
- if (variable == null) continue;
1494
- const declId = variable.identifiers.at(0) ?? null;
1495
- let current = enclosing;
1496
- while (current != null) {
1497
- if (declId != null && isNodeWithin(declId, current)) break;
1498
- if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
1499
- name: variable.name,
1500
- node
1501
- });
1502
- current = Traverse.findParent(current, Check.isFunction);
1503
- }
1504
- }
1505
- if (mutableFunctions.size === 0) return;
1506
- const reported = /* @__PURE__ */ new Set();
1507
- function checkSink(expr) {
1508
- if (expr == null || reported.has(expr)) return;
1509
- const fn = resolveToFunctionNode(context, expr);
1510
- if (fn == null) return;
1511
- const mutation = mutableFunctions.get(fn);
1512
- if (mutation == null) return;
1513
- reported.add(expr);
1514
- context.report({
1515
- data: { name: mutation.name },
1516
- messageId: "default",
1517
- node: expr
1518
- });
1519
- context.report({
1520
- data: { name: mutation.name },
1521
- messageId: "mutates",
1522
- node: mutation.node
1523
- });
1524
- }
1525
- for (const candidate of sinkCandidates) checkSink(candidate);
1526
- for (const hook of hc.api.getAllHooks(program)) for (const ret of hook.rets) checkSink(ret);
1527
- },
1528
- UnaryExpression(node) {
1529
- if (node.operator !== "delete") return;
1530
- const arg = Extract.unwrap(node.argument);
1531
- if (arg.type !== AST_NODE_TYPES.MemberExpression) return;
1532
- if (isRefLikeChain(context, arg)) return;
1533
- const rootId = Extract.getRootIdentifier(arg);
1534
- if (rootId == null) return;
1535
- pushMutationSite(node, rootId);
1536
- },
1537
- UpdateExpression(node) {
1538
- const arg = Extract.unwrap(node.argument);
1539
- switch (arg.type) {
1540
- case AST_NODE_TYPES.Identifier:
1541
- if (isRefLikeName$1(arg.name)) return;
1542
- pushMutationSite(node, arg);
1543
- return;
1544
- case AST_NODE_TYPES.MemberExpression: {
1545
- if (isRefLikeChain(context, arg)) return;
1546
- const rootId = Extract.getRootIdentifier(arg);
1547
- if (rootId == null) return;
1548
- pushMutationSite(node, rootId);
1549
- return;
1550
- }
1551
- }
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
+ });
1552
1610
  }
1553
- });
1611
+ } });
1554
1612
  }
1555
1613
 
1556
1614
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.14.2",
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.2",
49
- "@eslint-react/core": "5.14.2",
50
- "@eslint-react/shared": "5.14.2",
51
- "@eslint-react/eslint": "5.14.2",
52
- "@eslint-react/jsx": "5.14.2",
53
- "@eslint-react/var": "5.14.2"
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",