@blumintinc/eslint-plugin-blumint 1.20.169 → 1.20.170
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/lib/index.js
CHANGED
|
@@ -28,8 +28,9 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
28
28
|
const ts = __importStar(require("typescript"));
|
|
29
29
|
const createRule_1 = require("../utils/createRule");
|
|
30
30
|
/**
|
|
31
|
-
* Node types whose presence
|
|
32
|
-
* safe to eager-evaluate inside a
|
|
31
|
+
* Node types whose presence in a branch value — outside any function the value
|
|
32
|
+
* itself carries — means the value is NOT safe to eager-evaluate inside a
|
|
33
|
+
* `Record` (all entries construct at once).
|
|
33
34
|
*/
|
|
34
35
|
const EAGER_UNSAFE_NODES = new Set([
|
|
35
36
|
utils_1.AST_NODE_TYPES.CallExpression,
|
|
@@ -959,7 +960,28 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
959
960
|
}
|
|
960
961
|
return null;
|
|
961
962
|
}
|
|
962
|
-
/**
|
|
963
|
+
/**
|
|
964
|
+
* True when any node in the subtree is unsafe to eager-evaluate.
|
|
965
|
+
*
|
|
966
|
+
* The walk stops at a function boundary, because what the `Record` literal
|
|
967
|
+
* evaluates per entry is the branch value itself, not what that value does
|
|
968
|
+
* when it is later invoked. A branch value that IS a function is already
|
|
969
|
+
* the thunk the report-only message asks for: its body runs on invocation,
|
|
970
|
+
* after the lookup, so a call inside it can no more fire per entry than a
|
|
971
|
+
* call inside any other uninvoked function. Scanning through the boundary
|
|
972
|
+
* declined every function-valued dispatch whose body did anything but
|
|
973
|
+
* arithmetic, and told the author to write the very shape they had (#2062).
|
|
974
|
+
*
|
|
975
|
+
* The boundary answers `await` too, and answers it correctly in both
|
|
976
|
+
* directions: an `await` is eager exactly when it is evaluated where the
|
|
977
|
+
* literal is built, so a top-level one still blocks, while one inside an
|
|
978
|
+
* `async` branch value — the only place it can legally appear inside a
|
|
979
|
+
* function — is suspended until that value is called.
|
|
980
|
+
*
|
|
981
|
+
* A class expression is deliberately not a boundary: its static
|
|
982
|
+
* initializers, static blocks and computed member names all run when the
|
|
983
|
+
* class definition is evaluated, which is per entry.
|
|
984
|
+
*/
|
|
963
985
|
function containsEagerUnsafe(node) {
|
|
964
986
|
let found = false;
|
|
965
987
|
const visit = (n) => {
|
|
@@ -974,6 +996,19 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
974
996
|
found = true;
|
|
975
997
|
return;
|
|
976
998
|
}
|
|
999
|
+
if (FUNCTION_TYPES.has(anyNode.type)) {
|
|
1000
|
+
// A parameter decorator is the one position inside a function that
|
|
1001
|
+
// does not wait for a call: it is evaluated with the enclosing class
|
|
1002
|
+
// definition, which the `Record` literal performs for every entry.
|
|
1003
|
+
for (const param of anyNode.params ?? []) {
|
|
1004
|
+
const decorators = param
|
|
1005
|
+
?.decorators;
|
|
1006
|
+
for (const decorator of decorators ?? []) {
|
|
1007
|
+
visit(decorator);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
977
1012
|
for (const key of Object.keys(anyNode)) {
|
|
978
1013
|
if (key === 'parent') {
|
|
979
1014
|
continue;
|
|
@@ -1410,15 +1445,98 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1410
1445
|
}
|
|
1411
1446
|
}
|
|
1412
1447
|
/**
|
|
1413
|
-
* The
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1448
|
+
* The opening parenthesis the PARENT's own syntax puts in front of `node` —
|
|
1449
|
+
* an `if`/`while`/`switch`/`with` head, a lone call or `new` argument, a
|
|
1450
|
+
* `do…while` test, `import(...)`. Such a pair delimits the parent rather
|
|
1451
|
+
* than grouping the expression, and dropping it does not parse.
|
|
1452
|
+
*
|
|
1453
|
+
* A call with more than one argument needs no entry: the token on one side
|
|
1454
|
+
* of any argument is then a comma, so no pair is seen around it at all.
|
|
1455
|
+
*/
|
|
1456
|
+
function parentSyntaxParen(node, parent) {
|
|
1457
|
+
const parenAfter = (preceding) => sourceCode.getTokenAfter(preceding, {
|
|
1458
|
+
filter: (token) => token.value === '(',
|
|
1459
|
+
});
|
|
1460
|
+
switch (parent.type) {
|
|
1461
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1462
|
+
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
1463
|
+
return parent.arguments.length === 1 && parent.arguments[0] === node
|
|
1464
|
+
? parenAfter(parent.callee)
|
|
1465
|
+
: null;
|
|
1466
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
1467
|
+
return parent.test === node ? parenAfter(parent.body) : null;
|
|
1468
|
+
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
1469
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
1470
|
+
return parent.test === node
|
|
1471
|
+
? sourceCode.getFirstToken(parent, 1)
|
|
1472
|
+
: null;
|
|
1473
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement:
|
|
1474
|
+
return parent.discriminant === node
|
|
1475
|
+
? sourceCode.getFirstToken(parent, 1)
|
|
1476
|
+
: null;
|
|
1477
|
+
case utils_1.AST_NODE_TYPES.WithStatement:
|
|
1478
|
+
return parent.object === node
|
|
1479
|
+
? sourceCode.getFirstToken(parent, 1)
|
|
1480
|
+
: null;
|
|
1481
|
+
case utils_1.AST_NODE_TYPES.ImportExpression:
|
|
1482
|
+
return parent.source === node
|
|
1483
|
+
? sourceCode.getFirstToken(parent, 1)
|
|
1484
|
+
: null;
|
|
1485
|
+
default:
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* The parentheses standing around `node` that the emitted lookup does not
|
|
1491
|
+
* need — the outermost such pair, so a nested grouping goes with it.
|
|
1492
|
+
*
|
|
1493
|
+
* Precedence is a property of the REPLACEMENT text, not of the expression
|
|
1494
|
+
* being replaced: a computed member access is the tightest-binding
|
|
1495
|
+
* production there is, so no enclosing operator can require a pair around
|
|
1496
|
+
* one. Parentheses a ternary genuinely needed (`(k === 'a' ? 1 : 2) > 0`
|
|
1497
|
+
* parses differently without them) are therefore redundant the moment the
|
|
1498
|
+
* lookup takes its place, and keeping them ships
|
|
1499
|
+
* `if ((RESULT_BY_KIND[kind]) > 0)` — text the next `prettier --write`
|
|
1500
|
+
* strips, so the fix is not a fixed point (#2063).
|
|
1501
|
+
*
|
|
1502
|
+
* Two pairs are not the expression's own grouping and stay: one the
|
|
1503
|
+
* parent's syntax requires, and one a decorator requires, whose grammar
|
|
1504
|
+
* admits no computed access at all (`@(cond)` cannot become `@X[k]`).
|
|
1505
|
+
*/
|
|
1506
|
+
function redundantParens(node) {
|
|
1507
|
+
const parent = node.parent;
|
|
1508
|
+
if (!parent || parent.type === utils_1.AST_NODE_TYPES.Decorator) {
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
const syntaxParen = parentSyntaxParen(node, parent);
|
|
1512
|
+
let pair = null;
|
|
1513
|
+
let open = sourceCode.getTokenBefore(node);
|
|
1514
|
+
let close = sourceCode.getTokenAfter(node);
|
|
1515
|
+
while (open !== null &&
|
|
1516
|
+
close !== null &&
|
|
1517
|
+
open !== syntaxParen &&
|
|
1518
|
+
open.type === utils_1.AST_TOKEN_TYPES.Punctuator &&
|
|
1519
|
+
open.value === '(' &&
|
|
1520
|
+
close.type === utils_1.AST_TOKEN_TYPES.Punctuator &&
|
|
1521
|
+
close.value === ')') {
|
|
1522
|
+
pair = { open, close };
|
|
1523
|
+
open = sourceCode.getTokenBefore(open);
|
|
1524
|
+
close = sourceCode.getTokenAfter(close);
|
|
1525
|
+
}
|
|
1526
|
+
return pair;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* The ternary's replacement, widened over text the lookup makes redundant —
|
|
1530
|
+
* the parentheses that used to bind the conditional, and the host's line
|
|
1531
|
+
* break when the shortened host fits on one line; null when only the
|
|
1532
|
+
* expression itself should be replaced.
|
|
1416
1533
|
*
|
|
1417
1534
|
* The break exists only because the ternary used to be wide; a lookup is
|
|
1418
1535
|
* short, and Prettier answers the shortened host by joining it back onto one
|
|
1419
1536
|
* line (#2060). This is the mirror of measuring an inserted head: the fixer
|
|
1420
1537
|
* SHORTENS, so every line it writes is well inside the width and nothing
|
|
1421
|
-
* about the emitted text reveals the stale wrap
|
|
1538
|
+
* about the emitted text reveals the stale wrap — which is equally true of
|
|
1539
|
+
* the stale parentheses (#2063).
|
|
1422
1540
|
*
|
|
1423
1541
|
* Measure, do not always-join: a host that is over-width for a reason the
|
|
1424
1542
|
* ternary never caused — a long binding name, a wide type annotation —
|
|
@@ -1429,9 +1547,28 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1429
1547
|
if (!parent) {
|
|
1430
1548
|
return null;
|
|
1431
1549
|
}
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1550
|
+
const parens = redundantParens(node);
|
|
1551
|
+
const span = {
|
|
1552
|
+
node,
|
|
1553
|
+
left: parens?.open ?? node,
|
|
1554
|
+
right: parens?.close ?? node,
|
|
1555
|
+
};
|
|
1556
|
+
const parenEdit = parens === null
|
|
1557
|
+
? null
|
|
1558
|
+
: {
|
|
1559
|
+
range: [parens.open.range[0], parens.close.range[1]],
|
|
1560
|
+
text: lookupText,
|
|
1561
|
+
};
|
|
1562
|
+
// Widest first: a host join is measured from the parentheses' outer
|
|
1563
|
+
// edges, so where one applies it already carries the paren widening. A
|
|
1564
|
+
// candidate whose span would delete a comment steps down to the next,
|
|
1565
|
+
// narrower one rather than withholding the conversion.
|
|
1566
|
+
const candidates = [
|
|
1567
|
+
absorbedOperatorWrap(span, parent, lookupText),
|
|
1568
|
+
absorbedArgumentWrap(span, parent, lookupText),
|
|
1569
|
+
parenEdit,
|
|
1570
|
+
];
|
|
1571
|
+
return (candidates.find((edit) => edit !== null && !absorbsComment(node, edit)) ?? null);
|
|
1435
1572
|
}
|
|
1436
1573
|
/**
|
|
1437
1574
|
* Whether the margins an absorbing edit adds around `node` hold a comment.
|
|
@@ -1447,6 +1584,11 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1447
1584
|
* declining there would cost the join and save nothing. The join is a layout
|
|
1448
1585
|
* optimization, so dropping it and replacing the expression in place keeps
|
|
1449
1586
|
* both the conversion and the comment.
|
|
1587
|
+
*
|
|
1588
|
+
* Measured against the reported node whatever the edit spans, this is the
|
|
1589
|
+
* single gate for every widening: the parenthesis absorption (#2063) adds
|
|
1590
|
+
* exactly such a margin, and a comment written between a parenthesis and the
|
|
1591
|
+
* expression it groups sits in it.
|
|
1450
1592
|
*/
|
|
1451
1593
|
function absorbsComment(node, edit) {
|
|
1452
1594
|
return sourceCode
|
|
@@ -1457,35 +1599,39 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1457
1599
|
comment.range[0] < edit.range[1]));
|
|
1458
1600
|
}
|
|
1459
1601
|
/** The join for a host that broke after `=` / `:` (see `absorbedLookupEdit`). */
|
|
1460
|
-
function absorbedOperatorWrap(
|
|
1602
|
+
function absorbedOperatorWrap(span, parent, lookupText) {
|
|
1461
1603
|
if (!WRAP_ABSORBING_PARENTS.has(parent.type) ||
|
|
1462
|
-
!isWrapAbsorbingPosition(node, parent)) {
|
|
1604
|
+
!isWrapAbsorbingPosition(span.node, parent)) {
|
|
1463
1605
|
return null;
|
|
1464
1606
|
}
|
|
1465
|
-
const prevToken = sourceCode.getTokenBefore(
|
|
1466
|
-
if (!prevToken || prevToken.loc.end.line ===
|
|
1607
|
+
const prevToken = sourceCode.getTokenBefore(span.left);
|
|
1608
|
+
if (!prevToken || prevToken.loc.end.line === span.left.loc.start.line) {
|
|
1467
1609
|
return null;
|
|
1468
1610
|
}
|
|
1469
1611
|
// Joining across a comment would pull the lookup onto a `//` line, which
|
|
1470
1612
|
// comments the value out.
|
|
1471
|
-
if (sourceCode.getCommentsBefore(
|
|
1613
|
+
if (sourceCode.getCommentsBefore(span.left).length > 0) {
|
|
1472
1614
|
return null;
|
|
1473
1615
|
}
|
|
1474
1616
|
const headLine = sourceCode.lines[prevToken.loc.end.line - 1] ?? '';
|
|
1475
1617
|
if (headLine.slice(prevToken.loc.end.column).trim() !== '') {
|
|
1476
1618
|
return null;
|
|
1477
1619
|
}
|
|
1478
|
-
const tailLine = sourceCode.lines[
|
|
1479
|
-
const tail = tailLine.slice(
|
|
1480
|
-
// Anything after the
|
|
1481
|
-
//
|
|
1620
|
+
const tailLine = sourceCode.lines[span.right.loc.end.line - 1] ?? '';
|
|
1621
|
+
const tail = tailLine.slice(span.right.loc.end.column).trimEnd();
|
|
1622
|
+
// Anything after the span beyond its own terminator (a closing paren the
|
|
1623
|
+
// span does not own, a trailing comment) means joining does not produce
|
|
1624
|
+
// one line.
|
|
1482
1625
|
if (!/^[,;]?$/.test(tail)) {
|
|
1483
1626
|
return null;
|
|
1484
1627
|
}
|
|
1485
1628
|
const head = headLine.slice(0, prevToken.loc.end.column);
|
|
1486
1629
|
const joined = `${head} ${lookupText}${tail}`;
|
|
1487
1630
|
return joined.length <= printWidth
|
|
1488
|
-
? {
|
|
1631
|
+
? {
|
|
1632
|
+
range: [prevToken.range[1], span.right.range[1]],
|
|
1633
|
+
text: ` ${lookupText}`,
|
|
1634
|
+
}
|
|
1489
1635
|
: null;
|
|
1490
1636
|
}
|
|
1491
1637
|
/**
|
|
@@ -1494,14 +1640,14 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1494
1640
|
* them — is what makes the shortened call a fixed point; keeping the comma
|
|
1495
1641
|
* would leave `render(RESULT[x],);`, which Prettier rewrites again.
|
|
1496
1642
|
*/
|
|
1497
|
-
function absorbedArgumentWrap(
|
|
1643
|
+
function absorbedArgumentWrap(span, parent, lookupText) {
|
|
1498
1644
|
if ((parent.type !== utils_1.AST_NODE_TYPES.CallExpression &&
|
|
1499
1645
|
parent.type !== utils_1.AST_NODE_TYPES.NewExpression) ||
|
|
1500
1646
|
parent.arguments.length !== 1 ||
|
|
1501
|
-
parent.arguments[0] !== node) {
|
|
1647
|
+
parent.arguments[0] !== span.node) {
|
|
1502
1648
|
return null;
|
|
1503
1649
|
}
|
|
1504
|
-
const openParen = sourceCode.getTokenBefore(
|
|
1650
|
+
const openParen = sourceCode.getTokenBefore(span.left);
|
|
1505
1651
|
const closeParen = sourceCode.getLastToken(parent);
|
|
1506
1652
|
if (!openParen ||
|
|
1507
1653
|
!closeParen ||
|
|
@@ -1510,18 +1656,18 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1510
1656
|
openParen.loc.end.line === closeParen.loc.start.line) {
|
|
1511
1657
|
return null;
|
|
1512
1658
|
}
|
|
1513
|
-
//
|
|
1514
|
-
// trail from the
|
|
1515
|
-
// Prettier's dangling comma.
|
|
1516
|
-
const afterNode = sourceCode.getTokenAfter(
|
|
1659
|
+
// Parentheses the span does not own would otherwise be swallowed: the
|
|
1660
|
+
// token trail from the span's right edge to the call's closer must hold
|
|
1661
|
+
// nothing but Prettier's dangling comma.
|
|
1662
|
+
const afterNode = sourceCode.getTokenAfter(span.right);
|
|
1517
1663
|
const afterComma = afterNode && afterNode.value === ','
|
|
1518
1664
|
? sourceCode.getTokenAfter(afterNode)
|
|
1519
1665
|
: afterNode;
|
|
1520
1666
|
if (afterComma !== closeParen) {
|
|
1521
1667
|
return null;
|
|
1522
1668
|
}
|
|
1523
|
-
if (sourceCode.getCommentsBefore(
|
|
1524
|
-
sourceCode.getCommentsAfter(
|
|
1669
|
+
if (sourceCode.getCommentsBefore(span.left).length > 0 ||
|
|
1670
|
+
sourceCode.getCommentsAfter(span.right).length > 0) {
|
|
1525
1671
|
return null;
|
|
1526
1672
|
}
|
|
1527
1673
|
const headLine = sourceCode.lines[openParen.loc.end.line - 1] ?? '';
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.170",
|
|
4
|
+
"date": "2026-08-19T23:48:20.873Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-map-over-conditional-dispatch",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2062,
|
|
11
|
+
2063
|
|
12
|
+
],
|
|
13
|
+
"summary": "drop the parentheses the replaced ternary needed (closes #2063); stop the eager-call scan at function boundaries (closes #2062)"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
2
17
|
{
|
|
3
18
|
"version": "1.20.169",
|
|
4
19
|
"date": "2026-08-19T20:16:38.161Z",
|