@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 CHANGED
@@ -224,7 +224,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
224
224
  module.exports = {
225
225
  meta: {
226
226
  name: '@blumintinc/eslint-plugin-blumint',
227
- version: '1.21.16',
227
+ version: '1.21.18',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -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: `Object.keys(${identifierText}).length === 0` },
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 guard = `${node.operator}${identifierText} || Object.keys(${identifierText}).length === 0`;
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
- if (widened) {
1930
- return fixer.replaceTextRange(widened.range, widened.text);
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
- return fixer.replaceText(node, replacement);
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) && mentionsAny(guarded, bindings)) {
676
- return true;
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 name a binding PATTERN introduces — `x`, `{ n }`, `{ n: value }`,
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 patternBoundNames = (pattern, names = new Set()) => {
508
+ const patternBoundIdentifiers = (pattern, bindings = new Set()) => {
509
509
  switch (pattern.type) {
510
510
  case utils_1.AST_NODE_TYPES.Identifier:
511
- names.add(pattern.name);
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
- patternBoundNames(property.type === utils_1.AST_NODE_TYPES.Property
515
+ patternBoundIdentifiers(property.type === utils_1.AST_NODE_TYPES.Property
516
516
  ? property.value
517
- : property.argument, names);
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
- patternBoundNames(element, names);
525
+ patternBoundIdentifiers(element, bindings);
526
526
  }
527
527
  }
528
528
  break;
529
529
  case utils_1.AST_NODE_TYPES.AssignmentPattern:
530
- patternBoundNames(pattern.left, names);
530
+ patternBoundIdentifiers(pattern.left, bindings);
531
531
  break;
532
532
  case utils_1.AST_NODE_TYPES.RestElement:
533
- patternBoundNames(pattern.argument, names);
533
+ patternBoundIdentifiers(pattern.argument, bindings);
534
534
  break;
535
535
  default:
536
536
  break;
537
537
  }
538
- return names;
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 elementNames = patternBoundNames(parameter);
745
- if (elementNames.size === 0) {
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 terms rather than because they differ by one: an element keeps
1049
- * the constant's type through every derivation the iteration walk follows,
1050
- * while the receiver is the constant only when the iteration reads the
1051
- * constant's own value or member path — see `iterationBindingsOf`.
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 declares it `readonly T[]` — measured
1057
- * against `lib.es2020` — so `arr[0].n = 2` inside a `flatMap` callback is
1058
- * TS2540 for an input that compiled, while a mutating method called through the
1059
- * parameter compiles unchanged. `MUTABLE_ARRAY_PARAMETER_METHODS` carries that
1060
- * second half, which enrolment alone cannot express (Issue #2340).
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 `Object.values`/`Object.entries` array over it, the
1424
- * iterator its own `values`/`entries` hands back, and the collection built out
1425
- * of it. Each resolver returns an ANCESTOR of the node it is given, which is
1426
- * what lets the iteration walk follow them transitively without looping.
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
- * or member path rather than something derived from it. It gates the receiver
1546
- * ARRAY parameter alone: the element parameter is typed from the constant
1547
- * either way, while the array parameter names the constant only in the first
1548
- * case — see `iterationBindingsOf`.
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 = iteratesConstantValue
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: !MUTABLE_ARRAY_PARAMETER_METHODS.has(method),
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 the constant's own value or
1646
- * member path ALONE, the one iterable that hands the callback the constant
1647
- * itself. A derivation hands it the fresh outer value it
1648
- * built, and mutating that is no readonly violation:
1649
- * `[...ITEMS].forEach((item, index, arr) => { arr.push(3); })` does break after
1650
- * the fix, but as TS2345 — the spread narrows the element type, so `3` is not
1651
- * assignable — which belongs to the literal-narrowing family filed as #2330 and
1652
- * needs the type checker. Enrolling it here would withhold the assertion for a
1653
- * reason this arm cannot justify, so it is an over-decline (Issue #2339).
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
- return node.typeParameters.params.some((param) => typeNodeExcludesProperty(param, propertyName, resolveAlias, seen, argumentsAreTheType));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.21.16",
3
+ "version": "1.21.18",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",