eslint-plugin-react-x 5.7.2 → 5.7.3

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 +57 -31
  2. package/package.json +12 -12
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ const rules$6 = {
143
143
  //#endregion
144
144
  //#region package.json
145
145
  var name$6 = "eslint-plugin-react-x";
146
- var version = "5.7.2";
146
+ var version = "5.7.3";
147
147
 
148
148
  //#endregion
149
149
  //#region src/utils/create-rule.ts
@@ -1279,6 +1279,46 @@ const MUTATING_ARRAY_METHODS = new Set([
1279
1279
  "splice",
1280
1280
  "unshift"
1281
1281
  ]);
1282
+ /**
1283
+ * Check if a name is ref-like ("ref" or ends with "Ref").
1284
+ * Refs are mutable by design and exempted from immutability checks.
1285
+ */
1286
+ function isRefLikeName(name) {
1287
+ return name === "ref" || name.endsWith("Ref");
1288
+ }
1289
+ /**
1290
+ * Check if any identifier or property name in a member-expression chain
1291
+ * is ref-like.
1292
+ */
1293
+ function hasRefLikeNameInChain(node) {
1294
+ if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName(node.name);
1295
+ if (node.type === AST_NODE_TYPES.MemberExpression) {
1296
+ const propName = Extract.getPropertyName(node.property);
1297
+ if (propName != null && isRefLikeName(propName)) return true;
1298
+ return hasRefLikeNameInChain(node.object);
1299
+ }
1300
+ return false;
1301
+ }
1302
+ /**
1303
+ * Check if `name` appears anywhere inside a parameter pattern.
1304
+ * @param pattern - The parameter pattern to search in.
1305
+ * @param name - The identifier name to look for.
1306
+ */
1307
+ function identifierExistsInPattern(pattern, name) {
1308
+ switch (pattern.type) {
1309
+ case AST_NODE_TYPES.Identifier: return pattern.name === name;
1310
+ case AST_NODE_TYPES.ObjectPattern: return pattern.properties.some((p) => {
1311
+ if (p.type === AST_NODE_TYPES.Property) return identifierExistsInPattern(p.value, name);
1312
+ if (p.type === AST_NODE_TYPES.RestElement) return identifierExistsInPattern(p.argument, name);
1313
+ return false;
1314
+ });
1315
+ case AST_NODE_TYPES.ArrayPattern: return pattern.elements.some((el) => el != null && identifierExistsInPattern(el, name));
1316
+ case AST_NODE_TYPES.RestElement: return identifierExistsInPattern(pattern.argument, name);
1317
+ case AST_NODE_TYPES.AssignmentPattern: return identifierExistsInPattern(pattern.left, name);
1318
+ case AST_NODE_TYPES.MemberExpression: return Extract.getRootIdentifier(pattern)?.name === name;
1319
+ default: return false;
1320
+ }
1321
+ }
1282
1322
 
1283
1323
  //#endregion
1284
1324
  //#region src/rules/immutability/immutability.ts
@@ -1289,7 +1329,8 @@ var immutability_default = createRule({
1289
1329
  docs: { description: "Validates against mutating props, state, and other values that are immutable." },
1290
1330
  messages: {
1291
1331
  mutatingArrayMethod: "Do not call '{{method}}()' on '{{name}}'. Props and state are immutable — create a new array instead.",
1292
- mutatingAssignment: "Do not mutate '{{name}}' directly. Props and state are immutable — create a new object instead."
1332
+ mutatingAssignment: "Do not mutate '{{name}}' directly. Props and state are immutable — create a new object instead.",
1333
+ noRefLikeStateName: "State variable '{{name}}' should not be named 'ref' or end with 'Ref'. Use a different name to avoid confusion with mutable refs."
1293
1334
  },
1294
1335
  schema: []
1295
1336
  },
@@ -1309,14 +1350,6 @@ function create$48(context) {
1309
1350
  function isUseStateCall(node) {
1310
1351
  return core.isUseStateLikeCall(node, additionalStateHooks);
1311
1352
  }
1312
- function isUseReducerCall(node) {
1313
- const callee = Extract.unwrap(node.callee);
1314
- if (callee.type === AST_NODE_TYPES.Identifier) return callee.name === "useReducer";
1315
- if (callee.type === AST_NODE_TYPES.MemberExpression) {
1316
- if (Extract.getRootIdentifier(callee)?.name === "React") return Extract.getPropertyName(callee.property) === "useReducer";
1317
- }
1318
- return false;
1319
- }
1320
1353
  /**
1321
1354
  * Return true when `id` is the *value* variable (index 0) produced by a
1322
1355
  * `useState(…)` or `useReducer(…)` call.
@@ -1326,32 +1359,12 @@ function create$48(context) {
1326
1359
  function isStateValue(id) {
1327
1360
  const initNode = resolve(context, id);
1328
1361
  if (initNode == null || initNode.type !== AST_NODE_TYPES.CallExpression) return false;
1329
- if (!isUseStateCall(initNode) && !isUseReducerCall(initNode)) return false;
1362
+ if (!isUseStateCall(initNode) && !core.isUseReducerCall(context, initNode)) return false;
1330
1363
  const declarator = initNode.parent;
1331
1364
  if (!("id" in declarator) || declarator.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
1332
1365
  return declarator.id.elements.findIndex((el) => el?.type === AST_NODE_TYPES.Identifier && el.name === id.name) === 0;
1333
1366
  }
1334
1367
  /**
1335
- * Check if `name` appears anywhere inside a parameter pattern.
1336
- * @param pattern - The parameter pattern to search in.
1337
- * @param name - The identifier name to look for.
1338
- */
1339
- function identifierExistsInPattern(pattern, name) {
1340
- switch (pattern.type) {
1341
- case AST_NODE_TYPES.Identifier: return pattern.name === name;
1342
- case AST_NODE_TYPES.ObjectPattern: return pattern.properties.some((p) => {
1343
- if (p.type === AST_NODE_TYPES.Property) return identifierExistsInPattern(p.value, name);
1344
- if (p.type === AST_NODE_TYPES.RestElement) return identifierExistsInPattern(p.argument, name);
1345
- return false;
1346
- });
1347
- case AST_NODE_TYPES.ArrayPattern: return pattern.elements.some((el) => el != null && identifierExistsInPattern(el, name));
1348
- case AST_NODE_TYPES.RestElement: return identifierExistsInPattern(pattern.argument, name);
1349
- case AST_NODE_TYPES.AssignmentPattern: return identifierExistsInPattern(pattern.left, name);
1350
- case AST_NODE_TYPES.MemberExpression: return Extract.getRootIdentifier(pattern)?.name === name;
1351
- default: return false;
1352
- }
1353
- }
1354
- /**
1355
1368
  * Return the function node when `id` is a parameter of any position in any
1356
1369
  * ancestor function; otherwise return `null`.
1357
1370
  *
@@ -1396,6 +1409,17 @@ function create$48(context) {
1396
1409
  * @param node The CallExpression node to analyze.
1397
1410
  */
1398
1411
  CallExpression(node) {
1412
+ if (isUseStateCall(node) || core.isUseReducerCall(context, node)) {
1413
+ const declarator = node.parent;
1414
+ if (declarator?.type === AST_NODE_TYPES.VariableDeclarator && declarator.id?.type === AST_NODE_TYPES.ArrayPattern) {
1415
+ const [firstElement] = declarator.id.elements;
1416
+ if (firstElement?.type === AST_NODE_TYPES.Identifier && isRefLikeName(firstElement.name)) context.report({
1417
+ data: { name: firstElement.name },
1418
+ messageId: "noRefLikeStateName",
1419
+ node: firstElement
1420
+ });
1421
+ }
1422
+ }
1399
1423
  const callee = Extract.unwrap(node.callee);
1400
1424
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
1401
1425
  const { object, property } = callee;
@@ -1404,6 +1428,7 @@ function create$48(context) {
1404
1428
  const rootId = Extract.getRootIdentifier(object);
1405
1429
  if (rootId == null) return;
1406
1430
  if (rootId.name === "draft") return;
1431
+ if (hasRefLikeNameInChain(object)) return;
1407
1432
  const enclosingFn = Traverse.findParent(node, Check.isFunction);
1408
1433
  if (enclosingFn == null) return;
1409
1434
  const isState = isStateValue(rootId);
@@ -1434,6 +1459,7 @@ function create$48(context) {
1434
1459
  const rootId = Extract.getRootIdentifier(node.left);
1435
1460
  if (rootId == null) return;
1436
1461
  if (rootId.name === "draft") return;
1462
+ if (hasRefLikeNameInChain(node.left.object)) return;
1437
1463
  const enclosingFn = Traverse.findParent(node, Check.isFunction);
1438
1464
  if (enclosingFn == null) return;
1439
1465
  const isState = isStateValue(rootId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.7.2",
3
+ "version": "5.7.3",
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,21 +36,21 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@typescript-eslint/scope-manager": "^8.59.1",
40
- "@typescript-eslint/type-utils": "^8.59.1",
41
- "@typescript-eslint/types": "^8.59.1",
42
- "@typescript-eslint/typescript-estree": "^8.59.1",
43
- "@typescript-eslint/utils": "^8.59.1",
39
+ "@typescript-eslint/scope-manager": "^8.59.2",
40
+ "@typescript-eslint/type-utils": "^8.59.2",
41
+ "@typescript-eslint/types": "^8.59.2",
42
+ "@typescript-eslint/typescript-estree": "^8.59.2",
43
+ "@typescript-eslint/utils": "^8.59.2",
44
44
  "compare-versions": "^6.1.1",
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.7.2",
49
- "@eslint-react/core": "5.7.2",
50
- "@eslint-react/eslint": "5.7.2",
51
- "@eslint-react/jsx": "5.7.2",
52
- "@eslint-react/shared": "5.7.2",
53
- "@eslint-react/var": "5.7.2"
48
+ "@eslint-react/ast": "5.7.3",
49
+ "@eslint-react/eslint": "5.7.3",
50
+ "@eslint-react/jsx": "5.7.3",
51
+ "@eslint-react/shared": "5.7.3",
52
+ "@eslint-react/core": "5.7.3",
53
+ "@eslint-react/var": "5.7.3"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.14",