eslint-plugin-react-x 5.14.2 → 5.14.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 +181 -170
  2. package/package.json +10 -10
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.6";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -1344,81 +1344,176 @@ const MUTATING_METHODS = /* @__PURE__ */ new Set([
1344
1344
  "splice",
1345
1345
  "unshift"
1346
1346
  ]);
1347
- /**
1348
- * Check if `inner` is fully contained within `outer`'s source range.
1349
- * @param inner The node that may be contained.
1350
- * @param outer The node that may contain it.
1351
- */
1352
- function isNodeWithin(inner, outer) {
1353
- return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
1354
- }
1355
- /**
1356
- * Resolve an expression to the function it ultimately refers to, following
1357
- * simple local aliasing (`const fn2 = fn;`) via scope resolution.
1358
- * @param context The ESLint rule context.
1359
- * @param node The expression to resolve.
1360
- * @param seen Identifiers already visited, to guard against cycles.
1361
- */
1347
+ const isUseRouterCall = core.isAPICall("useRouter");
1362
1348
  function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set()) {
1363
1349
  const expr = Extract.unwrap(node);
1364
1350
  if (Check.isFunction(expr)) return expr;
1365
- if (expr.type !== AST_NODE_TYPES.Identifier) return null;
1366
- if (seen.has(expr)) return null;
1351
+ if (expr.type !== AST_NODE_TYPES.Identifier || seen.has(expr)) return null;
1367
1352
  seen.add(expr);
1368
1353
  const resolved = resolve(context, expr);
1369
- if (resolved == null) return null;
1370
- return resolveToFunctionNode(context, resolved, seen);
1354
+ return resolved == null ? null : resolveToFunctionNode(context, resolved, seen);
1355
+ }
1356
+ function resolveVariableOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
1357
+ if (seen.has(variable)) return variable;
1358
+ seen.add(variable);
1359
+ const def = variable.defs.length === 1 ? variable.defs[0] : null;
1360
+ if (def?.type !== DefinitionType.Variable || def.node.init == null) return variable;
1361
+ const init = Extract.unwrap(def.node.init);
1362
+ if (init.type !== AST_NODE_TYPES.Identifier) return variable;
1363
+ const source = findVariable(context.sourceCode.getScope(init), init);
1364
+ return source == null ? variable : resolveVariableOrigin(context, source, seen);
1371
1365
  }
1372
- /**
1373
- * Check if a name is ref-like ("ref" or ends with "Ref").
1374
- * Refs are mutable by design and exempted from immutability checks.
1375
- * @param name The identifier name to check.
1376
- */
1377
1366
  function isRefLikeName$1(name) {
1378
1367
  return name === "ref" || name.endsWith("Ref");
1379
1368
  }
1380
- /**
1381
- * Check if any identifier or property name in a member-expression chain
1382
- * is ref-like.
1383
- * @param node The AST node to inspect.
1384
- */
1385
1369
  function hasRefLikeNameInChain(node) {
1386
1370
  if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(node.name);
1387
- if (node.type === AST_NODE_TYPES.MemberExpression) {
1388
- const propName = Extract.getPropertyName(node.property);
1389
- if (propName != null && isRefLikeName$1(propName)) return true;
1390
- return hasRefLikeNameInChain(node.object);
1391
- }
1392
- return false;
1371
+ if (node.type !== AST_NODE_TYPES.MemberExpression) return false;
1372
+ const propertyName = Extract.getPropertyName(node.property);
1373
+ return propertyName != null ? isRefLikeName$1(propertyName) || hasRefLikeNameInChain(node.object) : hasRefLikeNameInChain(node.object);
1374
+ }
1375
+ function isInitializedFromCall(context, node, isCall) {
1376
+ const root = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
1377
+ if (root == null) return false;
1378
+ const variable = findVariable(context.sourceCode.getScope(root), root);
1379
+ if (variable == null) return false;
1380
+ const origin = resolveVariableOrigin(context, variable);
1381
+ const def = origin.defs.length === 1 ? origin.defs[0] : null;
1382
+ if (def?.type !== DefinitionType.Variable || def.node.init == null) return false;
1383
+ const init = Extract.unwrap(def.node.init);
1384
+ return init.type === AST_NODE_TYPES.CallExpression && isCall(init);
1393
1385
  }
1394
- /**
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}.
1401
- * @param context The ESLint rule context.
1402
- * @param node The AST node to inspect (an identifier or member-expression chain).
1403
- */
1404
1386
  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);
