@blumintinc/eslint-plugin-blumint 1.21.7 → 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 +201 -13
- package/package.json +1 -1
- package/release-manifest.json +28 -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) => {
|
|
@@ -370,9 +372,177 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
370
372
|
}
|
|
371
373
|
};
|
|
372
374
|
/**
|
|
373
|
-
* Whether
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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
|
+
};
|
|
438
|
+
/** Pattern nodes a parameter's binding can be nested inside. */
|
|
439
|
+
const PATTERN_CONTAINERS = new Set([
|
|
440
|
+
utils_1.AST_NODE_TYPES.AssignmentPattern,
|
|
441
|
+
utils_1.AST_NODE_TYPES.Property,
|
|
442
|
+
utils_1.AST_NODE_TYPES.ObjectPattern,
|
|
443
|
+
utils_1.AST_NODE_TYPES.ArrayPattern,
|
|
444
|
+
utils_1.AST_NODE_TYPES.RestElement,
|
|
445
|
+
]);
|
|
446
|
+
const FUNCTION_TYPES = new Set([
|
|
447
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
448
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
449
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
450
|
+
utils_1.AST_NODE_TYPES.TSDeclareFunction,
|
|
451
|
+
]);
|
|
452
|
+
/**
|
|
453
|
+
* Whether a default value is what a PARAMETER's type is inferred FROM.
|
|
454
|
+
*
|
|
455
|
+
* Answered false in the two cases where freezing the default cannot change a
|
|
456
|
+
* signature: the parameter carries a type annotation, so its type is declared
|
|
457
|
+
* rather than inferred — looked for up the whole pattern, since a destructured
|
|
458
|
+
* parameter carries it on the pattern (`({ distance = DEFAULT }: Props)`) and
|
|
459
|
+
* a plain one on its binding (`(model: ModelName = DEFAULT)`) — or the default
|
|
460
|
+
* belongs to a destructuring declaration rather than a parameter list, which
|
|
461
|
+
* declares no signature at all.
|
|
462
|
+
*/
|
|
463
|
+
const isInferredParameterDefault = (pattern) => {
|
|
464
|
+
let current = pattern;
|
|
465
|
+
for (;;) {
|
|
466
|
+
if (current.typeAnnotation) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const parent = current.parent;
|
|
470
|
+
if (!parent) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (FUNCTION_TYPES.has(parent.type)) {
|
|
474
|
+
return parent.params.includes(current);
|
|
475
|
+
}
|
|
476
|
+
if (!PATTERN_CONTAINERS.has(parent.type)) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
current = parent;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
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.
|
|
486
|
+
*
|
|
487
|
+
* `as const` does not only freeze: it makes the literal type NON-WIDENING, and
|
|
488
|
+
* an inference site that widened `'ready'` to `string` then keeps the literal.
|
|
489
|
+
* A parameter defaulted from the constant therefore narrows to that one value,
|
|
490
|
+
* and every call passing a different one stops compiling (TS2345) for an input
|
|
491
|
+
* that compiled. The mutation walk cannot see this: nothing is written, the
|
|
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%.
|
|
504
|
+
*/
|
|
505
|
+
const isInferenceSite = (identifier) => {
|
|
506
|
+
let value = outermostValueOf(identifier);
|
|
507
|
+
for (;;) {
|
|
508
|
+
const parent = value.parent;
|
|
509
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
510
|
+
parent.right === value) {
|
|
511
|
+
return isInferredParameterDefault(parent.left);
|
|
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
|
+
}
|
|
521
|
+
const container = storageContainerOf(value);
|
|
522
|
+
if (!container) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
value = outermostValueOf(container);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* Whether anything in the file stops this binding taking `as const`, under its
|
|
530
|
+
* own name or through an alias of it.
|
|
531
|
+
*
|
|
532
|
+
* Two things disqualify it, because `as const` does two things. It freezes the
|
|
533
|
+
* value, so a WRITE — through the binding (`X.push(1)`), or to a binding that
|
|
534
|
+
* aliases it (`other = X`) — becomes TS2339/TS2540. And it makes the literal
|
|
535
|
+
* type NON-WIDENING, so an INFERENCE site that read the widened type keeps the
|
|
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`.
|
|
543
|
+
*
|
|
544
|
+
* Answered from the scope manager's reference list rather than a textual
|
|
545
|
+
* search for the name, so a same-named binding in
|
|
376
546
|
* another scope (`const arr` shadowed inside a callback) contributes nothing,
|
|
377
547
|
* and a same-named method on an unrelated receiver (`other.push(1)`) is never
|
|
378
548
|
* even visited.
|
|
@@ -394,21 +564,39 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
394
564
|
* check keyed on `const` would leave the `let` spelling breaking builds under
|
|
395
565
|
* `--fix`.
|
|
396
566
|
*/
|
|
397
|
-
const
|
|
567
|
+
const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
398
568
|
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
399
569
|
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
400
570
|
const pending = [variable];
|
|
401
571
|
const visited = new Set(pending);
|
|
402
572
|
for (let index = 0; index < pending.length; index += 1) {
|
|
403
573
|
for (const reference of pending[index].references) {
|
|
574
|
+
// Reassigning an alias is as disqualifying as writing through one. A
|
|
575
|
+
// binding that takes its type from the constant narrows to the frozen
|
|
576
|
+
// literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
|
|
577
|
+
// an input that compiled. `init` excludes the declaration's own write,
|
|
578
|
+
// which is how the alias was established rather than a change to it.
|
|
579
|
+
if (reference.isWrite() && !reference.init) {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (isInferenceSite(reference.identifier)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
404
585
|
const path = accessPathOf(reference.identifier);
|
|
405
|
-
if (path !== null
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
continue;
|
|
586
|
+
if (path !== null &&
|
|
587
|
+
(isMutatingMethodCall(path) || isWriteTarget(path))) {
|
|
588
|
+
return true;
|
|
410
589
|
}
|
|
411
|
-
|
|
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;
|
|
412
600
|
if (!declarator) {
|
|
413
601
|
continue;
|
|
414
602
|
}
|
|
@@ -837,7 +1025,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
837
1025
|
.getDeclaredVariables(declaration)
|
|
838
1026
|
.find((variable) => variable.name === name);
|
|
839
1027
|
return (!declaredVariable ||
|
|
840
|
-
!
|
|
1028
|
+
!blocksAsConstAssertion(declaredVariable, declaredVariablesOf));
|
|
841
1029
|
};
|
|
842
1030
|
if (shouldHaveAsConst(init)) {
|
|
843
1031
|
context.report({
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
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
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.21.8",
|
|
18
|
+
"date": "2026-09-05T03:15:57.605Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "global-const-style",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2329
|
|
25
|
+
],
|
|
26
|
+
"summary": "withhold the assertion where a type is inferred from the constant (closes #2329)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.21.7",
|
|
4
32
|
"date": "2026-09-04T21:52:00.765Z",
|