@blumintinc/eslint-plugin-blumint 1.21.16 → 1.21.18
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 +1 -1
- package/lib/rules/enforce-empty-object-check.d.ts +14 -0
- package/lib/rules/enforce-empty-object-check.js +230 -7
- package/lib/rules/enforce-memoize-async.js +120 -3
- package/lib/rules/global-const-style.js +438 -43
- package/lib/rules/prevent-children-clobber.js +11 -1
- package/package.json +1 -1
- package/release-manifest.json +46 -0
package/lib/index.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* The emptiness call the fixer emits, and where its binding comes from.
|
|
4
|
+
*
|
|
5
|
+
* Recognition and EMISSION are separate settings on purpose. `emptyCheckFunctions`
|
|
6
|
+
* is an allowlist of spellings the rule accepts and carries no import path, so
|
|
7
|
+
* reusing it to emit would write a bare call to an identifier the file does not
|
|
8
|
+
* import, and — since it defaults to `isEmpty` — would silently change what every
|
|
9
|
+
* existing consumer's `--fix` produces (#2360).
|
|
10
|
+
*/
|
|
11
|
+
type EmptyCheckFix = {
|
|
12
|
+
name: string;
|
|
13
|
+
importPath?: string;
|
|
14
|
+
};
|
|
2
15
|
type Options = [
|
|
3
16
|
{
|
|
4
17
|
objectNamePattern?: string[];
|
|
5
18
|
ignoreInLoops?: boolean;
|
|
6
19
|
emptyCheckFunctions?: string[];
|
|
20
|
+
emptyCheckFix?: EmptyCheckFix;
|
|
7
21
|
printWidth?: number;
|
|
8
22
|
}
|
|
9
23
|
];
|
|
@@ -28,6 +28,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
28
28
|
const ts = __importStar(require("typescript"));
|
|
29
29
|
const createRule_1 = require("../utils/createRule");
|
|
30
30
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
31
|
+
const importInsertion_1 = require("../utils/importInsertion");
|
|
31
32
|
const DEFAULT_OBJECT_SUFFIXES = [
|
|
32
33
|
'Config',
|
|
33
34
|
'Configs',
|
|
@@ -79,6 +80,20 @@ const NON_OBJECT_LIKE_NAMES = [
|
|
|
79
80
|
'array',
|
|
80
81
|
'arr',
|
|
81
82
|
];
|
|
83
|
+
/**
|
|
84
|
+
* The emptiness operand the fixer writes beside the falsiness check.
|
|
85
|
+
*
|
|
86
|
+
* `Object.keys(...).length === 0` needs nothing in scope, so it stays the
|
|
87
|
+
* default and every consumer that has not opted in keeps byte-identical output.
|
|
88
|
+
* A consumer whose config BANS the `Object` accessors names a helper instead,
|
|
89
|
+
* without which `--fix` has no fixed point: the rewrite it just wrote is
|
|
90
|
+
* rejected by the same run that produced it (#2360).
|
|
91
|
+
*/
|
|
92
|
+
function buildEmptyCheckText(identifierText, emptyCheckFix) {
|
|
93
|
+
return emptyCheckFix
|
|
94
|
+
? `${emptyCheckFix.name}(${identifierText})`
|
|
95
|
+
: `Object.keys(${identifierText}).length === 0`;
|
|
96
|
+
}
|
|
82
97
|
function hasBooleanPrefixBoundary(name) {
|
|
83
98
|
const lower = name.toLowerCase();
|
|
84
99
|
return BOOLEAN_PREFIXES.some((prefix) => {
|
|
@@ -1163,7 +1178,7 @@ function getDeclarationStatement(declaration) {
|
|
|
1163
1178
|
* width was measured (#2095).
|
|
1164
1179
|
*/
|
|
1165
1180
|
function planWidenedFix(input) {
|
|
1166
|
-
const { sourceCode, target, identifierText, needsParentheses } = input;
|
|
1181
|
+
const { sourceCode, target, identifierText, emptyCheckText, needsParentheses, } = input;
|
|
1167
1182
|
const source = sourceCode.getText();
|
|
1168
1183
|
const root = getLayoutRoot(target);
|
|
1169
1184
|
const parent = root.parent;
|
|
@@ -1175,7 +1190,7 @@ function planWidenedFix(input) {
|
|
|
1175
1190
|
operator: '||',
|
|
1176
1191
|
parts: [
|
|
1177
1192
|
{ kind: 'leaf', text: `${target.operator}${identifierText}` },
|
|
1178
|
-
{ kind: 'leaf', text:
|
|
1193
|
+
{ kind: 'leaf', text: emptyCheckText },
|
|
1179
1194
|
],
|
|
1180
1195
|
};
|
|
1181
1196
|
const docContext = {
|
|
@@ -1514,6 +1529,150 @@ function planReturnFix(root, parent, flat, doc, { sourceCode, printWidth }, sour
|
|
|
1514
1529
|
}
|
|
1515
1530
|
return { range: [root.range[0], root.range[1]], text: lines.join('\n') };
|
|
1516
1531
|
}
|
|
1532
|
+
/** Every name a binding pattern introduces. */
|
|
1533
|
+
function collectPatternNames(pattern, names) {
|
|
1534
|
+
switch (pattern.type) {
|
|
1535
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
1536
|
+
names.add(pattern.name);
|
|
1537
|
+
return;
|
|
1538
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
1539
|
+
for (const property of pattern.properties) {
|
|
1540
|
+
collectPatternNames(property.type === utils_1.AST_NODE_TYPES.RestElement
|
|
1541
|
+
? property.argument
|
|
1542
|
+
: property.value, names);
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1545
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
1546
|
+
for (const element of pattern.elements) {
|
|
1547
|
+
if (element) {
|
|
1548
|
+
collectPatternNames(element, names);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return;
|
|
1552
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
1553
|
+
collectPatternNames(pattern.left, names);
|
|
1554
|
+
return;
|
|
1555
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
1556
|
+
collectPatternNames(pattern.argument, names);
|
|
1557
|
+
return;
|
|
1558
|
+
default:
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* The names already declared at the module's top level.
|
|
1563
|
+
*
|
|
1564
|
+
* An import is only safe to insert when the name is free: a second binding of a
|
|
1565
|
+
* name the file already declares — imported from anywhere, or declared locally —
|
|
1566
|
+
* is a redeclaration error, which is a worse outcome than the lint warning the
|
|
1567
|
+
* insertion was meant to avoid.
|
|
1568
|
+
*/
|
|
1569
|
+
function moduleBindingNames(program) {
|
|
1570
|
+
const names = new Set();
|
|
1571
|
+
for (const statement of program.body) {
|
|
1572
|
+
const node = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
1573
|
+
statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
1574
|
+
? statement.declaration
|
|
1575
|
+
: statement;
|
|
1576
|
+
if (!node) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
1580
|
+
for (const specifier of node.specifiers) {
|
|
1581
|
+
names.add(specifier.local.name);
|
|
1582
|
+
}
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
1586
|
+
for (const declarator of node.declarations) {
|
|
1587
|
+
collectPatternNames(declarator.id, names);
|
|
1588
|
+
}
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
1592
|
+
node.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
1593
|
+
node.type === utils_1.AST_NODE_TYPES.TSEnumDeclaration ||
|
|
1594
|
+
node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
1595
|
+
node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
1596
|
+
node.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration ||
|
|
1597
|
+
node.type === utils_1.AST_NODE_TYPES.TSDeclareFunction ||
|
|
1598
|
+
node.type === utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration) &&
|
|
1599
|
+
node.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1600
|
+
names.add(node.id.name);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return names;
|
|
1604
|
+
}
|
|
1605
|
+
/** A module specifier written as source text, with its quotes escaped. */
|
|
1606
|
+
function quoteModuleSpecifier(importPath) {
|
|
1607
|
+
return `'${importPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* The edit that brings the emitted helper into scope, or null when nothing is
|
|
1611
|
+
* needed.
|
|
1612
|
+
*
|
|
1613
|
+
* A helper call the file cannot resolve trades a lint warning for an undefined
|
|
1614
|
+
* identifier, so emission and import travel together in ONE fixer. Four
|
|
1615
|
+
* outcomes are distinguished, because they are four different mistakes:
|
|
1616
|
+
*
|
|
1617
|
+
* * the name is already bound — insert nothing, whatever path it came from,
|
|
1618
|
+
* since a second binding would not compile;
|
|
1619
|
+
* * a value import from the same path already has a named-specifier list — join
|
|
1620
|
+
* it, so the file keeps one declaration per module;
|
|
1621
|
+
* * a file with imports gains a declaration after the LAST of them, which is
|
|
1622
|
+
* past every prologue by construction;
|
|
1623
|
+
* * a file with none defers to `importInsertion`, whose anchor owns the
|
|
1624
|
+
* placement a raw offset gets wrong: a `'use client'` demoted out of the
|
|
1625
|
+
* prologue, a `#!` pushed off line 1, or a line-binding suppression comment
|
|
1626
|
+
* severed from the line it covers.
|
|
1627
|
+
*/
|
|
1628
|
+
function planImportInsertion(sourceCode, emptyCheckFix) {
|
|
1629
|
+
const { name, importPath } = emptyCheckFix;
|
|
1630
|
+
if (!importPath) {
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
const program = sourceCode.ast;
|
|
1634
|
+
if (moduleBindingNames(program).has(name)) {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
const imports = program.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
1638
|
+
for (const declaration of imports) {
|
|
1639
|
+
/** A type-only import carries no value binding for the call to resolve. */
|
|
1640
|
+
if (declaration.importKind === 'type' ||
|
|
1641
|
+
declaration.source.value !== importPath) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const specifiers = declaration.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
1645
|
+
const last = specifiers[specifiers.length - 1];
|
|
1646
|
+
if (!last) {
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* A specifier list Prettier has already broken keeps one name per row, so
|
|
1651
|
+
* joining it inline would emit a layout the next format run undoes.
|
|
1652
|
+
*/
|
|
1653
|
+
const indent = declaration.loc.start.line === declaration.loc.end.line
|
|
1654
|
+
? null
|
|
1655
|
+
: getLineIndent(last, sourceCode);
|
|
1656
|
+
const text = indent === null ? `, ${name}` : `,\n${indent}${name}`;
|
|
1657
|
+
return {
|
|
1658
|
+
offset: last.range[1],
|
|
1659
|
+
apply: (fixer) => fixer.insertTextAfter(last, text),
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
const statement = `import { ${name} } from ${quoteModuleSpecifier(importPath)};`;
|
|
1663
|
+
const lastImport = imports[imports.length - 1];
|
|
1664
|
+
if (lastImport) {
|
|
1665
|
+
return {
|
|
1666
|
+
offset: lastImport.range[1],
|
|
1667
|
+
apply: (fixer) => fixer.insertTextAfter(lastImport, `\n${statement}`),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
|
|
1671
|
+
return {
|
|
1672
|
+
offset: anchor.kind === 'before' ? anchor.target.range[0] : anchor.index,
|
|
1673
|
+
apply: (fixer) => (0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, anchor, `${statement}\n`),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1517
1676
|
exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
1518
1677
|
name: 'enforce-empty-object-check',
|
|
1519
1678
|
meta: {
|
|
@@ -1538,6 +1697,20 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1538
1697
|
type: 'array',
|
|
1539
1698
|
items: { type: 'string' },
|
|
1540
1699
|
},
|
|
1700
|
+
emptyCheckFix: {
|
|
1701
|
+
type: 'object',
|
|
1702
|
+
properties: {
|
|
1703
|
+
/**
|
|
1704
|
+
* A bare identifier, so the emitted call is the shape the
|
|
1705
|
+
* recognition arm accepts — anything else would leave `--fix`
|
|
1706
|
+
* rewriting text it reports again on the next pass.
|
|
1707
|
+
*/
|
|
1708
|
+
name: { type: 'string', pattern: '^[A-Za-z_$][A-Za-z0-9_$]*$' },
|
|
1709
|
+
importPath: { type: 'string', minLength: 1 },
|
|
1710
|
+
},
|
|
1711
|
+
required: ['name'],
|
|
1712
|
+
additionalProperties: false,
|
|
1713
|
+
},
|
|
1541
1714
|
printWidth: {
|
|
1542
1715
|
type: 'number',
|
|
1543
1716
|
minimum: 1,
|
|
@@ -1556,7 +1729,7 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1556
1729
|
const parserServices = sourceCode.parserServices;
|
|
1557
1730
|
const checker = parserServices?.program?.getTypeChecker();
|
|
1558
1731
|
const options = context.options[0] ?? {};
|
|
1559
|
-
const { objectNamePattern = [], ignoreInLoops = false, emptyCheckFunctions = [], } = options;
|
|
1732
|
+
const { objectNamePattern = [], ignoreInLoops = false, emptyCheckFunctions = [], emptyCheckFix, } = options;
|
|
1560
1733
|
const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
|
|
1561
1734
|
? options.printWidth
|
|
1562
1735
|
: DEFAULT_PRINT_WIDTH;
|
|
@@ -1564,9 +1737,16 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1564
1737
|
...DEFAULT_OBJECT_SUFFIXES,
|
|
1565
1738
|
...objectNamePattern,
|
|
1566
1739
|
]);
|
|
1740
|
+
/**
|
|
1741
|
+
* The emitted helper counts as a satisfying check even when it is absent
|
|
1742
|
+
* from `emptyCheckFunctions`. Without this the fixer would report its own
|
|
1743
|
+
* output, so `--fix` would rewrite the same guard on every pass — the
|
|
1744
|
+
* fixed point this option exists to restore (#2360).
|
|
1745
|
+
*/
|
|
1567
1746
|
const emptyCheckFunctionsSet = new Set([
|
|
1568
1747
|
...DEFAULT_EMPTY_CHECK_FUNCTIONS,
|
|
1569
1748
|
...emptyCheckFunctions,
|
|
1749
|
+
...(emptyCheckFix ? [emptyCheckFix.name] : []),
|
|
1570
1750
|
]);
|
|
1571
1751
|
const processedExpressions = new WeakSet();
|
|
1572
1752
|
const processedNegations = new WeakSet();
|
|
@@ -1883,6 +2063,31 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1883
2063
|
}
|
|
1884
2064
|
return isObjectLikeName(identifier.name, patternSet);
|
|
1885
2065
|
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Whether a binding between the report site and the module scope would
|
|
2068
|
+
* capture the emitted helper call.
|
|
2069
|
+
*
|
|
2070
|
+
* A bare call resolves wherever the CALL sits, so an inner binding of the
|
|
2071
|
+
* same name silently takes it over — the rewrite compiles, reads
|
|
2072
|
+
* correctly, and calls something else entirely (#1455, #1456 are this
|
|
2073
|
+
* shape). No spelling of the call escapes that, so the fix is declined
|
|
2074
|
+
* and the report stands on its own; the message already says what to
|
|
2075
|
+
* write.
|
|
2076
|
+
*
|
|
2077
|
+
* The unconfigured emission is untouched by this: `Object` is a global
|
|
2078
|
+
* rather than a module binding, and its output is a compatibility
|
|
2079
|
+
* contract (#2360).
|
|
2080
|
+
*/
|
|
2081
|
+
function emissionIsShadowed(node, name) {
|
|
2082
|
+
let scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
2083
|
+
while (scope) {
|
|
2084
|
+
if (scope.variables.some((variable) => variable.name === name)) {
|
|
2085
|
+
return scope.type !== 'module' && scope.type !== 'global';
|
|
2086
|
+
}
|
|
2087
|
+
scope = scope.upper;
|
|
2088
|
+
}
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
1886
2091
|
function reportNegation(node, identifier) {
|
|
1887
2092
|
if (processedNegations.has(node)) {
|
|
1888
2093
|
return;
|
|
@@ -1906,8 +2111,12 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1906
2111
|
name: identifier.name,
|
|
1907
2112
|
},
|
|
1908
2113
|
fix(fixer) {
|
|
2114
|
+
if (emptyCheckFix && emissionIsShadowed(node, emptyCheckFix.name)) {
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
1909
2117
|
const identifierText = sourceCode.getText(identifier);
|
|
1910
|
-
const
|
|
2118
|
+
const emptyCheckText = buildEmptyCheckText(identifierText, emptyCheckFix);
|
|
2119
|
+
const guard = `${node.operator}${identifierText} || ${emptyCheckText}`;
|
|
1911
2120
|
const needsParentheses = replacementNeedsParentheses(node, sourceCode);
|
|
1912
2121
|
const replacement = needsParentheses ? `(${guard})` : guard;
|
|
1913
2122
|
/**
|
|
@@ -1923,13 +2132,27 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1923
2132
|
sourceCode,
|
|
1924
2133
|
target: node,
|
|
1925
2134
|
identifierText,
|
|
2135
|
+
emptyCheckText,
|
|
1926
2136
|
needsParentheses,
|
|
1927
2137
|
printWidth,
|
|
1928
2138
|
});
|
|
1929
|
-
|
|
1930
|
-
|
|
2139
|
+
const guardEdit = widened ?? {
|
|
2140
|
+
range: node.range,
|
|
2141
|
+
text: replacement,
|
|
2142
|
+
};
|
|
2143
|
+
const importFix = emptyCheckFix
|
|
2144
|
+
? planImportInsertion(sourceCode, emptyCheckFix)
|
|
2145
|
+
: null;
|
|
2146
|
+
if (!importFix) {
|
|
2147
|
+
return fixer.replaceTextRange(guardEdit.range, guardEdit.text);
|
|
1931
2148
|
}
|
|
1932
|
-
|
|
2149
|
+
const guardFix = {
|
|
2150
|
+
offset: guardEdit.range[0],
|
|
2151
|
+
apply: (target) => target.replaceTextRange(guardEdit.range, guardEdit.text),
|
|
2152
|
+
};
|
|
2153
|
+
return [importFix, guardFix]
|
|
2154
|
+
.sort((left, right) => left.offset - right.offset)
|
|
2155
|
+
.map((edit) => edit.apply(fixer));
|
|
1933
2156
|
},
|
|
1934
2157
|
});
|
|
1935
2158
|
}
|
|
@@ -636,6 +636,108 @@ function namesRelease(call) {
|
|
|
636
636
|
const name = calleeName(call);
|
|
637
637
|
return (name !== undefined && wordsOf(name).some((word) => RELEASE_WORDS.has(word)));
|
|
638
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Words that name REPORTING on the work rather than acting on what the work
|
|
641
|
+
* held. A call spelled from one of them observes the attempt: it reads the
|
|
642
|
+
* handle to say something about it, which leaves the handle exactly as
|
|
643
|
+
* outstanding as it was.
|
|
644
|
+
*
|
|
645
|
+
* Words for a lifetime ending stay out of this set for the reason they stay
|
|
646
|
+
* out of `RELEASE_WORDS`: `end` and `close` name the moment, and reading them
|
|
647
|
+
* here would strip the carve-out from a genuine hand-back spelled that way.
|
|
648
|
+
*/
|
|
649
|
+
const REPORT_WORDS = new Set([
|
|
650
|
+
'count',
|
|
651
|
+
'debug',
|
|
652
|
+
'emit',
|
|
653
|
+
'error',
|
|
654
|
+
'increment',
|
|
655
|
+
'info',
|
|
656
|
+
'log',
|
|
657
|
+
'measure',
|
|
658
|
+
'observe',
|
|
659
|
+
'record',
|
|
660
|
+
'report',
|
|
661
|
+
'span',
|
|
662
|
+
'time',
|
|
663
|
+
'track',
|
|
664
|
+
'trace',
|
|
665
|
+
'warn',
|
|
666
|
+
]);
|
|
667
|
+
/**
|
|
668
|
+
* Receivers that are somewhere to SEND an observation, not something a method
|
|
669
|
+
* holds. A call routed through one of them reports whatever its own verb is
|
|
670
|
+
* called, which is what reaches `span.setAttribute(…)` and `this.stats.bump(…)`
|
|
671
|
+
* — spellings whose verb carries no report word at all.
|
|
672
|
+
*/
|
|
673
|
+
const SINK_NAMES = new Set([
|
|
674
|
+
'analytics',
|
|
675
|
+
'console',
|
|
676
|
+
'log',
|
|
677
|
+
'logger',
|
|
678
|
+
'metrics',
|
|
679
|
+
'span',
|
|
680
|
+
'stats',
|
|
681
|
+
'telemetry',
|
|
682
|
+
'tracer',
|
|
683
|
+
]);
|
|
684
|
+
/**
|
|
685
|
+
* The nearest static name of a call's receiver: `logger` for
|
|
686
|
+
* `this.logger.info(…)` and for `this.deps.logger.info(…)` alike, `console`
|
|
687
|
+
* for `console.log(…)`. The nearest name rather than the root is what a sink
|
|
688
|
+
* is spelled as — the root of an injected logger is `this`, which says nothing.
|
|
689
|
+
*/
|
|
690
|
+
function receiverName(call) {
|
|
691
|
+
const callee = withoutChain(call.callee);
|
|
692
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
const receiver = withoutValueWrappers(callee.object);
|
|
696
|
+
if (receiver.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
697
|
+
return receiver.name;
|
|
698
|
+
}
|
|
699
|
+
return receiver.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
700
|
+
!receiver.computed &&
|
|
701
|
+
receiver.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
702
|
+
? receiver.property.name
|
|
703
|
+
: undefined;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The binding a call's receiver is rooted at: `log` for `log.flush()`,
|
|
707
|
+
* undefined for `this.logger.info(…)`, whose root is `this`.
|
|
708
|
+
*/
|
|
709
|
+
function receiverRootBinding(call) {
|
|
710
|
+
const callee = withoutChain(call.callee);
|
|
711
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
let receiver = withoutValueWrappers(callee.object);
|
|
715
|
+
while (receiver.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
716
|
+
receiver = withoutValueWrappers(receiver.object);
|
|
717
|
+
}
|
|
718
|
+
return receiver.type === utils_1.AST_NODE_TYPES.Identifier
|
|
719
|
+
? receiver.name
|
|
720
|
+
: undefined;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Whether the call reads as an observation of the attempt: its verb names a
|
|
724
|
+
* report, or it is routed through a sink. Either way what it does with a handle
|
|
725
|
+
* it names is describe it.
|
|
726
|
+
*
|
|
727
|
+
* A hand-back word in the verb is decided ahead of this, so
|
|
728
|
+
* `this.logger.release(lease)` stays a release: a sink is inferred from a name
|
|
729
|
+
* a caller chose freely, while a release verb states the intent outright.
|
|
730
|
+
*/
|
|
731
|
+
function readsAsReport(call) {
|
|
732
|
+
const name = calleeName(call);
|
|
733
|
+
if (name !== undefined &&
|
|
734
|
+
wordsOf(name).some((word) => REPORT_WORDS.has(word))) {
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const receiver = receiverName(call);
|
|
738
|
+
return (receiver !== undefined &&
|
|
739
|
+
wordsOf(receiver).some((word) => SINK_NAMES.has(word)));
|
|
740
|
+
}
|
|
639
741
|
/**
|
|
640
742
|
* Whether the `finally` gives back something the method took, rather than
|
|
641
743
|
* reporting on the work that took it.
|
|
@@ -650,6 +752,14 @@ function namesRelease(call) {
|
|
|
650
752
|
* bookkeeping the work never touched — `clearTimeout(t)` beside a `fetch` that
|
|
651
753
|
* ignores `t`, or a span that observes the attempt from outside it.
|
|
652
754
|
*
|
|
755
|
+
* Naming the handle is evidence the finalizer USED it, though, and a log line
|
|
756
|
+
* uses it exactly as loudly as a hand-back does: `this.logger.info('used
|
|
757
|
+
* lease', lease.id)` closes the triangle while the lease leaks. So a call that
|
|
758
|
+
* reads as an observation — a reporting verb, or a route through a sink — does
|
|
759
|
+
* not close it. The exception is a call ON the handle whose own acquisition is
|
|
760
|
+
* being paired: `log.flush()` acts on the thing it is spelled after, and only a
|
|
761
|
+
* receiver NAME suggested otherwise.
|
|
762
|
+
*
|
|
653
763
|
* The second shape is the name, and it covers the release a method does not
|
|
654
764
|
* acquire: the caller takes the lock and passes it in, leaving `finally {
|
|
655
765
|
* lock.release() }` with nothing in the body to pair against. Structurally that
|
|
@@ -671,10 +781,16 @@ function releasesResource(guarded, finalizer, acquisitions) {
|
|
|
671
781
|
if (namesRelease(node)) {
|
|
672
782
|
return true;
|
|
673
783
|
}
|
|
784
|
+
const reporting = readsAsReport(node);
|
|
785
|
+
const receiver = reporting ? receiverRootBinding(node) : undefined;
|
|
674
786
|
for (const bindings of acquisitions) {
|
|
675
|
-
if (mentionsAny(node, bindings)
|
|
676
|
-
|
|
787
|
+
if (!mentionsAny(node, bindings) || !mentionsAny(guarded, bindings)) {
|
|
788
|
+
continue;
|
|
677
789
|
}
|
|
790
|
+
if (reporting && (receiver === undefined || !bindings.has(receiver))) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
return true;
|
|
678
794
|
}
|
|
679
795
|
}
|
|
680
796
|
return false;
|
|
@@ -691,7 +807,8 @@ function releasesResource(guarded, finalizer, acquisitions) {
|
|
|
691
807
|
* (`finally { done = true; }`) records that the attempt finished instead of
|
|
692
808
|
* undoing it, and one that logs, emits a metric, ends a span or clears a timer
|
|
693
809
|
* the work never touched reports on the attempt instead of giving anything
|
|
694
|
-
* back, so none of them costs the method its report.
|
|
810
|
+
* back, so none of them costs the method its report. A log line naming the
|
|
811
|
+
* handle is such a report however much of the handle it names.
|
|
695
812
|
*/
|
|
696
813
|
function releasesInOwnFinalizer(fn) {
|
|
697
814
|
const acquisitions = acquisitionsOf(fn);
|
|
@@ -486,7 +486,7 @@ const accessPathRootOf = (node) => {
|
|
|
486
486
|
}
|
|
487
487
|
};
|
|
488
488
|
/**
|
|
489
|
-
* Every
|
|
489
|
+
* Every identifier a binding PATTERN introduces — `x`, `{ n }`, `{ n: value }`,
|
|
490
490
|
* `[head]`, `{ n, ...rest }` and every nesting of them.
|
|
491
491
|
*
|
|
492
492
|
* Destructuring and member access are two spellings of ONE extraction, so a
|
|
@@ -505,16 +505,16 @@ const accessPathRootOf = (node) => {
|
|
|
505
505
|
* Reading the default would trade an over-decline, which costs one report, for
|
|
506
506
|
* a `--fix` that stops the file compiling.
|
|
507
507
|
*/
|
|
508
|
-
const
|
|
508
|
+
const patternBoundIdentifiers = (pattern, bindings = new Set()) => {
|
|
509
509
|
switch (pattern.type) {
|
|
510
510
|
case utils_1.AST_NODE_TYPES.Identifier:
|
|
511
|
-
|
|
511
|
+
bindings.add(pattern);
|
|
512
512
|
break;
|
|
513
513
|
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
514
514
|
for (const property of pattern.properties) {
|
|
515
|
-
|
|
515
|
+
patternBoundIdentifiers(property.type === utils_1.AST_NODE_TYPES.Property
|
|
516
516
|
? property.value
|
|
517
|
-
: property.argument,
|
|
517
|
+
: property.argument, bindings);
|
|
518
518
|
}
|
|
519
519
|
break;
|
|
520
520
|
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
@@ -522,21 +522,28 @@ const patternBoundNames = (pattern, names = new Set()) => {
|
|
|
522
522
|
// An array pattern holds a HOLE for each skipped position
|
|
523
523
|
// (`([, second]) => second`), which binds no name.
|
|
524
524
|
if (element) {
|
|
525
|
-
|
|
525
|
+
patternBoundIdentifiers(element, bindings);
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
break;
|
|
529
529
|
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
530
|
-
|
|
530
|
+
patternBoundIdentifiers(pattern.left, bindings);
|
|
531
531
|
break;
|
|
532
532
|
case utils_1.AST_NODE_TYPES.RestElement:
|
|
533
|
-
|
|
533
|
+
patternBoundIdentifiers(pattern.argument, bindings);
|
|
534
534
|
break;
|
|
535
535
|
default:
|
|
536
536
|
break;
|
|
537
537
|
}
|
|
538
|
-
return
|
|
538
|
+
return bindings;
|
|
539
539
|
};
|
|
540
|
+
/**
|
|
541
|
+
* The NAMES those bindings introduce, which is what the value descent matches
|
|
542
|
+
* the element by — see `returnsHandedElement` for why that descent works in
|
|
543
|
+
* spellings rather than in bindings. Derived from the identifiers rather than
|
|
544
|
+
* collected alongside them so the two answers cannot drift apart.
|
|
545
|
+
*/
|
|
546
|
+
const patternBoundNames = (pattern) => new Set([...patternBoundIdentifiers(pattern)].map((binding) => binding.name));
|
|
540
547
|
/**
|
|
541
548
|
* Every value a function hands back: its expression body, the argument of each
|
|
542
549
|
* `return` in its block, and the argument of each `yield` — a generator hands
|
|
@@ -706,6 +713,205 @@ const functionCarriesElementType = (fn, elementNames) => {
|
|
|
706
713
|
return (carriesDefault ||
|
|
707
714
|
returnedValuesOf(fn).some((handed) => carriesElementType(handed, reachable)));
|
|
708
715
|
};
|
|
716
|
+
/**
|
|
717
|
+
* Every VALUE binding a single statement introduces into the scope holding it.
|
|
718
|
+
*
|
|
719
|
+
* Type declarations are skipped because `typeof x` reads the value namespace,
|
|
720
|
+
* so an alias or an interface spelled like the element shadows it nowhere.
|
|
721
|
+
*
|
|
722
|
+
* A `var` is collected against the block it is written in rather than the
|
|
723
|
+
* function it hoists to, so it is seen from inside that block alone. That
|
|
724
|
+
* under-reads shadowing, and a missed shadow reads a `typeof` as the element,
|
|
725
|
+
* which withholds a report — the cheap error of the two, since claiming a shadow
|
|
726
|
+
* that is not there licenses a freeze.
|
|
727
|
+
*/
|
|
728
|
+
const statementBoundIdentifiers = (statement, bindings) => {
|
|
729
|
+
switch (statement.type) {
|
|
730
|
+
case utils_1.AST_NODE_TYPES.VariableDeclaration:
|
|
731
|
+
for (const declarator of statement.declarations) {
|
|
732
|
+
patternBoundIdentifiers(declarator.id, bindings);
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
736
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
737
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration:
|
|
738
|
+
if (statement.id) {
|
|
739
|
+
bindings.add(statement.id);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case utils_1.AST_NODE_TYPES.ImportDeclaration:
|
|
743
|
+
for (const specifier of statement.specifiers) {
|
|
744
|
+
bindings.add(specifier.local);
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
case utils_1.AST_NODE_TYPES.ExportNamedDeclaration:
|
|
748
|
+
case utils_1.AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
749
|
+
if (statement.declaration) {
|
|
750
|
+
statementBoundIdentifiers(statement.declaration, bindings);
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
default:
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* The binding a node introduces for `name` in its OWN scope, or null when it
|
|
759
|
+
* introduces none.
|
|
760
|
+
*
|
|
761
|
+
* Carries just enough of the scope grammar to answer whether anything between a
|
|
762
|
+
* `typeof` and the element parameter rebinds the name, which is the only
|
|
763
|
+
* question `resolvesToElementBinding` asks of it. A scope form left out can fail
|
|
764
|
+
* to find a shadow and never invent one, so the error it admits is a withheld
|
|
765
|
+
* report rather than a freeze.
|
|
766
|
+
*/
|
|
767
|
+
const scopeBindingOf = (node, name) => {
|
|
768
|
+
const bindings = new Set();
|
|
769
|
+
switch (node.type) {
|
|
770
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
771
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
772
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
773
|
+
case utils_1.AST_NODE_TYPES.TSDeclareFunction:
|
|
774
|
+
for (const parameter of node.params) {
|
|
775
|
+
patternBoundIdentifiers(parameter, bindings);
|
|
776
|
+
}
|
|
777
|
+
// A function EXPRESSION binds its own name inside itself; an arrow has no
|
|
778
|
+
// name to bind.
|
|
779
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression && node.id) {
|
|
780
|
+
bindings.add(node.id);
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case utils_1.AST_NODE_TYPES.CatchClause:
|
|
784
|
+
if (node.param) {
|
|
785
|
+
patternBoundIdentifiers(node.param, bindings);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
789
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
790
|
+
if (node.id) {
|
|
791
|
+
bindings.add(node.id);
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
795
|
+
if (node.init) {
|
|
796
|
+
statementBoundIdentifiers(node.init, bindings);
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
800
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
801
|
+
statementBoundIdentifiers(node.left, bindings);
|
|
802
|
+
break;
|
|
803
|
+
case utils_1.AST_NODE_TYPES.Program:
|
|
804
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
805
|
+
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
806
|
+
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
807
|
+
for (const statement of node.body) {
|
|
808
|
+
statementBoundIdentifiers(statement, bindings);
|
|
809
|
+
}
|
|
810
|
+
break;
|
|
811
|
+
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
812
|
+
for (const statement of node.consequent) {
|
|
813
|
+
statementBoundIdentifiers(statement, bindings);
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
for (const binding of bindings) {
|
|
820
|
+
if (binding.name === name) {
|
|
821
|
+
return binding;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Whether an identifier written in a TYPE position resolves to one of the
|
|
828
|
+
* bindings the element parameter introduces.
|
|
829
|
+
*
|
|
830
|
+
* Resolution is lexical — the nearest enclosing binder wins — rather than a
|
|
831
|
+
* match on the name, because a `typeof` deep inside a callback can spell the
|
|
832
|
+
* element's name while naming something else entirely: `(item) => () => { const
|
|
833
|
+
* item = { n: 9 }; return (x: typeof item) => x; }` is typed from that local
|
|
834
|
+
* binding, which the constant reaches never, and freezing under it is measured
|
|
835
|
+
* safe.
|
|
836
|
+
*/
|
|
837
|
+
const resolvesToElementBinding = (identifier, elementBindings) => {
|
|
838
|
+
for (let node = identifier.parent; node; node = node.parent) {
|
|
839
|
+
const binding = scopeBindingOf(node, identifier.name);
|
|
840
|
+
if (binding) {
|
|
841
|
+
return elementBindings.has(binding);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* The identifier a `typeof` query is rooted at — `item` for `typeof item` and
|
|
848
|
+
* for `typeof item.n` alike — or null when the query names an import instead.
|
|
849
|
+
*/
|
|
850
|
+
const typeQueryRootOf = (query) => {
|
|
851
|
+
let entity = query.exprName;
|
|
852
|
+
while (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
853
|
+
entity = entity.left;
|
|
854
|
+
}
|
|
855
|
+
return entity.type === utils_1.AST_NODE_TYPES.Identifier ? entity : null;
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Whether a `typeof` anywhere inside `node` names the element.
|
|
859
|
+
*
|
|
860
|
+
* The element's type reaches a mapper's result through TYPE positions as
|
|
861
|
+
* readily as through value ones: a handed-back closure annotated `(x: typeof
|
|
862
|
+
* item) => x` is typed from the element without ever returning it, so freezing
|
|
863
|
+
* the receiver makes `makers.push((v: { n: number }) => v)` TS2345 for an input
|
|
864
|
+
* that compiled (Issue #2357).
|
|
865
|
+
*
|
|
866
|
+
* The query is looked for by STRUCTURE rather than by enumerating the type
|
|
867
|
+
* syntax it can sit under, because every wrapper measured breaks the build the
|
|
868
|
+
* same way — `typeof item.n`, `Wrapper<typeof item>`, `(typeof item)[]`,
|
|
869
|
+
* `[typeof item]`, `(typeof item)['n']`, a union, a conditional type, a
|
|
870
|
+
* function type, and a RETURN annotation rather than a parameter — and
|
|
871
|
+
* enumerating forms is what leaves the next spelling out.
|
|
872
|
+
*
|
|
873
|
+
* The sweep covers the whole CALLBACK rather than the values it returns,
|
|
874
|
+
* because the type an annotation names can be introduced beside them: `type
|
|
875
|
+
* Held = typeof item;` or an `interface` declared in the callback's body, a
|
|
876
|
+
* `satisfies` written in an expression, a class expression's field. Each of
|
|
877
|
+
* those is measured to break under `--fix` too, and each is invisible to a
|
|
878
|
+
* sweep of the returned value alone. A `typeof` of the element that composes
|
|
879
|
+
* the result nowhere then withholds one report, which is the cheap error of
|
|
880
|
+
* the two.
|
|
881
|
+
*/
|
|
882
|
+
const typeQueryNamesElement = (node, elementBindings) => {
|
|
883
|
+
// A query spelling some other name resolves to some other binding whatever
|
|
884
|
+
// the scopes hold, so screening on the spelling first keeps the lexical walk
|
|
885
|
+
// for the queries that can possibly be the element.
|
|
886
|
+
const spellings = new Set([...elementBindings].map((binding) => binding.name));
|
|
887
|
+
let found = false;
|
|
888
|
+
const visit = (current) => {
|
|
889
|
+
if (found) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeQuery) {
|
|
893
|
+
const root = typeQueryRootOf(current);
|
|
894
|
+
if (root &&
|
|
895
|
+
spellings.has(root.name) &&
|
|
896
|
+
resolvesToElementBinding(root, elementBindings)) {
|
|
897
|
+
found = true;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
for (const [key, value] of Object.entries(current)) {
|
|
902
|
+
if (key === 'parent') {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
906
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
907
|
+
visit(child);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
visit(node);
|
|
913
|
+
return found;
|
|
914
|
+
};
|
|
709
915
|
/**
|
|
710
916
|
* Whether a callback hands back the value it is given at `elementIndex`, or a
|
|
711
917
|
* value the element's type reaches — see `carriesElementType`.
|
|
@@ -732,6 +938,9 @@ const functionCarriesElementType = (fn, elementNames) => {
|
|
|
732
938
|
* different things widen their union, so the assertion may then reach nothing
|
|
733
939
|
* and the withhold costs a report; demanding EVERY branch would instead ship a
|
|
734
940
|
* `--fix` that stops the file compiling.
|
|
941
|
+
*
|
|
942
|
+
* A TYPE position carries the element too, and is asked as a separate question
|
|
943
|
+
* because it is resolved rather than name-matched — see `typeQueryNamesElement`.
|
|
735
944
|
*/
|
|
736
945
|
const returnsHandedElement = (callback, elementIndex) => {
|
|
737
946
|
if (!callback || !isFunctionValue(callback)) {
|
|
@@ -741,10 +950,14 @@ const returnsHandedElement = (callback, elementIndex) => {
|
|
|
741
950
|
if (!parameter) {
|
|
742
951
|
return false;
|
|
743
952
|
}
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
953
|
+
const elementBindings = patternBoundIdentifiers(parameter);
|
|
954
|
+
if (elementBindings.size === 0) {
|
|
746
955
|
return false;
|
|
747
956
|
}
|
|
957
|
+
if (typeQueryNamesElement(callback, elementBindings)) {
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
const elementNames = patternBoundNames(parameter);
|
|
748
961
|
return returnedValuesOf(callback).some((value) => carriesElementType(value, elementNames));
|
|
749
962
|
};
|
|
750
963
|
/**
|
|
@@ -874,6 +1087,165 @@ const copyExpressionOf = (node) => {
|
|
|
874
1087
|
}
|
|
875
1088
|
return null;
|
|
876
1089
|
};
|
|
1090
|
+
/**
|
|
1091
|
+
* The argument position `Array.from`'s MAPPER arrives in.
|
|
1092
|
+
*
|
|
1093
|
+
* Named beside `ARRAY_FROM_MAPPER_ELEMENT_INDEX` because the mapper is the
|
|
1094
|
+
* SECOND argument while the element it is handed is its FIRST parameter, so
|
|
1095
|
+
* two different indices for one call sit beside each other here.
|
|
1096
|
+
*/
|
|
1097
|
+
const ARRAY_FROM_MAPPER_ARGUMENT_INDEX = 1;
|
|
1098
|
+
/**
|
|
1099
|
+
* The call whose RESULT is typed from what this function literal hands back:
|
|
1100
|
+
* the `map`/`flatMap` call it is the callback of, or the `Array.from` call it
|
|
1101
|
+
* is the mapper of.
|
|
1102
|
+
*
|
|
1103
|
+
* The RECEIVER is not consulted, because the question is what composes the
|
|
1104
|
+
* result rather than where the elements came from — a mapper handing back a
|
|
1105
|
+
* value of its own retypes the result whatever it is mapping over.
|
|
1106
|
+
*
|
|
1107
|
+
* A callback passed by NAME is a LIMITATION rather than a decision: the climb
|
|
1108
|
+
* ends at the function's own declaration, and reaching the call sites from
|
|
1109
|
+
* there is a hop through the binding that this resolver does not take, so
|
|
1110
|
+
* `const pick = () => OTHER; items.map(pick)` keeps its report and its fix.
|
|
1111
|
+
*/
|
|
1112
|
+
const mapperCallOf = (fn) => {
|
|
1113
|
+
const value = outermostValueOf(fn);
|
|
1114
|
+
const call = value.parent;
|
|
1115
|
+
if (!call || call.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
if (call.arguments[ARRAY_FROM_MAPPER_ARGUMENT_INDEX] === value &&
|
|
1119
|
+
isNamespacedCallee(call.callee, 'Array', 'from')) {
|
|
1120
|
+
return call;
|
|
1121
|
+
}
|
|
1122
|
+
if (call.arguments[0] !== value) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const callee = unwrapValueWrappers(call.callee);
|
|
1126
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const method = accessedPropertyName(callee);
|
|
1130
|
+
return method !== null && CALLBACK_TYPED_COPY_METHODS.has(method)
|
|
1131
|
+
? call
|
|
1132
|
+
: null;
|
|
1133
|
+
};
|
|
1134
|
+
/** The function whose body encloses this node, or null at the top level. */
|
|
1135
|
+
const enclosingFunctionOf = (node) => {
|
|
1136
|
+
let current = node.parent;
|
|
1137
|
+
while (current) {
|
|
1138
|
+
if (FUNCTION_TYPES.has(current.type)) {
|
|
1139
|
+
return current;
|
|
1140
|
+
}
|
|
1141
|
+
current = current.parent;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
};
|
|
1145
|
+
/**
|
|
1146
|
+
* The expression whose type this value COMPOSES, or null where it composes
|
|
1147
|
+
* nothing that leaves the position it sits in.
|
|
1148
|
+
*
|
|
1149
|
+
* These are the positions `carriesElementType` descends through, read in the
|
|
1150
|
+
* opposite direction: that resolver starts from a returned value and asks
|
|
1151
|
+
* whether the element reaches it, while this one starts from a reference and
|
|
1152
|
+
* climbs to what is returned. The two must agree, since one file's
|
|
1153
|
+
* `ITEMS.map((x) => [x])` and `ITEMS.map(() => [OTHER])` are the same
|
|
1154
|
+
* composition of a frozen value into a fresh array.
|
|
1155
|
+
*
|
|
1156
|
+
* A `return` and a `yield` climb to the function they leave, whose result type
|
|
1157
|
+
* they compose — the pair `returnedValuesOf` collects going the other way.
|
|
1158
|
+
*
|
|
1159
|
+
* Everything else stops the climb, which withholds nothing that matters: an
|
|
1160
|
+
* arithmetic operand, a template literal and a call argument all WIDEN, so the
|
|
1161
|
+
* assertion no longer reaches the value the position produces.
|
|
1162
|
+
*/
|
|
1163
|
+
const composedValueOf = (value) => {
|
|
1164
|
+
const parent = value.parent;
|
|
1165
|
+
if (!parent) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
switch (parent.type) {
|
|
1169
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
1170
|
+
// The TEST decides which branch runs rather than what the expression is
|
|
1171
|
+
// typed as, so only the branches compose the result's union.
|
|
1172
|
+
return parent.consequent === value || parent.alternate === value
|
|
1173
|
+
? parent
|
|
1174
|
+
: null;
|
|
1175
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
1176
|
+
return parent;
|
|
1177
|
+
case utils_1.AST_NODE_TYPES.SequenceExpression:
|
|
1178
|
+
return parent.expressions[parent.expressions.length - 1] === value
|
|
1179
|
+
? parent
|
|
1180
|
+
: null;
|
|
1181
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
1182
|
+
return parent;
|
|
1183
|
+
case utils_1.AST_NODE_TYPES.Property:
|
|
1184
|
+
// A COMPUTED key types the property NAME rather than the value, and the
|
|
1185
|
+
// pattern screen reads names, not keys.
|
|
1186
|
+
return parent.value === value &&
|
|
1187
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression
|
|
1188
|
+
? parent.parent
|
|
1189
|
+
: null;
|
|
1190
|
+
case utils_1.AST_NODE_TYPES.SpreadElement:
|
|
1191
|
+
return parent.parent &&
|
|
1192
|
+
(parent.parent.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
1193
|
+
parent.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
1194
|
+
? parent.parent
|
|
1195
|
+
: null;
|
|
1196
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
1197
|
+
return parent;
|
|
1198
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
1199
|
+
return parent.body === value ? parent : null;
|
|
1200
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1201
|
+
// A function literal INVOKED on the spot hands its returns to the call,
|
|
1202
|
+
// which is the one call whose result the assertion still reaches — the
|
|
1203
|
+
// exception `carriesElementType` makes for the same shape.
|
|
1204
|
+
return isFunctionValue(value) && parent.callee === value ? parent : null;
|
|
1205
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
1206
|
+
case utils_1.AST_NODE_TYPES.YieldExpression:
|
|
1207
|
+
return parent.argument === value ? enclosingFunctionOf(parent) : null;
|
|
1208
|
+
default:
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* The `map`/`flatMap`/`Array.from` call whose result is typed from THIS value,
|
|
1214
|
+
* or null when no mapper hands it back.
|
|
1215
|
+
*
|
|
1216
|
+
* A mapper's result is typed from what it returns, so a constant reached
|
|
1217
|
+
* through that return types the copy exactly as the receiver's own element type
|
|
1218
|
+
* does — and freezing the constant narrows the copy's elements, making
|
|
1219
|
+
* `const xs = ITEMS.map(() => OTHER); xs.push({ n: 9 })` TS2322 for an input
|
|
1220
|
+
* that compiled (Issue #2356). `carriesElementType` cannot answer this: it is
|
|
1221
|
+
* keyed on the names the ELEMENT parameter binds, and the value here comes from
|
|
1222
|
+
* a different binding entirely.
|
|
1223
|
+
*
|
|
1224
|
+
* The constant is identified by the reference walk that calls this, which reads
|
|
1225
|
+
* the scope manager's reference lists rather than matching names — so a
|
|
1226
|
+
* callback-local binding that shadows the constant's name is a different
|
|
1227
|
+
* variable and reaches this resolver never, and a LOCAL constant, which this
|
|
1228
|
+
* rule does not freeze, is walked never.
|
|
1229
|
+
*
|
|
1230
|
+
* Returns an ANCESTOR of the node it is given, as every derivation resolver
|
|
1231
|
+
* must, so the walk that follows them strictly ascends.
|
|
1232
|
+
*/
|
|
1233
|
+
const mapperResultOf = (node) => {
|
|
1234
|
+
let value = outermostValueOf(node);
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
if (isFunctionValue(value)) {
|
|
1237
|
+
const call = mapperCallOf(value);
|
|
1238
|
+
if (call) {
|
|
1239
|
+
return call;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const composed = composedValueOf(value);
|
|
1243
|
+
if (!composed) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
value = outermostValueOf(composed);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
877
1249
|
/**
|
|
878
1250
|
* Array methods whose result is an ELEMENT of the receiver rather than a fresh
|
|
879
1251
|
* array over it.
|
|
@@ -1045,19 +1417,23 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
1045
1417
|
* handed-node spelling escapes it (Issue #2339).
|
|
1046
1418
|
*
|
|
1047
1419
|
* The position is carried apart from the element's because the two are enrolled
|
|
1048
|
-
* on different
|
|
1049
|
-
* the constant's
|
|
1050
|
-
* while the receiver
|
|
1051
|
-
* constant's own value or member path
|
|
1420
|
+
* on different TERMS rather than because they differ by one: an element keeps
|
|
1421
|
+
* the constant's readonly-ness through every derivation the iteration walk
|
|
1422
|
+
* follows, while the receiver parameter carries it only when the iteration
|
|
1423
|
+
* reads the constant's own value or member path. Over a DERIVED receiver
|
|
1424
|
+
* (`[...ITEMS].forEach(…)`) the parameter names the fresh array the derivation
|
|
1425
|
+
* built, which is mutable, and only the narrower assignability question remains
|
|
1426
|
+
* — see `MUTABLE_ARRAY_PARAMETER_METHODS` and `iterationBindingsOf`.
|
|
1052
1427
|
* `reduce`/`reduceRight` push it to fourth, having spent the first position on
|
|
1053
1428
|
* the accumulator.
|
|
1054
1429
|
*
|
|
1055
1430
|
* `flatMap` is listed for its ELEMENTS alone. Its lib signature declares the
|
|
1056
|
-
* parameter `T[]` where every sibling
|
|
1057
|
-
* against `lib.es2020` — so `arr[0].n = 2` inside a
|
|
1058
|
-
* TS2540 for an input that compiled, while a mutating
|
|
1059
|
-
* parameter compiles unchanged.
|
|
1060
|
-
* second half, which enrolment
|
|
1431
|
+
* parameter `T[]` where every sibling on `ReadonlyArray` declares it
|
|
1432
|
+
* `readonly T[]` — measured against `lib.es2020` — so `arr[0].n = 2` inside a
|
|
1433
|
+
* `flatMap` callback is TS2540 for an input that compiled, while a mutating
|
|
1434
|
+
* method called through the parameter compiles unchanged.
|
|
1435
|
+
* `MUTABLE_ARRAY_PARAMETER_METHODS` carries that second half, which enrolment
|
|
1436
|
+
* alone cannot express (Issue #2340).
|
|
1061
1437
|
*/
|
|
1062
1438
|
const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
1063
1439
|
['forEach', 2],
|
|
@@ -1074,7 +1450,14 @@ const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
1074
1450
|
['reduceRight', 3],
|
|
1075
1451
|
]);
|
|
1076
1452
|
/**
|
|
1077
|
-
* The methods above whose receiver-array parameter is declared MUTABLE `T[]
|
|
1453
|
+
* The methods above whose receiver-array parameter is declared MUTABLE `T[]`
|
|
1454
|
+
* even when the receiver itself is frozen.
|
|
1455
|
+
*
|
|
1456
|
+
* The set is keyed on the METHOD because that is the only thing that varies
|
|
1457
|
+
* once the receiver is the constant's own value. A DERIVED receiver settles the
|
|
1458
|
+
* same question by a shorter route and does not consult this set at all: the
|
|
1459
|
+
* derivation builds a fresh `Array<T>`, so every one of its methods declares
|
|
1460
|
+
* the parameter `T[]` and the whole family is mutable (Issue #2355).
|
|
1078
1461
|
*
|
|
1079
1462
|
* Enrolling that parameter answers two questions at once, and each needs its own
|
|
1080
1463
|
* answer. Its ELEMENTS are frozen with the constant, so an element write through
|
|
@@ -1420,13 +1803,15 @@ const frozenAccessPathsRootedAt = (identifier, frozenValue) => {
|
|
|
1420
1803
|
};
|
|
1421
1804
|
/**
|
|
1422
1805
|
* Every way a value derived from this one keeps the constant's ELEMENT types:
|
|
1423
|
-
* a copy of it, the
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1806
|
+
* a copy of it, the mapper result it is handed back into, the
|
|
1807
|
+
* `Object.values`/`Object.entries` array over it, the iterator its own
|
|
1808
|
+
* `values`/`entries` hands back, and the collection built out of it. Each
|
|
1809
|
+
* resolver returns an ANCESTOR of the node it is given, which is what lets the
|
|
1810
|
+
* iteration walk follow them transitively without looping.
|
|
1427
1811
|
*/
|
|
1428
1812
|
const DERIVATION_RESOLVERS = [
|
|
1429
1813
|
copyExpressionOf,
|
|
1814
|
+
mapperResultOf,
|
|
1430
1815
|
elementProjectionCallOf,
|
|
1431
1816
|
iteratorProjectionCallOf,
|
|
1432
1817
|
elementCollectionOf,
|
|
@@ -1446,12 +1831,18 @@ const DERIVATION_RESOLVERS = [
|
|
|
1446
1831
|
* absent from the resolver itself, its result being `string[]` whatever the
|
|
1447
1832
|
* argument holds.
|
|
1448
1833
|
*
|
|
1834
|
+
* A mapper RESULT is followed for that same TYPE half: the copy it builds is
|
|
1835
|
+
* typed from what the callback hands back, so a constant reached through that
|
|
1836
|
+
* return narrows the copy's elements and `const xs = ITEMS.map(() => OTHER);
|
|
1837
|
+
* xs.push({ n: 9 });` is TS2322 for an input that compiled (Issue #2356).
|
|
1838
|
+
*
|
|
1449
1839
|
* The remaining iteration resolvers stay absent: an ITERATOR is not a value a
|
|
1450
1840
|
* binding is written through, and `new Set(ITEMS)` reshapes the constant into a
|
|
1451
1841
|
* collection whose own question the iteration walk asks.
|
|
1452
1842
|
*/
|
|
1453
1843
|
const ALIAS_DERIVATION_RESOLVERS = [
|
|
1454
1844
|
copyExpressionOf,
|
|
1845
|
+
mapperResultOf,
|
|
1455
1846
|
elementExpressionOf,
|
|
1456
1847
|
elementProjectionCallOf,
|
|
1457
1848
|
];
|
|
@@ -1541,11 +1932,12 @@ const parameterBindingsOf = (callback, positions, declaredVariablesOf) => {
|
|
|
1541
1932
|
* passed by name is declared elsewhere, where its parameter carries whatever
|
|
1542
1933
|
* type that declaration gives it rather than one read off the constant.
|
|
1543
1934
|
*
|
|
1544
|
-
* `iteratesConstantValue` says whether `iterable` is the constant's own value
|
|
1545
|
-
*
|
|
1546
|
-
* ARRAY parameter
|
|
1547
|
-
*
|
|
1548
|
-
* case
|
|
1935
|
+
* `iteratesConstantValue` says whether `iterable` is the constant's own value or
|
|
1936
|
+
* member path rather than something derived from it. It decides the TERMS the
|
|
1937
|
+
* receiver ARRAY parameter is enrolled on rather than whether to enrol it: the
|
|
1938
|
+
* element parameter is typed from the constant either way, while the array
|
|
1939
|
+
* parameter is the constant itself only in the first case and a fresh mutable
|
|
1940
|
+
* array in the second — see `iterationBindingsOf`.
|
|
1549
1941
|
*/
|
|
1550
1942
|
const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
|
|
1551
1943
|
// The member path is resolved first because the iterated expression is
|
|
@@ -1596,9 +1988,7 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1596
1988
|
if (!callback || !isFunctionValue(callback)) {
|
|
1597
1989
|
return [];
|
|
1598
1990
|
}
|
|
1599
|
-
const arrayIndex =
|
|
1600
|
-
? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
|
|
1601
|
-
: undefined;
|
|
1991
|
+
const arrayIndex = ARRAY_PARAMETER_INDEX_BY_METHOD.get(method);
|
|
1602
1992
|
return parameterBindingsOf(callback, [
|
|
1603
1993
|
{ index: elementIndex, breaksOnAnyMutatingMethod: true },
|
|
1604
1994
|
...(arrayIndex === undefined
|
|
@@ -1606,7 +1996,8 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1606
1996
|
: [
|
|
1607
1997
|
{
|
|
1608
1998
|
index: arrayIndex,
|
|
1609
|
-
breaksOnAnyMutatingMethod:
|
|
1999
|
+
breaksOnAnyMutatingMethod: iteratesConstantValue &&
|
|
2000
|
+
!MUTABLE_ARRAY_PARAMETER_METHODS.has(method),
|
|
1610
2001
|
},
|
|
1611
2002
|
]),
|
|
1612
2003
|
], declaredVariablesOf);
|
|
@@ -1642,15 +2033,19 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1642
2033
|
* path as well as from the reference, since the value a derivation is taken
|
|
1643
2034
|
* from is routinely a PROPERTY of the constant — see `accessPathsRootedAt`.
|
|
1644
2035
|
*
|
|
1645
|
-
* The receiver ARRAY parameter is enrolled for
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
2036
|
+
* The receiver ARRAY parameter is enrolled for a derived iterable as well as for
|
|
2037
|
+
* the constant's own value, but on different terms, because the two break on
|
|
2038
|
+
* different things. The constant's own value hands the callback the frozen
|
|
2039
|
+
* array, so any mutating call through the parameter is a TS2339. A derivation
|
|
2040
|
+
* hands it the fresh MUTABLE array the derivation built, where there is no
|
|
2041
|
+
* TS2339 to have — and the fresh array's ELEMENTS are still the constant's, so
|
|
2042
|
+
* the assertion narrows what it accepts and a call that INSERTS a value from
|
|
2043
|
+
* outside the constant is rejected:
|
|
2044
|
+
* `[...ITEMS].forEach((item, index, arr) => { arr.push({ n: 3 }); })` compiles
|
|
2045
|
+
* and is TS2322 under the assertion (Issue #2355). That is the same narrow
|
|
2046
|
+
* question `MUTABLE_ARRAY_PARAMETER_METHODS` poses for `flatMap`, so the derived
|
|
2047
|
+
* enrolment reuses it, keeping `arr.sort()` and `arr.push(item)` fixable while
|
|
2048
|
+
* declining the insertion.
|
|
1654
2049
|
*/
|
|
1655
2050
|
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
1656
2051
|
const value = outermostValueOf(identifier);
|
|
@@ -352,7 +352,17 @@ function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new S
|
|
|
352
352
|
if (node.typeParameters?.params) {
|
|
353
353
|
const argumentsAreTheType = closedLiteralCounts &&
|
|
354
354
|
PROPERTY_PRESERVING_WRAPPERS.has(wrapperNameOf(node.typeName) ?? '');
|
|
355
|
-
|
|
355
|
+
if (node.typeParameters.params.some((param) => typeNodeExcludesProperty(param, propertyName, resolveAlias, seen, argumentsAreTheType))) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
// Arguments that prove nothing are not proof that the referenced alias
|
|
359
|
+
// proves nothing, so a reference carrying them still falls through to its
|
|
360
|
+
// own body. Returning here made `type P<T> = Omit<X, 'children'>` report
|
|
361
|
+
// when referenced as `P<string>` while the byte-identical unparameterized
|
|
362
|
+
// spelling passed. Type arguments are deliberately NOT substituted into
|
|
363
|
+
// the body: an unbound parameter there proves nothing, which is the
|
|
364
|
+
// conservative answer, and an undeclared wrapper still resolves to
|
|
365
|
+
// nothing and still reports.
|
|
356
366
|
}
|
|
357
367
|
// Keyed by position as well as by name: the same alias proves different
|
|
358
368
|
// things in a props position than inside an arbitrary generic, so a visit
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.18",
|
|
4
|
+
"date": "2026-09-07T22:15:12.507Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-empty-object-check",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2360
|
|
11
|
+
],
|
|
12
|
+
"summary": "emit a configurable empty-check helper (closes #2360)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prevent-children-clobber",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2361
|
|
19
|
+
],
|
|
20
|
+
"summary": "resolve a parameterized props alias (closes #2361)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.21.17",
|
|
26
|
+
"date": "2026-09-07T12:35:50.668Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-memoize-async",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2359
|
|
33
|
+
],
|
|
34
|
+
"summary": "tell a finalizer's log from a hand-back (closes #2359)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "global-const-style",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
2355,
|
|
41
|
+
2356,
|
|
42
|
+
2357
|
|
43
|
+
],
|
|
44
|
+
"summary": "read the element's type through type positions, not only value ones (closes #2357); track a copy against every constant whose type reaches it (closes #2356); withhold when a callback inserts through the receiver parameter (closes #2355)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
2
48
|
{
|
|
3
49
|
"version": "1.21.16",
|
|
4
50
|
"date": "2026-09-07T07:42:18.078Z",
|