1387
+ return isInitializedFromCall(context, node, (init) => core.isUseRefCall(context, init));
1388
+ }
1389
+ function isInitializedFromUseRouter(context, node) {
1390
+ return isInitializedFromCall(context, node, (init) => isUseRouterCall(context, init));
1391
+ }
1392
+ function isKnownNonMutatingMethodCall(context, node) {
1393
+ const callee = Extract.unwrap(node.callee);
1394
+ return Check.isExpression(callee) && isInitializedFromUseRouter(context, callee);
1409
1395
  }
1410
- /**
1411
- * Check if a mutated expression chain should be exempt from immutability
1412
- * checks because it is rooted at a ref: either by naming convention
1413
- * ({@link hasRefLikeNameInChain}) or because it is initialized from a
1414
- * `useRef()` call ({@link isInitializedFromUseRef}).
1415
- * @param context The ESLint rule context.
1416
- * @param node The AST node to inspect (an identifier or member-expression chain).
1417
- */
1418
1396
  function isRefLikeChain(context, node) {
1419
1397
  return hasRefLikeNameInChain(node) || isInitializedFromUseRef(context, node);
1420
1398
  }
1421
1399
 
1400
+ //#endregion
1401
+ //#region src/rules/immutability/collect.ts
1402
+ function createImmutabilityCollector() {
1403
+ const facts = {
1404
+ mutations: [],
1405
+ sinks: []
1406
+ };
1407
+ function pushMutation(kind, node, target, root) {
1408
+ const enclosingFunction = Traverse.findParent(node, Check.isFunction);
1409
+ if (enclosingFunction == null) return;
1410
+ facts.mutations.push({
1411
+ kind,
1412
+ enclosingFunction,
1413
+ node,
1414
+ root,
1415
+ target
1416
+ });
1417
+ }
1418
+ return {
1419
+ facts,
1420
+ visitor: {
1421
+ AssignmentExpression(node) {
1422
+ const target = Extract.unwrap(node.left);
1423
+ switch (target.type) {
1424
+ case AST_NODE_TYPES.Identifier:
1425
+ pushMutation("binding", node, target, target);
1426
+ return;
1427
+ case AST_NODE_TYPES.MemberExpression: {
1428
+ const root = Extract.getRootIdentifier(target);
1429
+ if (root != null) pushMutation("value", node, target, root);
1430
+ return;
1431
+ }
1432
+ }
1433
+ },
1434
+ CallExpression(node) {
1435
+ const callee = Extract.unwrap(node.callee);
1436
+ if (callee.type === AST_NODE_TYPES.MemberExpression) {
1437
+ const property = Extract.getPropertyName(callee.property);
1438
+ if (property != null && MUTATING_METHODS.has(property)) {
1439
+ const root = Extract.getRootIdentifier(callee.object);
1440
+ if (root != null) pushMutation("value", node, callee.object, root);
1441
+ }
1442
+ }
1443
+ if (!core.isHookCall(node)) return;
1444
+ for (const argument of node.arguments) {
1445
+ if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
1446
+ facts.sinks.push({
1447
+ kind: "hook-argument",
1448
+ expression: argument
1449
+ });
1450
+ }
1451
+ },
1452
+ JSXAttribute(node) {
1453
+ if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1454
+ const expression = node.value.expression;
1455
+ if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return;
1456
+ facts.sinks.push({
1457
+ kind: "jsx-prop",
1458
+ expression
1459
+ });
1460
+ },
1461
+ UnaryExpression(node) {
1462
+ if (node.operator !== "delete") return;
1463
+ const target = Extract.unwrap(node.argument);
1464
+ if (target.type !== AST_NODE_TYPES.MemberExpression) return;
1465
+ const root = Extract.getRootIdentifier(target);
1466
+ if (root != null) pushMutation("value", node, target, root);
1467
+ },
1468
+ UpdateExpression(node) {
1469
+ const target = Extract.unwrap(node.argument);
1470
+ switch (target.type) {
1471
+ case AST_NODE_TYPES.Identifier:
1472
+ pushMutation("binding", node, target, target);
1473
+ return;
1474
+ case AST_NODE_TYPES.MemberExpression: {
1475
+ const root = Extract.getRootIdentifier(target);
1476
+ if (root != null) pushMutation("value", node, target, root);
1477
+ return;
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ };
1483
+ }
1484
+
1485
+ //#endregion
1486
+ //#region src/rules/immutability/effects.ts
1487
+ function isGlobalOrModuleVariable(variable) {
1488
+ return variable.defs.length === 0 || variable.scope.type === ScopeType.global || variable.scope.type === ScopeType.module;
1489
+ }
1490
+ function isRefMutation(context, mutation) {
1491
+ if (mutation.target.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(mutation.target.name);
1492
+ return isRefLikeChain(context, mutation.target);
1493
+ }
1494
+ function inferMutableFunctions(context, mutations) {
1495
+ const mutableFunctions = /* @__PURE__ */ new Map();
1496
+ for (const mutation of mutations) {
1497
+ if (mutation.node.type === AST_NODE_TYPES.CallExpression && isKnownNonMutatingMethodCall(context, mutation.node)) continue;
1498
+ if (isRefMutation(context, mutation)) continue;
1499
+ const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
1500
+ if (variable == null) continue;
1501
+ const origin = mutation.kind === "binding" ? variable : resolveVariableOrigin(context, variable);
1502
+ if (isGlobalOrModuleVariable(origin)) continue;
1503
+ const declaration = origin.identifiers.at(0) ?? null;
1504
+ let current = mutation.enclosingFunction;
1505
+ while (current != null) {
1506
+ if (declaration != null && Traverse.findParent(declaration, Check.isFunction) === current) break;
1507
+ if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
1508
+ name: origin.name,
1509
+ node: mutation.node
1510
+ });
1511
+ current = Traverse.findParent(current, Check.isFunction);
1512
+ }
1513
+ }
1514
+ return mutableFunctions;
1515
+ }
1516
+
1422
1517
  //#endregion
1423
1518
  //#region src/rules/immutability/immutability.ts
1424
1519
  const RULE_NAME$48 = "immutability";
@@ -1437,120 +1532,36 @@ var immutability_default = createRule({
1437
1532
  defaultOptions: []
1438
1533
  });
1439
1534
  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
1535
+ const hooks = core.getHookCollector(context);
1536
+ const collector = createImmutabilityCollector();
1537
+ return merge(hooks.visitor, collector.visitor, { "Program:exit"(program) {
1538
+ for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) collector.facts.sinks.push({
1539
+ kind: "hook-return",
1540
+ expression
1450
1541
  });
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
- }
1542
+ const mutableFunctions = inferMutableFunctions(context, collector.facts.mutations);
1543
+ if (mutableFunctions.size === 0) return;
1544
+ const reported = /* @__PURE__ */ new Set();
1545
+ for (const sink of collector.facts.sinks) {
1546
+ const expression = sink.expression;
1547
+ if (reported.has(expression)) continue;
1548
+ const fn = resolveToFunctionNode(context, expression);
1549
+ if (fn == null) continue;
1550
+ const mutation = mutableFunctions.get(fn);
1551
+ if (mutation == null) continue;
1552
+ reported.add(expression);
1553
+ context.report({
1554
+ data: { name: mutation.name },
1555
+ messageId: "default",
1556
+ node: expression
1557
+ });
1558
+ context.report({
1559
+ data: { name: mutation.name },
1560
+ messageId: "mutates",
1561
+ node: mutation.node
1562
+ });
1552
1563
  }
1553
- });
1564
+ } });
1554
1565
  }
1555
1566
 
1556
1567
  //#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.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",
@@ -45,26 +45,26 @@
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/ast": "5.14.6",
49
+ "@eslint-react/core": "5.14.6",
50
+ "@eslint-react/jsx": "5.14.6",
51
+ "@eslint-react/eslint": "5.14.6",
52
+ "@eslint-react/shared": "5.14.6",
53
+ "@eslint-react/var": "5.14.6"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",
57
57
  "@types/react-dom": "^19.2.3",
58
58
  "dedent": "^1.7.2",
59
- "eslint": "^10.6.0",
59
+ "eslint": "^10.7.0",
60
60
  "react": "^19.2.7",
61
61
  "react-dom": "^19.2.7",
62
62
  "tsdown": "^0.22.4",
63
63
  "tsl": "^1.0.30",
64
64
  "tsl-dx": "^0.13.2",
65
65
  "typescript": "6.0.3",
66
- "@local/eff": "0.0.0",
67
- "@local/configs": "0.0.0"
66
+ "@local/configs": "0.0.0",
67
+ "@local/eff": "0.0.0"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "eslint": "*",