@blumintinc/eslint-plugin-blumint 1.20.131 → 1.20.133
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
|
@@ -394,6 +394,97 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
394
394
|
}
|
|
395
395
|
return false;
|
|
396
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* The array folds whose callback receives the PREVIOUS iteration's result as
|
|
399
|
+
* its first parameter. `reduceRight` carries the same contract as `reduce`;
|
|
400
|
+
* only the traversal order differs, so both fold a chain the same way.
|
|
401
|
+
*
|
|
402
|
+
* `map`/`forEach`/`filter` are deliberately absent: their callbacks receive
|
|
403
|
+
* an ELEMENT first, and awaiting an element is an ordinary data read rather
|
|
404
|
+
* than a serialization point.
|
|
405
|
+
*/
|
|
406
|
+
const SEQUENTIAL_FOLD_METHODS = new Set(['reduce', 'reduceRight']);
|
|
407
|
+
/**
|
|
408
|
+
* Reports whether a function node is the callback a fold folds WITH -- the
|
|
409
|
+
* first argument of a `.reduce(...)` / `.reduceRight(...)` call.
|
|
410
|
+
*
|
|
411
|
+
* Only a direct argument qualifies. A callback bound to a name first
|
|
412
|
+
* (`const step = async (promise, doc) => {...}; items.reduce(step, seed)`)
|
|
413
|
+
* is left alone rather than chased through the binding, which keeps the
|
|
414
|
+
* barrier to the shape it can prove.
|
|
415
|
+
*/
|
|
416
|
+
function isFoldCallback(fn) {
|
|
417
|
+
const call = fn.parent;
|
|
418
|
+
if (!call ||
|
|
419
|
+
call.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
420
|
+
call.arguments[0] !== fn) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const callee = call.callee;
|
|
424
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
const property = callee.property;
|
|
428
|
+
const methodName = callee.computed
|
|
429
|
+
? property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
430
|
+
typeof property.value === 'string'
|
|
431
|
+
? property.value
|
|
432
|
+
: null
|
|
433
|
+
: property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
434
|
+
? property.name
|
|
435
|
+
: null;
|
|
436
|
+
return !!methodName && SEQUENTIAL_FOLD_METHODS.has(methodName);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Reports whether a parameter binding is the FIRST parameter of a function
|
|
440
|
+
* -- the accumulator slot of a fold callback.
|
|
441
|
+
*
|
|
442
|
+
* Keyed on position rather than on the binding's name, because the
|
|
443
|
+
* accumulator is spelled `promise`, `acc`, `previous`, `prev`, `chain` or
|
|
444
|
+
* anything else the author preferred, and matching a name list would both
|
|
445
|
+
* miss the unlisted spellings and fire on an element parameter that happens
|
|
446
|
+
* to be called `promise`. A default (`async (acc = Promise.resolve(), doc)`)
|
|
447
|
+
* still binds the accumulator first, so the pattern is unwrapped.
|
|
448
|
+
*/
|
|
449
|
+
function isAccumulatorParameter(fn, binding) {
|
|
450
|
+
if (!FUNCTION_BOUNDARY_TYPES.has(fn.type)) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const [first] = fn.params;
|
|
454
|
+
if (!first) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
const target = first.type === utils_1.AST_NODE_TYPES.AssignmentPattern ? first.left : first;
|
|
458
|
+
return target === binding;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Reports whether an awaited expression is a bare read of the ACCUMULATOR
|
|
462
|
+
* parameter of an enclosing fold callback.
|
|
463
|
+
*
|
|
464
|
+
* The parameter is resolved through the scope chain rather than matched by
|
|
465
|
+
* name, so a local that merely shadows the accumulator's spelling
|
|
466
|
+
* (`const promise = fetchThing(); await promise;` inside the callback) is
|
|
467
|
+
* correctly NOT the accumulator, and an accumulator under any spelling is.
|
|
468
|
+
* TS-only wrappers are unwrapped because `await acc!` and
|
|
469
|
+
* `await acc as Promise<void>` read the same binding.
|
|
470
|
+
*/
|
|
471
|
+
function isFoldAccumulatorAwait(awaitExpr) {
|
|
472
|
+
let argument = awaitExpr.argument;
|
|
473
|
+
while (argument.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
474
|
+
argument.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
475
|
+
argument = argument.expression;
|
|
476
|
+
}
|
|
477
|
+
if (argument.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, argument), argument.name);
|
|
481
|
+
if (!variable) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return variable.defs.some((definition) => definition.type === utils_1.TSESLint.Scope.DefinitionType.Parameter &&
|
|
485
|
+
isAccumulatorParameter(definition.node, definition.name) &&
|
|
486
|
+
isFoldCallback(definition.node));
|
|
487
|
+
}
|
|
397
488
|
/**
|
|
398
489
|
* Follows an identifier back to the array literal a `const` binds to it.
|
|
399
490
|
*
|
|
@@ -838,6 +929,27 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
838
929
|
}
|
|
839
930
|
}
|
|
840
931
|
}
|
|
932
|
+
// 11. Fold-accumulator serialization barrier. `items.reduce(async
|
|
933
|
+
// (promise, item) => { await promise; await store(item); },
|
|
934
|
+
// Promise.resolve())` is the canonical idiom for FORCING sequential
|
|
935
|
+
// execution over a collection: the callback's first parameter IS the
|
|
936
|
+
// previous iteration's completion, so `await promise` is a serialization
|
|
937
|
+
// point rather than an operation of its own. The dependency it expresses
|
|
938
|
+
// is a sequencing one -- the accumulator and `store(item)` share no value
|
|
939
|
+
// at all -- so the identifier comparison above classifies the pair
|
|
940
|
+
// independent and the rewrite hoists the awaits into a Promise.all,
|
|
941
|
+
// starting every iteration's work at once and discarding the exact
|
|
942
|
+
// guarantee the idiom was written to provide. The run is deliberate, not
|
|
943
|
+
// a latency mistake, so the rule declines to report it rather than merely
|
|
944
|
+
// declining the fix. Only an accumulator await with something AFTER it
|
|
945
|
+
// qualifies, mirroring the guard barrier above: it is what follows the
|
|
946
|
+
// barrier that the rewrite would illegally start early. (#1851)
|
|
947
|
+
for (let i = 0; i < awaitNodes.length - 1; i++) {
|
|
948
|
+
const awaitExpr = getAwaitExpression(awaitNodes[i]);
|
|
949
|
+
if (awaitExpr && isFoldAccumulatorAwait(awaitExpr)) {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
841
953
|
return false;
|
|
842
954
|
}
|
|
843
955
|
/**
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
2
|
+
type MessageIds = 'preferType' | 'preferTypeDefaultExport';
|
|
3
|
+
export declare const preferTypeOverInterface: TSESLint.RuleModule<MessageIds, never[]>;
|
|
4
|
+
export {};
|
|
@@ -47,6 +47,21 @@ function isInsideModuleAugmentation(node) {
|
|
|
47
47
|
}
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* `export default interface X {…}` is the only spelling TypeScript has for a
|
|
52
|
+
* default-exported type: `export default type X = …` is a syntax error in every
|
|
53
|
+
* form, so the keyword swap has no landing site. It lands there anyway because
|
|
54
|
+
* the `TSInterfaceDeclaration`'s range starts at `interface` while
|
|
55
|
+
* `export default` belongs to the parent node, which made `eslint --fix` write
|
|
56
|
+
* a file that no longer parses (#1850).
|
|
57
|
+
*
|
|
58
|
+
* The conversion is still available to the author, as a two-statement
|
|
59
|
+
* restructure a keyword swap cannot express, so the report stands and carries
|
|
60
|
+
* that remedy instead of a fix.
|
|
61
|
+
*/
|
|
62
|
+
function isDefaultExported(node) {
|
|
63
|
+
return node.parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration;
|
|
64
|
+
}
|
|
50
65
|
/**
|
|
51
66
|
* Declaration merging is what `interface` can do and `type` cannot, so a name
|
|
52
67
|
* carrying more than one declaration is not a stylistic choice: rewriting one
|
|
@@ -92,6 +107,10 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
|
92
107
|
preferType: 'Interface "{{interfaceName}}" should be declared as a type alias. ' +
|
|
93
108
|
'Interfaces can merge across declarations and extend in chains, which fragments the resulting shape across files and makes composition harder to predict and trace. ' +
|
|
94
109
|
'Replace `interface` with `type` and use intersections (for example, `type {{interfaceName}} = Base & { field: string }`) to keep the contract closed and predictable.',
|
|
110
|
+
preferTypeDefaultExport: 'Interface "{{interfaceName}}" should be declared as a type alias, but a default-exported interface has no in-place conversion, so no autofix is offered here. ' +
|
|
111
|
+
'Interfaces can merge across declarations and extend in chains, which fragments the resulting shape across files and makes composition harder to predict and trace. ' +
|
|
112
|
+
'TypeScript has no default-exported type alias — `export default type {{interfaceName}} = ...` is a syntax error — so the conversion takes two statements: `type {{interfaceName}} = { field: string };` followed by `export type { {{interfaceName}} as default };`, which keeps every existing `import {{interfaceName}} from ...` working. ' +
|
|
113
|
+
'A named `export type {{interfaceName}} = ...` works too, once the import sites are switched to `import type { {{interfaceName}} }`.',
|
|
95
114
|
},
|
|
96
115
|
fixable: 'code',
|
|
97
116
|
},
|
|
@@ -108,6 +127,22 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
|
108
127
|
if (isMergedDeclaration(context, node)) {
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
130
|
+
// Withholding the fix rather than emitting a broken one: every rewrite
|
|
131
|
+
// below assumes the `interface` keyword can become `type` where it
|
|
132
|
+
// stands, and under `export default` that position accepts no
|
|
133
|
+
// declaration at all. The report is kept because the author *can* act
|
|
134
|
+
// on it — unlike a merge, the shape converts by hand — and the remedy
|
|
135
|
+
// travels in the message.
|
|
136
|
+
if (isDefaultExported(node)) {
|
|
137
|
+
context.report({
|
|
138
|
+
node,
|
|
139
|
+
messageId: 'preferTypeDefaultExport',
|
|
140
|
+
data: {
|
|
141
|
+
interfaceName: node.id.name,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
111
146
|
context.report({
|
|
112
147
|
node,
|
|
113
148
|
messageId: 'preferType',
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.133",
|
|
4
|
+
"date": "2026-08-07T14:16:50.604Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "parallelize-async-operations",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1851
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat a fold accumulator await as a sequencing barrier (closes #1851)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.132",
|
|
18
|
+
"date": "2026-08-07T12:26:09.881Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "prefer-type-over-interface",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1850
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline the fix for a default-exported interface (closes #1850)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.131",
|
|
4
32
|
"date": "2026-08-07T10:28:22.079Z",
|