@blumintinc/eslint-plugin-blumint 1.20.69 → 1.20.71
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/class-methods-read-top-to-bottom.js +36 -19
- package/lib/rules/no-firestore-object-arrays.js +138 -2
- package/lib/rules/no-useless-usememo-primitives.js +16 -0
- package/lib/rules/parallelize-async-operations.js +66 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.js +270 -5
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -80,25 +80,42 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
80
80
|
if (actualMember !== expectedMember) {
|
|
81
81
|
const classNameReport = className || 'this class';
|
|
82
82
|
const sourceCode = context.getSourceCode();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
83
|
+
const sourceText = sourceCode.getText();
|
|
84
|
+
// A member's block spans its leading comments through its own end,
|
|
85
|
+
// so documentation travels with the member it describes. Because
|
|
86
|
+
// every comment in the body is thereby absorbed into some block,
|
|
87
|
+
// the text between two adjacent blocks is pure whitespace.
|
|
88
|
+
const memberBlocks = node.body.map((member) => {
|
|
89
|
+
const comments = sourceCode.getCommentsBefore(member) || [];
|
|
90
|
+
const start = Math.min(member.range[0], ...comments.map((comment) => comment.range[0]));
|
|
91
|
+
return {
|
|
92
|
+
name: getMemberName(member),
|
|
93
|
+
text: sourceText.slice(start, member.range[1]),
|
|
94
|
+
start,
|
|
95
|
+
end: member.range[1],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
// Reuse those whitespace runs positionally instead of joining with
|
|
99
|
+
// a bare '\n'. The blank lines between members are the part that
|
|
100
|
+
// matters: prettier preserves existing blank lines but never
|
|
101
|
+
// inserts new ones, so collapsing them is irreversible (#1592).
|
|
102
|
+
// Carrying the runs verbatim also reproduces the newline and
|
|
103
|
+
// indentation after `{` and the newline before `}` for free, since
|
|
104
|
+
// every member sits at the same depth.
|
|
105
|
+
const separators = memberBlocks
|
|
106
|
+
.slice(1)
|
|
107
|
+
.map((block, index) => sourceText.slice(memberBlocks[index].end, block.start));
|
|
108
|
+
const prefix = sourceText.slice(node.range[0] + 1, memberBlocks[0].start);
|
|
109
|
+
const suffix = sourceText.slice(memberBlocks[memberBlocks.length - 1].end, node.range[1] - 1);
|
|
110
|
+
const newClassBody = prefix +
|
|
111
|
+
sortedOrder
|
|
112
|
+
.map((n) => {
|
|
113
|
+
const block = memberBlocks.find(({ name }) => name === n);
|
|
114
|
+
return block ? block.text : '';
|
|
115
|
+
})
|
|
116
|
+
.map((text, index) => index === 0 ? text : separators[index - 1] + text)
|
|
117
|
+
.join('') +
|
|
118
|
+
suffix;
|
|
102
119
|
return context.report({
|
|
103
120
|
node,
|
|
104
121
|
messageId: 'classMethodsReadTopToBottom',
|
|
@@ -43,6 +43,42 @@ const isParenthesizedType = (node) => {
|
|
|
43
43
|
return (candidate.type === 'TSParenthesizedType' &&
|
|
44
44
|
candidate.typeAnnotation !== undefined);
|
|
45
45
|
};
|
|
46
|
+
const unwrapParenthesizedTypeNode = (node) => {
|
|
47
|
+
let current = node;
|
|
48
|
+
// Cap iterations for the same reason as unwrapArrayElementType: wrappers are
|
|
49
|
+
// finite, but a future wrapper case must not be able to loop forever.
|
|
50
|
+
for (let i = 0; i < 10; i++) {
|
|
51
|
+
if (isParenthesizedType(current)) {
|
|
52
|
+
current = current.typeAnnotation;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
};
|
|
59
|
+
// Assertion wrappers never change the runtime value, so `[...] as const` and
|
|
60
|
+
// `[...] as const satisfies readonly string[]` both still describe an array.
|
|
61
|
+
const EXPRESSION_ASSERTION_TYPES = new Set([
|
|
62
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
63
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
64
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
65
|
+
'TSSatisfiesExpression',
|
|
66
|
+
]);
|
|
67
|
+
const unwrapExpressionAssertions = (node) => {
|
|
68
|
+
let current = node;
|
|
69
|
+
for (let i = 0; i < 10; i++) {
|
|
70
|
+
if (EXPRESSION_ASSERTION_TYPES.has(current.type)) {
|
|
71
|
+
const inner = current
|
|
72
|
+
.expression;
|
|
73
|
+
if (!inner)
|
|
74
|
+
break;
|
|
75
|
+
current = inner;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return current;
|
|
81
|
+
};
|
|
46
82
|
const unwrapArrayElementType = (node) => {
|
|
47
83
|
let current = node;
|
|
48
84
|
// Fixpoint loop: peel wrappers in any order until none remain
|
|
@@ -181,6 +217,7 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
181
217
|
const aliasNameToType = new Map();
|
|
182
218
|
const interfaceNames = new Set();
|
|
183
219
|
const enumNames = new Set();
|
|
220
|
+
const constArrayNameToLiteral = new Map();
|
|
184
221
|
const visitNode = (n) => {
|
|
185
222
|
switch (n.type) {
|
|
186
223
|
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration: {
|
|
@@ -195,6 +232,22 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
195
232
|
enumNames.add(n.id.name);
|
|
196
233
|
break;
|
|
197
234
|
}
|
|
235
|
+
case utils_1.AST_NODE_TYPES.VariableDeclaration: {
|
|
236
|
+
// Only `const` bindings can back a `(typeof X)[number]` element union
|
|
237
|
+
if (n.kind !== 'const')
|
|
238
|
+
break;
|
|
239
|
+
for (const declarator of n.declarations) {
|
|
240
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
241
|
+
!declarator.init) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const init = unwrapExpressionAssertions(declarator.init);
|
|
245
|
+
if (init.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
246
|
+
constArrayNameToLiteral.set(declarator.id.name, init);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
198
251
|
case utils_1.AST_NODE_TYPES.ExportNamedDeclaration: {
|
|
199
252
|
if (n.declaration)
|
|
200
253
|
visitNode(n.declaration);
|
|
@@ -235,6 +288,85 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
235
288
|
}
|
|
236
289
|
const seenAlias = new Set();
|
|
237
290
|
const visitingAliases = new Set();
|
|
291
|
+
const isPrimitiveLiteralElement = (element, visitedConstArrays) => {
|
|
292
|
+
// Array holes resolve to `undefined`, but the shape is unusual enough
|
|
293
|
+
// that refusing to classify it keeps the narrowing conservative.
|
|
294
|
+
if (!element)
|
|
295
|
+
return false;
|
|
296
|
+
if (element.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
297
|
+
const argument = unwrapExpressionAssertions(element.argument);
|
|
298
|
+
if (argument.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
299
|
+
return argument.elements.every((nested) => isPrimitiveLiteralElement(nested, visitedConstArrays));
|
|
300
|
+
}
|
|
301
|
+
if (argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
302
|
+
return isPrimitiveConstArray(argument.name, visitedConstArrays);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const expression = unwrapExpressionAssertions(element);
|
|
307
|
+
switch (expression.type) {
|
|
308
|
+
case utils_1.AST_NODE_TYPES.Literal: {
|
|
309
|
+
// A regex literal is an object; its `value` is engine-dependent, so
|
|
310
|
+
// reject it explicitly rather than relying on the typeof check.
|
|
311
|
+
if (expression.regex)
|
|
312
|
+
return false;
|
|
313
|
+
const value = expression.value;
|
|
314
|
+
return (value === null ||
|
|
315
|
+
typeof value === 'string' ||
|
|
316
|
+
typeof value === 'number' ||
|
|
317
|
+
typeof value === 'boolean' ||
|
|
318
|
+
typeof value === 'bigint');
|
|
319
|
+
}
|
|
320
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
321
|
+
return true; // a template literal always produces a string
|
|
322
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
323
|
+
// Nested primitive arrays mirror the allowance for `string[][]` and
|
|
324
|
+
// tuples of primitives elsewhere in this rule.
|
|
325
|
+
return expression.elements.every((nested) => isPrimitiveLiteralElement(nested, visitedConstArrays));
|
|
326
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression: {
|
|
327
|
+
const unary = expression;
|
|
328
|
+
if (unary.operator !== '-' && unary.operator !== '+')
|
|
329
|
+
return false;
|
|
330
|
+
return isPrimitiveLiteralElement(unary.argument, visitedConstArrays);
|
|
331
|
+
}
|
|
332
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
333
|
+
return expression.name === 'undefined';
|
|
334
|
+
default:
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const isPrimitiveConstArray = (name, visitedConstArrays) => {
|
|
339
|
+
// A cyclic spread cannot be resolved syntactically; refuse to classify it
|
|
340
|
+
if (visitedConstArrays.has(name))
|
|
341
|
+
return false;
|
|
342
|
+
const arrayLiteral = constArrayNameToLiteral.get(name);
|
|
343
|
+
if (!arrayLiteral)
|
|
344
|
+
return false;
|
|
345
|
+
visitedConstArrays.add(name);
|
|
346
|
+
const result = arrayLiteral.elements.every((element) => isPrimitiveLiteralElement(element, visitedConstArrays));
|
|
347
|
+
visitedConstArrays.delete(name);
|
|
348
|
+
return result;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* Recognizes `(typeof VALUES)[number]` where VALUES is a same-file const
|
|
352
|
+
* array of primitive literals. That form denotes the union of those
|
|
353
|
+
* literals, not an object lookup, and it is exactly what the sibling
|
|
354
|
+
* rule prefer-union-from-const-array autofixes toward.
|
|
355
|
+
*/
|
|
356
|
+
const isConstArrayElementUnion = (node) => {
|
|
357
|
+
const indexType = unwrapParenthesizedTypeNode(node.indexType);
|
|
358
|
+
// Only a `number` index yields the element union; `['length']` or any key
|
|
359
|
+
// lookup resolves to something this syntactic check cannot vouch for.
|
|
360
|
+
if (indexType.type !== utils_1.AST_NODE_TYPES.TSNumberKeyword)
|
|
361
|
+
return false;
|
|
362
|
+
const objectType = unwrapParenthesizedTypeNode(node.objectType);
|
|
363
|
+
if (objectType.type !== utils_1.AST_NODE_TYPES.TSTypeQuery)
|
|
364
|
+
return false;
|
|
365
|
+
const exprName = objectType.exprName;
|
|
366
|
+
if (exprName.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
367
|
+
return false;
|
|
368
|
+
return isPrimitiveConstArray(exprName.name, new Set());
|
|
369
|
+
};
|
|
238
370
|
const isPrimitiveLikeAlias = (name, recursionDepth) => {
|
|
239
371
|
if (seenAlias.has(name))
|
|
240
372
|
return true;
|
|
@@ -286,6 +418,8 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
286
418
|
return false;
|
|
287
419
|
case utils_1.AST_NODE_TYPES.TSLiteralType:
|
|
288
420
|
return true; // string/number/boolean literals
|
|
421
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
422
|
+
return isConstArrayElementUnion(node);
|
|
289
423
|
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
290
424
|
// Allow known primitive-like references and enums or primitive-like aliases
|
|
291
425
|
const ref = node;
|
|
@@ -361,8 +495,10 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
361
495
|
case utils_1.AST_NODE_TYPES.TSMappedType:
|
|
362
496
|
return true;
|
|
363
497
|
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
364
|
-
//
|
|
365
|
-
|
|
498
|
+
// An indexed access such as `DataShape['user']` is an object lookup,
|
|
499
|
+
// but `(typeof VALUES)[number]` over a const array of primitive
|
|
500
|
+
// literals is a primitive union and must not be flagged.
|
|
501
|
+
return !isConstArrayElementUnion(node);
|
|
366
502
|
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
367
503
|
if (node.operator === 'readonly') {
|
|
368
504
|
return isObjectType(node
|
|
@@ -381,6 +381,22 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
381
381
|
valueKind,
|
|
382
382
|
},
|
|
383
383
|
fix(fixer) {
|
|
384
|
+
// Inlining replaces the entire useMemo(...) call with the returned
|
|
385
|
+
// expression's text, so any comment inside the call but outside
|
|
386
|
+
// that expression — an eslint-disable-next-line directive on the
|
|
387
|
+
// return statement among them — has no representation in the
|
|
388
|
+
// replacement and would be silently destroyed, changing which
|
|
389
|
+
// rules report on the file (#1591). The inlined expression lands
|
|
390
|
+
// mid-line (e.g. `const label = <expr>;`), where a -next-line
|
|
391
|
+
// directive cannot be hosted, so the autofix declines and leaves
|
|
392
|
+
// the report for a manual fix.
|
|
393
|
+
const strandedComments = sourceCode
|
|
394
|
+
.getCommentsInside(node)
|
|
395
|
+
.filter((comment) => comment.range[0] < returnedExpression.range[0] ||
|
|
396
|
+
comment.range[1] > returnedExpression.range[1]);
|
|
397
|
+
if (strandedComments.length > 0) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
384
400
|
const replacement = `(${sourceCode.getText(returnedExpression)})`;
|
|
385
401
|
return fixer.replaceText(node, replacement);
|
|
386
402
|
},
|
|
@@ -785,6 +785,61 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
785
785
|
return null;
|
|
786
786
|
}
|
|
787
787
|
const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
|
|
788
|
+
const startPos = awaitNodes[0].range[0];
|
|
789
|
+
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
790
|
+
// The replacement text is rebuilt from the awaited expressions alone, so
|
|
791
|
+
// a comment inside the replaced span has no representation in it and
|
|
792
|
+
// would be silently deleted. Deleting an eslint-disable-next-line
|
|
793
|
+
// directive re-enables the suppressed rule on the code that survives
|
|
794
|
+
// inside the Promise.all (#1589). Each span comment is therefore either
|
|
795
|
+
// re-hosted directly above the array element built from the statement it
|
|
796
|
+
// annotates, or the fix is declined so no comment is ever destroyed. The
|
|
797
|
+
// report fires either way.
|
|
798
|
+
const spanComments = sourceCode
|
|
799
|
+
.getAllComments()
|
|
800
|
+
.filter((comment) => comment.range[0] >= startPos && comment.range[1] <= endPos);
|
|
801
|
+
const hostedComments = awaitNodes.map(() => []);
|
|
802
|
+
if (spanComments.length > 0) {
|
|
803
|
+
// Re-hosting maps the comments preceding statement i onto element i,
|
|
804
|
+
// which requires the element list to line up 1:1 with the statement
|
|
805
|
+
// list.
|
|
806
|
+
if (awaitExpressions.length !== awaitNodes.length) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
for (const comment of spanComments) {
|
|
810
|
+
const hostIndex = awaitNodes.findIndex((node, index) => index > 0 &&
|
|
811
|
+
comment.range[0] >= awaitNodes[index - 1].range[1] &&
|
|
812
|
+
comment.range[1] <= node.range[0]);
|
|
813
|
+
if (hostIndex === -1) {
|
|
814
|
+
// The comment sits inside one of the merged statements. A comment
|
|
815
|
+
// within the awaited expression itself travels verbatim with
|
|
816
|
+
// getText; anywhere else (between `await` and its operand, or
|
|
817
|
+
// around a declarator's `=`) has no slot in the rebuilt text.
|
|
818
|
+
const isInsideArgument = awaitExpressions.some((expr) => comment.range[0] >= expr.argument.range[0] &&
|
|
819
|
+
comment.range[1] <= expr.argument.range[1]);
|
|
820
|
+
if (!isInsideArgument) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
// A comment that shares the previous statement's last line is a
|
|
826
|
+
// trailing comment (e.g. an eslint-disable-line directive) governing
|
|
827
|
+
// THAT line; moving it above the next element would change which
|
|
828
|
+
// line it applies to.
|
|
829
|
+
if (comment.loc.start.line <= awaitNodes[hostIndex - 1].loc.end.line) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
// A directive above `const x = await f();` may target the
|
|
833
|
+
// declaration's identifier, which the rewrite moves into the
|
|
834
|
+
// destructuring pattern on the Promise.all line — away from every
|
|
835
|
+
// line the re-hosted directive could govern.
|
|
836
|
+
if (/^\s*eslint-/u.test(comment.value) &&
|
|
837
|
+
awaitNodes[hostIndex].type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
hostedComments[hostIndex].push(comment);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
788
843
|
const idsText = [];
|
|
789
844
|
const declKinds = new Set();
|
|
790
845
|
let hasVariableDeclarations = false;
|
|
@@ -817,7 +872,17 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
817
872
|
// the contents of a template literal, where whitespace is significant
|
|
818
873
|
// data rather than formatting and re-indenting would silently change the
|
|
819
874
|
// produced string.
|
|
820
|
-
const elementsText = awaitArguments
|
|
875
|
+
const elementsText = awaitArguments
|
|
876
|
+
.map((argumentText, index) => {
|
|
877
|
+
// Each re-hosted comment lands on its own line directly above the
|
|
878
|
+
// element, which is the only placement where a disable-next-line
|
|
879
|
+
// directive keeps suppressing it.
|
|
880
|
+
const leadingText = (hostedComments[index] ?? [])
|
|
881
|
+
.map((comment) => `${sourceCode.getText(comment)}\n${elementIndent}`)
|
|
882
|
+
.join('');
|
|
883
|
+
return `${leadingText}${argumentText}`;
|
|
884
|
+
})
|
|
885
|
+
.join(`,\n${elementIndent}`);
|
|
821
886
|
let promiseAllText;
|
|
822
887
|
if (hasVariableDeclarations) {
|
|
823
888
|
if (declKinds.size !== 1) {
|
|
@@ -834,8 +899,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
834
899
|
// Simple Promise.all without variable assignments
|
|
835
900
|
promiseAllText = `await Promise.all([\n${elementIndent}${elementsText}\n${baseIndent}]);`;
|
|
836
901
|
}
|
|
837
|
-
const startPos = awaitNodes[0].range[0];
|
|
838
|
-
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
839
902
|
return fixer.replaceTextRange([startPos, endPos], promiseAllText);
|
|
840
903
|
}
|
|
841
904
|
/**
|
|
@@ -62,6 +62,37 @@ function parenthesizeForUnion(text) {
|
|
|
62
62
|
const needsParens = text.includes('=>') || /^new\b/.test(text) || /\bextends\b/.test(text);
|
|
63
63
|
return needsParens ? `(${text})` : text;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* How position-sensitive a comment is when the fix relocates it onto the
|
|
67
|
+
* generated Record:
|
|
68
|
+
*
|
|
69
|
+
* - `next-line`: suppresses exactly the following line
|
|
70
|
+
* (`eslint-disable-next-line`, `@ts-expect-error`, `@ts-ignore`) — hosting
|
|
71
|
+
* it anywhere but directly above the line it suppressed silently changes
|
|
72
|
+
* which rules report.
|
|
73
|
+
* - `own-line`: suppresses its own line (`eslint-disable-line`) — must stay on
|
|
74
|
+
* the same line as the value it annotates.
|
|
75
|
+
* - `range`: opens/closes a suppression region or configures the linter
|
|
76
|
+
* (`eslint-disable`, `eslint-enable`, `eslint-env`, `eslint`, `global`,
|
|
77
|
+
* `exported`) — ESLint honors these only in block-comment form, and
|
|
78
|
+
* relocation can move the region boundary across reported lines, so the fix
|
|
79
|
+
* is never attempted around one.
|
|
80
|
+
* - `none`: prose; content preservation suffices.
|
|
81
|
+
*/
|
|
82
|
+
function directiveKindOf(comment) {
|
|
83
|
+
const text = comment.value.trim();
|
|
84
|
+
if (/^(eslint-disable-next-line|@ts-expect-error|@ts-ignore)\b/.test(text)) {
|
|
85
|
+
return 'next-line';
|
|
86
|
+
}
|
|
87
|
+
if (/^eslint-disable-line\b/.test(text)) {
|
|
88
|
+
return 'own-line';
|
|
89
|
+
}
|
|
90
|
+
if (comment.type === utils_1.AST_TOKEN_TYPES.Block &&
|
|
91
|
+
/^(eslint-disable|eslint-enable|eslint-env|eslint\b|globals?\b|exported\b)/.test(text)) {
|
|
92
|
+
return 'range';
|
|
93
|
+
}
|
|
94
|
+
return 'none';
|
|
95
|
+
}
|
|
65
96
|
exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
66
97
|
name: 'prefer-map-over-conditional-dispatch',
|
|
67
98
|
meta: {
|
|
@@ -360,7 +391,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
360
391
|
}
|
|
361
392
|
const stmt = stmts[0];
|
|
362
393
|
if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
|
|
363
|
-
return { kind: 'return', expr: stmt.argument };
|
|
394
|
+
return { kind: 'return', expr: stmt.argument, stmt };
|
|
364
395
|
}
|
|
365
396
|
if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
|
|
366
397
|
stmt.expression.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
@@ -369,6 +400,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
369
400
|
kind: 'assign',
|
|
370
401
|
target: stmt.expression.left,
|
|
371
402
|
expr: stmt.expression.right,
|
|
403
|
+
stmt,
|
|
372
404
|
};
|
|
373
405
|
}
|
|
374
406
|
return null;
|
|
@@ -504,13 +536,92 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
504
536
|
return match ? match[0] : '';
|
|
505
537
|
}
|
|
506
538
|
function buildRecordText(name, dText, vText, entries, baseIndent) {
|
|
507
|
-
const lines = entries.
|
|
539
|
+
const lines = entries.flatMap((e) => {
|
|
540
|
+
const hosted = (e.leadingComments ?? []).map((comment) => `${baseIndent} ${comment}`);
|
|
541
|
+
const trailing = e.trailingComments && e.trailingComments.length > 0
|
|
542
|
+
? ` ${e.trailingComments.join(' ')}`
|
|
543
|
+
: '';
|
|
544
|
+
hosted.push(`${baseIndent} ${formatKey(e.key)}: ${e.valueText},${trailing}`);
|
|
545
|
+
return hosted;
|
|
546
|
+
});
|
|
508
547
|
return [
|
|
509
548
|
`const ${name}: Record<${dText}, ${vText}> = {`,
|
|
510
549
|
...lines,
|
|
511
550
|
`${baseIndent}};`,
|
|
512
551
|
].join('\n');
|
|
513
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Decides where every comment inside the replaced construct lands on the
|
|
555
|
+
* generated Record, mutating `entries` with the hosted text. Comments
|
|
556
|
+
* inside a carried span (a branch value expression, the discriminant, the
|
|
557
|
+
* assignment target) survive verbatim inside the copied text; comments
|
|
558
|
+
* inside a dropped span (an unreachable default/tail) die together with
|
|
559
|
+
* the code they annotate, which is not an orphaning. Every other comment
|
|
560
|
+
* must be hosted onto a map entry — leading comments go on the line(s)
|
|
561
|
+
* directly above the entry, same-line trailing comments append after it.
|
|
562
|
+
* Returns false when a comment cannot be hosted without changing what it
|
|
563
|
+
* annotates or suppresses; the caller then withholds the autofix so a
|
|
564
|
+
* directive is never silently deleted or retargeted.
|
|
565
|
+
*/
|
|
566
|
+
function planCommentHosting(args) {
|
|
567
|
+
const { container, carriedSpans, droppedSpans, anchors, entries } = args;
|
|
568
|
+
const ordered = [...anchors].sort((a, b) => a.range[0] - b.range[0]);
|
|
569
|
+
const hostLeading = (entry, text) => {
|
|
570
|
+
entry.leadingComments = [...(entry.leadingComments ?? []), text];
|
|
571
|
+
};
|
|
572
|
+
const hostTrailing = (entry, text) => {
|
|
573
|
+
entry.trailingComments = [...(entry.trailingComments ?? []), text];
|
|
574
|
+
};
|
|
575
|
+
for (const comment of sourceCode.getCommentsInside(container)) {
|
|
576
|
+
const [cStart, cEnd] = comment.range;
|
|
577
|
+
const within = (spans) => spans.some(([start, end]) => cStart >= start && cEnd <= end);
|
|
578
|
+
if (within(carriedSpans) || within(droppedSpans)) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const kind = directiveKindOf(comment);
|
|
582
|
+
if (kind === 'range') {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const text = sourceCode.getText(comment);
|
|
586
|
+
// Same-line trailing (`doThing(); // note`): append to the entry line
|
|
587
|
+
// so an `eslint-disable-line` keeps suppressing the line its value
|
|
588
|
+
// lands on. The last matching anchor wins so a comment trailing two
|
|
589
|
+
// same-line statements attaches to the nearer one.
|
|
590
|
+
const trailingCandidates = ordered.filter((anchor) => anchor.endLine === comment.loc.start.line &&
|
|
591
|
+
cStart >= anchor.range[1]);
|
|
592
|
+
const trailingAnchor = trailingCandidates[trailingCandidates.length - 1];
|
|
593
|
+
if (trailingAnchor) {
|
|
594
|
+
if (kind === 'next-line') {
|
|
595
|
+
// A trailing disable-next-line targets whatever follows the
|
|
596
|
+
// statement; the generated map has no equivalent following line.
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
if (kind === 'own-line' && trailingAnchor.entryCount > 1) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
hostTrailing(entries[trailingAnchor.entryIndex], text);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// A comment inside the value statement but outside the copied
|
|
606
|
+
// expression (`return /* why */ value;`) belongs to that branch.
|
|
607
|
+
const containingAnchor = ordered.find((anchor) => cStart >= anchor.range[0] && cEnd <= anchor.range[1]);
|
|
608
|
+
const leadingAnchor = containingAnchor ?? ordered.find((anchor) => anchor.range[0] >= cEnd);
|
|
609
|
+
if (!leadingAnchor) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (kind === 'next-line') {
|
|
613
|
+
// Host a line-targeted directive only when it provably targeted the
|
|
614
|
+
// branch's value line, so relocation preserves — never widens or
|
|
615
|
+
// drops — the suppression.
|
|
616
|
+
if (leadingAnchor.entryCount > 1 ||
|
|
617
|
+
leadingAnchor.startLine !== comment.loc.end.line + 1) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
hostLeading(entries[leadingAnchor.entryIndex], text);
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
514
625
|
/**
|
|
515
626
|
* Given ordered explicit branches + optional tail, resolve coverage against
|
|
516
627
|
* the union's literal keys and (when full) the ordered Record entries.
|
|
@@ -564,20 +675,28 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
564
675
|
if (!flags.canPlaceFix) {
|
|
565
676
|
return 'the dispatch sits inside an expression-bodied function; extract the Record manually so it stays in scope';
|
|
566
677
|
}
|
|
678
|
+
if (!flags.commentSafe) {
|
|
679
|
+
return 'a comment inside the dispatch cannot be carried onto the generated Record without changing what it annotates or suppresses — relocate the comment, then convert';
|
|
680
|
+
}
|
|
567
681
|
return 'a collision-free lookup name could not be derived from the discriminant';
|
|
568
682
|
}
|
|
569
683
|
function report(node, analysis) {
|
|
570
|
-
const { entries, contributingValues, dText, form, assignTargetText, fullCoverage, hasNullish, canPlaceFix, } = analysis;
|
|
684
|
+
const { entries, contributingValues, dText, form, assignTargetText, fullCoverage, hasNullish, canPlaceFix, commentBlocked, } = analysis;
|
|
571
685
|
const eagerSafe = contributingValues.every((expr) => !containsEagerUnsafe(expr));
|
|
572
686
|
let name = null;
|
|
573
687
|
// Name derivation is only needed for the autofix path.
|
|
574
|
-
if (fullCoverage &&
|
|
688
|
+
if (fullCoverage &&
|
|
689
|
+
!hasNullish &&
|
|
690
|
+
eagerSafe &&
|
|
691
|
+
canPlaceFix &&
|
|
692
|
+
!commentBlocked) {
|
|
575
693
|
name = deriveLookupName(discriminantOf(node));
|
|
576
694
|
}
|
|
577
695
|
const autofixable = fullCoverage &&
|
|
578
696
|
!hasNullish &&
|
|
579
697
|
eagerSafe &&
|
|
580
698
|
canPlaceFix &&
|
|
699
|
+
!commentBlocked &&
|
|
581
700
|
name !== null;
|
|
582
701
|
if (!autofixable) {
|
|
583
702
|
context.report({
|
|
@@ -589,6 +708,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
589
708
|
hasNullish,
|
|
590
709
|
eagerSafe,
|
|
591
710
|
canPlaceFix,
|
|
711
|
+
commentSafe: !commentBlocked,
|
|
592
712
|
}),
|
|
593
713
|
},
|
|
594
714
|
});
|
|
@@ -700,7 +820,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
700
820
|
continue;
|
|
701
821
|
}
|
|
702
822
|
const value = extractBranchValue(c.consequent);
|
|
703
|
-
parsed.push({ tests: [...pending, c.test], value });
|
|
823
|
+
parsed.push({ tests: [...pending, c.test], value, caseNode: c });
|
|
704
824
|
pending = [];
|
|
705
825
|
}
|
|
706
826
|
if (pending.length > 0) {
|
|
@@ -714,6 +834,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
714
834
|
const explicit = [];
|
|
715
835
|
let defaultValue;
|
|
716
836
|
let hasDefault = false;
|
|
837
|
+
let defaultCaseNode = null;
|
|
717
838
|
for (const branch of parsed) {
|
|
718
839
|
const literalTests = branch.tests.filter((t) => t !== null);
|
|
719
840
|
const isDefaultGroup = literalTests.length !== branch.tests.length;
|
|
@@ -724,6 +845,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
724
845
|
}
|
|
725
846
|
hasDefault = true;
|
|
726
847
|
defaultValue = branch.value;
|
|
848
|
+
defaultCaseNode = branch.caseNode;
|
|
727
849
|
continue;
|
|
728
850
|
}
|
|
729
851
|
if (!branch.value) {
|
|
@@ -798,6 +920,57 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
798
920
|
if (isNarrowingExempt(node.discriminant, contributingValues)) {
|
|
799
921
|
return;
|
|
800
922
|
}
|
|
923
|
+
// Comment hosting only gates the autofix path (partial coverage is
|
|
924
|
+
// already report-only), so it is planned for full coverage alone.
|
|
925
|
+
let commentBlocked = false;
|
|
926
|
+
if (coverage.fullCoverage) {
|
|
927
|
+
const tailUsed = coverage.remainingCount === 1 && defaultVal !== null;
|
|
928
|
+
const carriedSpans = [node.discriminant.range];
|
|
929
|
+
const droppedSpans = [];
|
|
930
|
+
const anchors = [];
|
|
931
|
+
let entryIndex = 0;
|
|
932
|
+
for (const branch of explicit) {
|
|
933
|
+
carriedSpans.push(branch.value.expr.range);
|
|
934
|
+
anchors.push({
|
|
935
|
+
range: branch.value.stmt.range,
|
|
936
|
+
startLine: branch.value.stmt.loc.start.line,
|
|
937
|
+
endLine: branch.value.stmt.loc.end.line,
|
|
938
|
+
entryIndex,
|
|
939
|
+
entryCount: branch.keys.length,
|
|
940
|
+
});
|
|
941
|
+
entryIndex += branch.keys.length;
|
|
942
|
+
}
|
|
943
|
+
if (kind === 'assign' && explicit[0].value.kind === 'assign') {
|
|
944
|
+
carriedSpans.push(explicit[0].value.target.range);
|
|
945
|
+
}
|
|
946
|
+
if (tailUsed && defaultVal) {
|
|
947
|
+
carriedSpans.push(defaultVal.expr.range);
|
|
948
|
+
anchors.push({
|
|
949
|
+
range: defaultVal.stmt.range,
|
|
950
|
+
startLine: defaultVal.stmt.loc.start.line,
|
|
951
|
+
endLine: defaultVal.stmt.loc.end.line,
|
|
952
|
+
entryIndex: coverage.entries.length - 1,
|
|
953
|
+
entryCount: 1,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
else if (defaultCaseNode) {
|
|
957
|
+
// The unreachable default is deleted wholesale; its comments —
|
|
958
|
+
// including any directly above the `default:` label — annotate
|
|
959
|
+
// deleted code and die with it.
|
|
960
|
+
const caseIndex = node.cases.indexOf(defaultCaseNode);
|
|
961
|
+
const droppedStart = caseIndex > 0
|
|
962
|
+
? node.cases[caseIndex - 1].range[1]
|
|
963
|
+
: node.discriminant.range[1];
|
|
964
|
+
droppedSpans.push([droppedStart, defaultCaseNode.range[1]]);
|
|
965
|
+
}
|
|
966
|
+
commentBlocked = !planCommentHosting({
|
|
967
|
+
container: node,
|
|
968
|
+
carriedSpans,
|
|
969
|
+
droppedSpans,
|
|
970
|
+
anchors,
|
|
971
|
+
entries: coverage.entries,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
801
974
|
report(node, {
|
|
802
975
|
entries: coverage.entries,
|
|
803
976
|
contributingValues,
|
|
@@ -807,6 +980,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
807
980
|
fullCoverage: coverage.fullCoverage,
|
|
808
981
|
hasNullish,
|
|
809
982
|
canPlaceFix: true,
|
|
983
|
+
commentBlocked,
|
|
810
984
|
});
|
|
811
985
|
}
|
|
812
986
|
// ---- Ternary form -------------------------------------------------------
|
|
@@ -906,6 +1080,47 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
906
1080
|
crossesFunction = true;
|
|
907
1081
|
}
|
|
908
1082
|
}
|
|
1083
|
+
let commentBlocked = false;
|
|
1084
|
+
if (coverage.fullCoverage) {
|
|
1085
|
+
const tailUsed = coverage.remainingCount === 1;
|
|
1086
|
+
const carriedSpans = [
|
|
1087
|
+
head.discNode.range,
|
|
1088
|
+
...links.map((l) => l.expr.range),
|
|
1089
|
+
];
|
|
1090
|
+
const droppedSpans = [];
|
|
1091
|
+
const anchors = links.map((l, i) => ({
|
|
1092
|
+
range: l.expr.range,
|
|
1093
|
+
startLine: l.expr.loc.start.line,
|
|
1094
|
+
endLine: l.expr.loc.end.line,
|
|
1095
|
+
entryIndex: i,
|
|
1096
|
+
entryCount: 1,
|
|
1097
|
+
}));
|
|
1098
|
+
if (tailUsed) {
|
|
1099
|
+
carriedSpans.push(tailExpr.range);
|
|
1100
|
+
anchors.push({
|
|
1101
|
+
range: tailExpr.range,
|
|
1102
|
+
startLine: tailExpr.loc.start.line,
|
|
1103
|
+
endLine: tailExpr.loc.end.line,
|
|
1104
|
+
entryIndex: coverage.entries.length - 1,
|
|
1105
|
+
entryCount: 1,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
// The unreachable tail is deleted; comments between the last kept
|
|
1110
|
+
// consequent and the tail's end annotate deleted code.
|
|
1111
|
+
droppedSpans.push([
|
|
1112
|
+
links[links.length - 1].expr.range[1],
|
|
1113
|
+
tailExpr.range[1],
|
|
1114
|
+
]);
|
|
1115
|
+
}
|
|
1116
|
+
commentBlocked = !planCommentHosting({
|
|
1117
|
+
container: node,
|
|
1118
|
+
carriedSpans,
|
|
1119
|
+
droppedSpans,
|
|
1120
|
+
anchors,
|
|
1121
|
+
entries: coverage.entries,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
909
1124
|
discriminantMap.set(node, head.discNode);
|
|
910
1125
|
report(node, {
|
|
911
1126
|
entries: coverage.entries,
|
|
@@ -915,6 +1130,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
915
1130
|
fullCoverage: coverage.fullCoverage,
|
|
916
1131
|
hasNullish,
|
|
917
1132
|
canPlaceFix: !crossesFunction,
|
|
1133
|
+
commentBlocked,
|
|
918
1134
|
});
|
|
919
1135
|
}
|
|
920
1136
|
// ---- if / else-if form --------------------------------------------------
|
|
@@ -935,6 +1151,10 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
935
1151
|
}
|
|
936
1152
|
const links = [];
|
|
937
1153
|
let tail = null;
|
|
1154
|
+
// The final else statement/block and the end of the consequent before
|
|
1155
|
+
// it — the source region a dropped tail's comments die with.
|
|
1156
|
+
let tailNode = null;
|
|
1157
|
+
let tailPrevEnd = 0;
|
|
938
1158
|
let cur = node;
|
|
939
1159
|
while (cur) {
|
|
940
1160
|
const link = equalityDiscriminant(cur.test);
|
|
@@ -960,6 +1180,8 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
960
1180
|
return;
|
|
961
1181
|
}
|
|
962
1182
|
tail = tailValue;
|
|
1183
|
+
tailNode = alt;
|
|
1184
|
+
tailPrevEnd = cur.consequent.range[1];
|
|
963
1185
|
cur = null;
|
|
964
1186
|
}
|
|
965
1187
|
if (links.length === 0) {
|
|
@@ -1012,6 +1234,48 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1012
1234
|
if (isNarrowingExempt(head.discNode, contributingValues)) {
|
|
1013
1235
|
return;
|
|
1014
1236
|
}
|
|
1237
|
+
let commentBlocked = false;
|
|
1238
|
+
if (coverage.fullCoverage) {
|
|
1239
|
+
const tailUsed = coverage.remainingCount === 1 && tail !== null;
|
|
1240
|
+
const carriedSpans = [head.discNode.range];
|
|
1241
|
+
const droppedSpans = [];
|
|
1242
|
+
const anchors = [];
|
|
1243
|
+
links.forEach((link, i) => {
|
|
1244
|
+
carriedSpans.push(link.value.expr.range);
|
|
1245
|
+
anchors.push({
|
|
1246
|
+
range: link.value.stmt.range,
|
|
1247
|
+
startLine: link.value.stmt.loc.start.line,
|
|
1248
|
+
endLine: link.value.stmt.loc.end.line,
|
|
1249
|
+
entryIndex: i,
|
|
1250
|
+
entryCount: 1,
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
if (kind === 'assign' && links[0].value.kind === 'assign') {
|
|
1254
|
+
carriedSpans.push(links[0].value.target.range);
|
|
1255
|
+
}
|
|
1256
|
+
if (tailUsed && tail) {
|
|
1257
|
+
carriedSpans.push(tail.expr.range);
|
|
1258
|
+
anchors.push({
|
|
1259
|
+
range: tail.stmt.range,
|
|
1260
|
+
startLine: tail.stmt.loc.start.line,
|
|
1261
|
+
endLine: tail.stmt.loc.end.line,
|
|
1262
|
+
entryIndex: coverage.entries.length - 1,
|
|
1263
|
+
entryCount: 1,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
else if (tailNode) {
|
|
1267
|
+
// The unreachable else is deleted; comments from the last kept
|
|
1268
|
+
// consequent through the else's end annotate deleted code.
|
|
1269
|
+
droppedSpans.push([tailPrevEnd, tailNode.range[1]]);
|
|
1270
|
+
}
|
|
1271
|
+
commentBlocked = !planCommentHosting({
|
|
1272
|
+
container: node,
|
|
1273
|
+
carriedSpans,
|
|
1274
|
+
droppedSpans,
|
|
1275
|
+
anchors,
|
|
1276
|
+
entries: coverage.entries,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1015
1279
|
discriminantMap.set(node, head.discNode);
|
|
1016
1280
|
report(node, {
|
|
1017
1281
|
entries: coverage.entries,
|
|
@@ -1022,6 +1286,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1022
1286
|
fullCoverage: coverage.fullCoverage,
|
|
1023
1287
|
hasNullish,
|
|
1024
1288
|
canPlaceFix: true,
|
|
1289
|
+
commentBlocked,
|
|
1025
1290
|
});
|
|
1026
1291
|
}
|
|
1027
1292
|
return {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.71",
|
|
4
|
+
"date": "2026-08-02T01:25:22.489Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-firestore-object-arrays",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1594
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat (typeof X)[number] over a primitive const array as a primitive union (closes #1594)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.70",
|
|
18
|
+
"date": "2026-08-01T23:31:02.214Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "class-methods-read-top-to-bottom",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1592
|
|
25
|
+
],
|
|
26
|
+
"summary": "preserve the whitespace separating reordered class members (closes #1592)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "no-useless-usememo-primitives",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1591
|
|
33
|
+
],
|
|
34
|
+
"summary": "decline the autofix when inlining would strand a comment (closes #1591)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "parallelize-async-operations",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1589
|
|
41
|
+
],
|
|
42
|
+
"summary": "preserve comments between merged awaits (closes #1589)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "prefer-map-over-conditional-dispatch",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1590
|
|
49
|
+
],
|
|
50
|
+
"summary": "preserve branch comments in the generated map (closes #1590)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.69",
|
|
4
56
|
"date": "2026-08-01T20:48:40.484Z",
|