@blumintinc/eslint-plugin-blumint 1.21.6 → 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
|
@@ -305,6 +305,34 @@ const isWriteTarget = (node) => {
|
|
|
305
305
|
return false;
|
|
306
306
|
}
|
|
307
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* The composite literal a reference is STORED INTO — the object in
|
|
310
|
+
* `{ items: ITEMS }`, the array in `[ITEMS]` — or null for every other
|
|
311
|
+
* position.
|
|
312
|
+
*
|
|
313
|
+
* Storing a reference does not copy it: the same array stays reachable through
|
|
314
|
+
* the container, so `holder.items.push(3)` writes through to the binding
|
|
315
|
+
* exactly as a direct alias does, and freezing it raises the same TS2339. A
|
|
316
|
+
* `SpreadElement` is excluded because it genuinely builds a fresh value
|
|
317
|
+
* (`const COPY = [...ITEMS]`), and a computed key is excluded because it coerces
|
|
318
|
+
* the reference to a property name rather than retaining it.
|
|
319
|
+
*/
|
|
320
|
+
const storageContainerOf = (node) => {
|
|
321
|
+
const parent = node.parent;
|
|
322
|
+
if (!parent) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
326
|
+
parent.value === node &&
|
|
327
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
328
|
+
return parent.parent;
|
|
329
|
+
}
|
|
330
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
331
|
+
parent.elements.some((element) => element === node)) {
|
|
332
|
+
return parent;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
};
|
|
308
336
|
/**
|
|
309
337
|
* The declarator a reference initializes IN WHOLE — `OTHER` in
|
|
310
338
|
* `const OTHER = ITEMS` — or null for every other position. Such a declaration
|
|
@@ -319,24 +347,112 @@ const isWriteTarget = (node) => {
|
|
|
319
347
|
* tolerated — staying silent is the cheap error here, emitting a fix that stops
|
|
320
348
|
* the file compiling is not.
|
|
321
349
|
*
|
|
322
|
-
* A reference
|
|
323
|
-
*
|
|
324
|
-
* extracts a member rather than the whole, so
|
|
350
|
+
* A reference STORED INTO a composite literal is followed through that
|
|
351
|
+
* container, since storing does not copy — see `storageContainerOf`. A
|
|
352
|
+
* destructuring id extracts a member rather than the whole, so it is not an
|
|
353
|
+
* alias here.
|
|
325
354
|
*/
|
|
326
355
|
const aliasDeclaratorOf = (identifier) => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
declarator
|
|
331
|
-
declarator
|
|
332
|
-
|
|
356
|
+
// Ascends strictly, so reaching a node with no parent terminates the walk.
|
|
357
|
+
let value = outermostValueOf(identifier);
|
|
358
|
+
for (;;) {
|
|
359
|
+
const declarator = value.parent;
|
|
360
|
+
if (declarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
361
|
+
declarator.init === value &&
|
|
362
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
363
|
+
return declarator;
|
|
364
|
+
}
|
|
365
|
+
const container = storageContainerOf(value);
|
|
366
|
+
if (!container) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
value = outermostValueOf(container);
|
|
333
370
|
}
|
|
334
|
-
return declarator;
|
|
335
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
|
+
]);
|
|
336
386
|
/**
|
|
337
|
-
* Whether
|
|
338
|
-
*
|
|
339
|
-
*
|
|
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
|
+
};
|
|
416
|
+
/**
|
|
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
|
|
340
456
|
* another scope (`const arr` shadowed inside a callback) contributes nothing,
|
|
341
457
|
* and a same-named method on an unrelated receiver (`other.push(1)`) is never
|
|
342
458
|
* even visited.
|
|
@@ -358,13 +474,24 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
358
474
|
* check keyed on `const` would leave the `let` spelling breaking builds under
|
|
359
475
|
* `--fix`.
|
|
360
476
|
*/
|
|
361
|
-
const
|
|
477
|
+
const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
362
478
|
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
363
479
|
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
364
480
|
const pending = [variable];
|
|
365
481
|
const visited = new Set(pending);
|
|
366
482
|
for (let index = 0; index < pending.length; index += 1) {
|
|
367
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
|
+
}
|
|
368
495
|
const path = accessPathOf(reference.identifier);
|
|
369
496
|
if (path !== null) {
|
|
370
497
|
if (isMutatingMethodCall(path) || isWriteTarget(path)) {
|
|
@@ -801,7 +928,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
801
928
|
.getDeclaredVariables(declaration)
|
|
802
929
|
.find((variable) => variable.name === name);
|
|
803
930
|
return (!declaredVariable ||
|
|
804
|
-
!
|
|
931
|
+
!blocksAsConstAssertion(declaredVariable, declaredVariablesOf));
|
|
805
932
|
};
|
|
806
933
|
if (shouldHaveAsConst(init)) {
|
|
807
934
|
context.report({
|
|
@@ -68,6 +68,28 @@ function isFirestoreModuleSource(source) {
|
|
|
68
68
|
return (productSegment === product || !!productSegment?.startsWith(`${product}-`));
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* First segments that mark a path alias into the project's own tree rather than
|
|
73
|
+
* a published package. A scoped package's first segment is `@scope`, so a bare
|
|
74
|
+
* `@` can only be an alias.
|
|
75
|
+
*/
|
|
76
|
+
const FIRST_PARTY_SOURCE_ROOTS = new Set(['@', '~', 'src', 'app', 'lib']);
|
|
77
|
+
/**
|
|
78
|
+
* Whether a specifier names a published package, which is the only thing that
|
|
79
|
+
* can REFUTE Firestore provenance.
|
|
80
|
+
*
|
|
81
|
+
* A relative or absolute path, or a path alias, resolves into first-party code
|
|
82
|
+
* this rule cannot follow, so such a source says nothing about which product
|
|
83
|
+
* the binding came from. Reading its failure to look like `firebase/firestore`
|
|
84
|
+
* as proof of a different product is what silenced the rule on every
|
|
85
|
+
* `import { db } from '../../config/firebaseAdmin'` re-export.
|
|
86
|
+
*/
|
|
87
|
+
function isBarePackageSource(source) {
|
|
88
|
+
if (source.startsWith('.') || source.startsWith('/') || source === '') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return !FIRST_PARTY_SOURCE_ROOTS.has(moduleSegments(source)[0]);
|
|
92
|
+
}
|
|
71
93
|
/**
|
|
72
94
|
* The module `name` is imported from, or null when the file declares the name
|
|
73
95
|
* itself (a local helper, a parameter) or nothing declares it at all.
|
|
@@ -117,11 +139,13 @@ function provenanceIdentifier(callee) {
|
|
|
117
139
|
/**
|
|
118
140
|
* Whether a `runTransaction` call is the Firestore one this rule speaks about.
|
|
119
141
|
*
|
|
120
|
-
* The gate speaks only when it knows: a binding that resolves to an import
|
|
121
|
-
* judged by its module source, and anything else — a bare
|
|
122
|
-
* local helper, a member call on an unresolvable receiver
|
|
123
|
-
*
|
|
124
|
-
*
|
|
142
|
+
* The gate speaks only when it knows: a binding that resolves to an import of a
|
|
143
|
+
* published package is judged by its module source, and anything else — a bare
|
|
144
|
+
* call, a parameter, a local helper, a member call on an unresolvable receiver,
|
|
145
|
+
* an import from the project's own tree — keeps the rule's posture of
|
|
146
|
+
* reporting, since a name with no traceable origin is far more often Firestore
|
|
147
|
+
* (`db.runTransaction(...)`) than not. Only a package specifier can refute
|
|
148
|
+
* Firestore; a first-party path merely fails to confirm it.
|
|
125
149
|
*/
|
|
126
150
|
function isFirestoreTransactionCall(node, context) {
|
|
127
151
|
if (!isRunTransactionCall(node)) {
|
|
@@ -132,7 +156,7 @@ function isFirestoreTransactionCall(node, context) {
|
|
|
132
156
|
return true;
|
|
133
157
|
}
|
|
134
158
|
const source = importedSourceOf(ASTHelpers_1.ASTHelpers.getScope(context, node), carrier.name);
|
|
135
|
-
if (source === null) {
|
|
159
|
+
if (source === null || !isBarePackageSource(source)) {
|
|
136
160
|
return true;
|
|
137
161
|
}
|
|
138
162
|
return isFirestoreModuleSource(source);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
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
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.21.7",
|
|
18
|
+
"date": "2026-09-04T21:52:00.765Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "global-const-style",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2327
|
|
25
|
+
],
|
|
26
|
+
"summary": "follow the binding into a container (closes #2327)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "no-try-catch-already-exists-in-transaction",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2326
|
|
33
|
+
],
|
|
34
|
+
"summary": "report on first-party imports (closes #2326)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.21.6",
|
|
4
40
|
"date": "2026-09-04T20:52:21.747Z",
|