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.
- package/dist/index.js +181 -123
- 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.
|
|
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
|
|
1396
|
-
*
|
|
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
|
|
1406
|
-
if (
|
|
1407
|
-
const
|
|
1408
|
-
|
|
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
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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.
|
|
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/
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
51
|
-
"@eslint-react/
|
|
52
|
-
"@eslint-react/
|
|
53
|
-
"@eslint-react/var": "5.14.
|
|
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",
|