@blumintinc/eslint-plugin-blumint 1.21.7 → 1.21.8

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.7',
227
+ version: '1.21.8',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -369,10 +369,90 @@ const aliasDeclaratorOf = (identifier) => {
369
369
  value = outermostValueOf(container);
370
370
  }
371
371
  };
372
+ /** Pattern nodes a parameter's binding can be nested inside. */
373
+ const PATTERN_CONTAINERS = new Set([
374
+ utils_1.AST_NODE_TYPES.AssignmentPattern,
375
+ utils_1.AST_NODE_TYPES.Property,
376
+ utils_1.AST_NODE_TYPES.ObjectPattern,
377
+ utils_1.AST_NODE_TYPES.ArrayPattern,
378
+ utils_1.AST_NODE_TYPES.RestElement,
379
+ ]);
380
+ const FUNCTION_TYPES = new Set([
381
+ utils_1.AST_NODE_TYPES.FunctionDeclaration,
382
+ utils_1.AST_NODE_TYPES.FunctionExpression,
383
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
384
+ utils_1.AST_NODE_TYPES.TSDeclareFunction,
385
+ ]);
386
+ /**
387
+ * Whether a default value is what a PARAMETER's type is inferred FROM.
388
+ *
389
+ * Answered false in the two cases where freezing the default cannot change a
390
+ * signature: the parameter carries a type annotation, so its type is declared
391
+ * rather than inferred — looked for up the whole pattern, since a destructured
392
+ * parameter carries it on the pattern (`({ distance = DEFAULT }: Props)`) and
393
+ * a plain one on its binding (`(model: ModelName = DEFAULT)`) — or the default
394
+ * belongs to a destructuring declaration rather than a parameter list, which
395
+ * declares no signature at all.
396
+ */
397
+ const isInferredParameterDefault = (pattern) => {
398
+ let current = pattern;
399
+ for (;;) {
400
+ if (current.typeAnnotation) {
401
+ return false;
402
+ }
403
+ const parent = current.parent;
404
+ if (!parent) {
405
+ return false;
406
+ }
407
+ if (FUNCTION_TYPES.has(parent.type)) {
408
+ return parent.params.includes(current);
409
+ }
410
+ if (!PATTERN_CONTAINERS.has(parent.type)) {
411
+ return false;
412
+ }
413
+ current = parent;
414
+ }
415
+ };
372
416
  /**
373
- * Whether the binding is written through anywhere in the file, under its own
374
- * name or through an alias of it. Answered from the scope manager's reference
375
- * list rather than a textual search for the name, so a same-named binding in
417
+ * Whether a reference sits where TypeScript INFERS a type from it — the value
418
+ * of a default parameter, reached directly or through a composite literal it
419
+ * is stored into.
420
+ *
421
+ * `as const` does not only freeze: it makes the literal type NON-WIDENING, and
422
+ * an inference site that widened `'ready'` to `string` then keeps the literal.
423
+ * A parameter defaulted from the constant therefore narrows to that one value,
424
+ * and every call passing a different one stops compiling (TS2345) for an input
425
+ * that compiled. The mutation walk cannot see this: nothing is written, the
426
+ * signature is simply inferred from a value the assertion changes.
427
+ */
428
+ const isInferenceSite = (identifier) => {
429
+ let value = outermostValueOf(identifier);
430
+ for (;;) {
431
+ const parent = value.parent;
432
+ if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
433
+ parent.right === value) {
434
+ return isInferredParameterDefault(parent.left);
435
+ }
436
+ const container = storageContainerOf(value);
437
+ if (!container) {
438
+ return false;
439
+ }
440
+ value = outermostValueOf(container);
441
+ }
442
+ };
443
+ /**
444
+ * Whether anything in the file stops this binding taking `as const`, under its
445
+ * own name or through an alias of it.
446
+ *
447
+ * Two things disqualify it, because `as const` does two things. It freezes the
448
+ * value, so a WRITE — through the binding (`X.push(1)`), or to a binding that
449
+ * aliases it (`other = X`) — becomes TS2339/TS2540. And it makes the literal
450
+ * type NON-WIDENING, so an INFERENCE site that read the widened type keeps the
451
+ * literal instead, which rewrites a signature the assertion was never asked to
452
+ * touch.
453
+ *
454
+ * Answered from the scope manager's reference list rather than a textual
455
+ * search for the name, so a same-named binding in
376
456
  * another scope (`const arr` shadowed inside a callback) contributes nothing,
377
457
  * and a same-named method on an unrelated receiver (`other.push(1)`) is never
378
458
  * even visited.
@@ -394,13 +474,24 @@ const aliasDeclaratorOf = (identifier) => {
394
474
  * check keyed on `const` would leave the `let` spelling breaking builds under
395
475
  * `--fix`.
396
476
  */
397
- const isBindingMutated = (variable, declaredVariablesOf) => {
477
+ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
398
478
  // Grown in place and walked by index: an alias found mid-walk is appended and
399
479
  // reached by the same loop, so the traversal needs no recursion of its own.
400
480
  const pending = [variable];
401
481
  const visited = new Set(pending);
402
482
  for (let index = 0; index < pending.length; index += 1) {
403
483
  for (const reference of pending[index].references) {
484
+ // Reassigning an alias is as disqualifying as writing through one. A
485
+ // binding that takes its type from the constant narrows to the frozen
486
+ // literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
487
+ // an input that compiled. `init` excludes the declaration's own write,
488
+ // which is how the alias was established rather than a change to it.
489
+ if (reference.isWrite() && !reference.init) {
490
+ return true;
491
+ }
492
+ if (isInferenceSite(reference.identifier)) {
493
+ return true;
494
+ }
404
495
  const path = accessPathOf(reference.identifier);
405
496
  if (path !== null) {
406
497
  if (isMutatingMethodCall(path) || isWriteTarget(path)) {
@@ -837,7 +928,7 @@ exports.default = (0, createRule_1.createRule)({
837
928
  .getDeclaredVariables(declaration)
838
929
  .find((variable) => variable.name === name);
839
930
  return (!declaredVariable ||
840
- !isBindingMutated(declaredVariable, declaredVariablesOf));
931
+ !blocksAsConstAssertion(declaredVariable, declaredVariablesOf));
841
932
  };
842
933
  if (shouldHaveAsConst(init)) {
843
934
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.21.7",
3
+ "version": "1.21.8",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.21.8",
4
+ "date": "2026-09-05T03:15:57.605Z",
5
+ "rules": [
6
+ {
7
+ "name": "global-const-style",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2329
11
+ ],
12
+ "summary": "withhold the assertion where a type is inferred from the constant (closes #2329)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.21.7",
4
18
  "date": "2026-09-04T21:52:00.765Z",