@blumintinc/eslint-plugin-blumint 1.21.8 → 1.21.9
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/global-const-style.js +111 -14
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -313,8 +313,10 @@ const isWriteTarget = (node) => {
|
|
|
313
313
|
* Storing a reference does not copy it: the same array stays reachable through
|
|
314
314
|
* the container, so `holder.items.push(3)` writes through to the binding
|
|
315
315
|
* exactly as a direct alias does, and freezing it raises the same TS2339. A
|
|
316
|
-
* `SpreadElement` is excluded because it
|
|
317
|
-
* (`const COPY = [...ITEMS]`)
|
|
316
|
+
* `SpreadElement` is excluded because it builds a fresh VALUE
|
|
317
|
+
* (`const COPY = [...ITEMS]`) — it is not excluded from the walk entirely,
|
|
318
|
+
* because the copy still carries the constant's frozen TYPE, which
|
|
319
|
+
* `copyExpressionOf` handles. A computed key is excluded because it coerces
|
|
318
320
|
* the reference to a property name rather than retaining it.
|
|
319
321
|
*/
|
|
320
322
|
const storageContainerOf = (node) => {
|
|
@@ -369,6 +371,70 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
369
371
|
value = outermostValueOf(container);
|
|
370
372
|
}
|
|
371
373
|
};
|
|
374
|
+
/**
|
|
375
|
+
* Whether a callee spells `Object.assign`, in either the dotted or the
|
|
376
|
+
* bracketed form — read through `accessedPropertyName` so the two spellings
|
|
377
|
+
* cannot diverge from how the mutation walk already reads a method name.
|
|
378
|
+
*/
|
|
379
|
+
const isObjectAssignCallee = (callee) => {
|
|
380
|
+
const value = outermostValueOf(callee);
|
|
381
|
+
return (value.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
382
|
+
value.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
383
|
+
value.object.name === 'Object' &&
|
|
384
|
+
accessedPropertyName(value) === 'assign');
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Array methods whose result keeps the receiver's ELEMENT type. `map` is
|
|
388
|
+
* absent because its result is typed from the CALLBACK, so the constant's type
|
|
389
|
+
* reaches it only for a callback that returns its argument unchanged — a no-op
|
|
390
|
+
* `map`. Admitting it would withhold the assertion from every derived array
|
|
391
|
+
* anything is computed from, to cover a spelling nobody writes.
|
|
392
|
+
*/
|
|
393
|
+
const TYPE_PRESERVING_COPY_METHODS = new Set(['concat', 'slice', 'filter']);
|
|
394
|
+
/**
|
|
395
|
+
* The expression that builds a COPY carrying this value's type — the literal
|
|
396
|
+
* around a spread of it, the call of a copying array method on it, or an
|
|
397
|
+
* `Object.assign` it feeds.
|
|
398
|
+
*
|
|
399
|
+
* A copy is a fresh, mutable value, which is why `storageContainerOf` refuses
|
|
400
|
+
* it: writing to the copy cannot write through to the constant. But `as const`
|
|
401
|
+
* changes the constant's TYPE as well as its mutability, and a copy inherits
|
|
402
|
+
* that type — `[...ITEMS]` of a frozen `readonly [1, 2]` is `(1 | 2)[]`, so
|
|
403
|
+
* `COPY.push(3)` is TS2345 for an input that compiled. The copy is therefore
|
|
404
|
+
* followed for exactly the same question the alias walk asks: is the derived
|
|
405
|
+
* binding written?
|
|
406
|
+
*/
|
|
407
|
+
const copyExpressionOf = (node) => {
|
|
408
|
+
const parent = node.parent;
|
|
409
|
+
if (!parent) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (parent.type === utils_1.AST_NODE_TYPES.SpreadElement &&
|
|
413
|
+
parent.argument === node &&
|
|
414
|
+
(parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
415
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
|
|
416
|
+
return parent.parent;
|
|
417
|
+
}
|
|
418
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
419
|
+
parent.arguments.includes(node) &&
|
|
420
|
+
isObjectAssignCallee(parent.callee)) {
|
|
421
|
+
return parent;
|
|
422
|
+
}
|
|
423
|
+
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
424
|
+
parent.object === node) {
|
|
425
|
+
const method = accessedPropertyName(parent);
|
|
426
|
+
const callee = outermostValueOf(parent);
|
|
427
|
+
// A method REFERENCE (`const take = ITEMS.concat;`) builds nothing, so the
|
|
428
|
+
// copy only exists once the method is actually called.
|
|
429
|
+
if (method !== null &&
|
|
430
|
+
TYPE_PRESERVING_COPY_METHODS.has(method) &&
|
|
431
|
+
callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
432
|
+
callee.parent.callee === callee) {
|
|
433
|
+
return callee.parent;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
};
|
|
372
438
|
/** Pattern nodes a parameter's binding can be nested inside. */
|
|
373
439
|
const PATTERN_CONTAINERS = new Set([
|
|
374
440
|
utils_1.AST_NODE_TYPES.AssignmentPattern,
|
|
@@ -414,16 +480,27 @@ const isInferredParameterDefault = (pattern) => {
|
|
|
414
480
|
}
|
|
415
481
|
};
|
|
416
482
|
/**
|
|
417
|
-
* Whether a reference sits where TypeScript INFERS a type from it —
|
|
418
|
-
*
|
|
419
|
-
* is stored into.
|
|
483
|
+
* Whether a reference sits where TypeScript INFERS a type from it — a default
|
|
484
|
+
* parameter or a class property initializer — reached directly or through a
|
|
485
|
+
* composite literal it is stored into.
|
|
420
486
|
*
|
|
421
487
|
* `as const` does not only freeze: it makes the literal type NON-WIDENING, and
|
|
422
488
|
* an inference site that widened `'ready'` to `string` then keeps the literal.
|
|
423
489
|
* A parameter defaulted from the constant therefore narrows to that one value,
|
|
424
490
|
* and every call passing a different one stops compiling (TS2345) for an input
|
|
425
491
|
* that compiled. The mutation walk cannot see this: nothing is written, the
|
|
426
|
-
*
|
|
492
|
+
* declaration is simply inferred from a value the assertion changes.
|
|
493
|
+
*
|
|
494
|
+
* Both sites are answered on the same terms, because an annotation is what
|
|
495
|
+
* settles the question in each: a type written by hand is DECLARED, so nothing
|
|
496
|
+
* infers from the value and freezing it cannot move the declaration. Only the
|
|
497
|
+
* unannotated spelling narrows.
|
|
498
|
+
*
|
|
499
|
+
* A RETURN position infers in exactly the same way and is deliberately absent.
|
|
500
|
+
* Declining there costs 59 of 778 consumer reports (7.6%) — the constant need
|
|
501
|
+
* only be held in a literal that is returned — to prevent breaks that the
|
|
502
|
+
* consumer does not contain, so it is documented as a limitation instead. The
|
|
503
|
+
* comparable trade in #2330 was rejected at 5%.
|
|
427
504
|
*/
|
|
428
505
|
const isInferenceSite = (identifier) => {
|
|
429
506
|
let value = outermostValueOf(identifier);
|
|
@@ -433,6 +510,14 @@ const isInferenceSite = (identifier) => {
|
|
|
433
510
|
parent.right === value) {
|
|
434
511
|
return isInferredParameterDefault(parent.left);
|
|
435
512
|
}
|
|
513
|
+
// A class property's type is inferred from its initializer exactly as a
|
|
514
|
+
// parameter's is from its default, so `session.stage = 'live'` becomes
|
|
515
|
+
// TS2322 once the constant behind `stage = DEFAULT_STAGE` is frozen.
|
|
516
|
+
if ((parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
517
|
+
parent?.type === utils_1.AST_NODE_TYPES.AccessorProperty) &&
|
|
518
|
+
parent.value === value) {
|
|
519
|
+
return !parent.typeAnnotation;
|
|
520
|
+
}
|
|
436
521
|
const container = storageContainerOf(value);
|
|
437
522
|
if (!container) {
|
|
438
523
|
return false;
|
|
@@ -448,8 +533,13 @@ const isInferenceSite = (identifier) => {
|
|
|
448
533
|
* value, so a WRITE — through the binding (`X.push(1)`), or to a binding that
|
|
449
534
|
* aliases it (`other = X`) — becomes TS2339/TS2540. And it makes the literal
|
|
450
535
|
* type NON-WIDENING, so an INFERENCE site that read the widened type keeps the
|
|
451
|
-
* literal instead, which rewrites a
|
|
452
|
-
* touch.
|
|
536
|
+
* literal instead, which rewrites a declaration the assertion was never asked
|
|
537
|
+
* to touch.
|
|
538
|
+
*
|
|
539
|
+
* The type half reaches further than the value half, so the walk follows one
|
|
540
|
+
* edge the mutation question does not need: a COPY (`[...X]`, `X.concat()`),
|
|
541
|
+
* which is a fresh value but not a fresh type, and breaks on a write to the
|
|
542
|
+
* copy rather than to `X`.
|
|
453
543
|
*
|
|
454
544
|
* Answered from the scope manager's reference list rather than a textual
|
|
455
545
|
* search for the name, so a same-named binding in
|
|
@@ -493,13 +583,20 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
493
583
|
return true;
|
|
494
584
|
}
|
|
495
585
|
const path = accessPathOf(reference.identifier);
|
|
496
|
-
if (path !== null
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
continue;
|
|
586
|
+
if (path !== null &&
|
|
587
|
+
(isMutatingMethodCall(path) || isWriteTarget(path))) {
|
|
588
|
+
return true;
|
|
501
589
|
}
|
|
502
|
-
|
|
590
|
+
// A copy carries the constant's frozen type into a second binding, so it
|
|
591
|
+
// is enrolled on the same terms as an alias — but it is reached through a
|
|
592
|
+
// member access (`ITEMS.concat()`), which the alias walk deliberately
|
|
593
|
+
// refuses, so it is resolved before that refusal applies.
|
|
594
|
+
const copy = copyExpressionOf(outermostValueOf(reference.identifier));
|
|
595
|
+
const declarator = copy
|
|
596
|
+
? aliasDeclaratorOf(copy)
|
|
597
|
+
: path === null
|
|
598
|
+
? aliasDeclaratorOf(reference.identifier)
|
|
599
|
+
: null;
|
|
503
600
|
if (!declarator) {
|
|
504
601
|
continue;
|
|
505
602
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.9",
|
|
4
|
+
"date": "2026-09-05T05:55:33.427Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2331
|
|
11
|
+
],
|
|
12
|
+
"summary": "withhold the assertion where a copy or a class property carries the frozen type (closes #2331)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.21.8",
|
|
4
18
|
"date": "2026-09-05T03:15:57.605Z",
|