@blumintinc/eslint-plugin-blumint 1.20.141 → 1.20.142
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/no-explicit-return-type.js +120 -2
- package/lib/rules/no-usememo-for-pass-by-value.js +8 -6
- package/lib/utils/harvestRuleTesterCases.js +25 -5
- package/lib/utils/replacementSegments.d.ts +19 -0
- package/lib/utils/replacementSegments.js +30 -1
- package/package.json +1 -1
- package/release-manifest.json +22 -0
package/lib/index.js
CHANGED
|
@@ -846,6 +846,115 @@ function carriedText(source, range) {
|
|
|
846
846
|
: ' ';
|
|
847
847
|
return `${lead}${body}${trail}`;
|
|
848
848
|
}
|
|
849
|
+
/** Every character the syntactic grammar counts as a LineTerminator. */
|
|
850
|
+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
|
851
|
+
const textOf = (source, range) => source.text.slice(range[0], range[1]);
|
|
852
|
+
/**
|
|
853
|
+
* The span an arrow's return annotation occupies between the parameter list and
|
|
854
|
+
* the `=>`, together with that arrow token.
|
|
855
|
+
*
|
|
856
|
+
* The span holds the annotation, whitespace and comments and nothing else,
|
|
857
|
+
* which is what makes it safe to rewrite wholesale: no binding reference can
|
|
858
|
+
* hide in it beyond the ones the annotation itself names.
|
|
859
|
+
*/
|
|
860
|
+
function arrowAnnotationGap(source, returnType) {
|
|
861
|
+
const parametersEnd = source.getTokenBefore(returnType);
|
|
862
|
+
const arrow = source.getTokenAfter(returnType, {
|
|
863
|
+
filter: (token) => token.value === '=>',
|
|
864
|
+
});
|
|
865
|
+
if (!parametersEnd || !arrow)
|
|
866
|
+
return null;
|
|
867
|
+
const gap = [parametersEnd.range[1], arrow.range[0]];
|
|
868
|
+
return containsRange(gap, returnType.range) ? { gap, arrow } : null;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Re-emits `comments` on the far side of the arrow, where a line terminator is
|
|
872
|
+
* inert, consuming the horizontal whitespace the arrow already had after it so
|
|
873
|
+
* the body keeps a single separator.
|
|
874
|
+
*/
|
|
875
|
+
function hoistPastArrow(source, arrow, comments) {
|
|
876
|
+
const indent = indentAt(source, arrow.range[0]);
|
|
877
|
+
const trailingText = source.text.slice(arrow.range[1]);
|
|
878
|
+
const [spacing] = /^[ \t]*/.exec(trailingText) ?? [''];
|
|
879
|
+
const body = (0, replacementSegments_1.joinSegmentBody)(comments.map((comment) => ({
|
|
880
|
+
text: textOf(source, comment.range),
|
|
881
|
+
breakAfter: true,
|
|
882
|
+
})), indent);
|
|
883
|
+
const rest = trailingText.slice(spacing.length);
|
|
884
|
+
const separator = LINE_TERMINATOR.test(rest.charAt(0))
|
|
885
|
+
? ''
|
|
886
|
+
: (0, replacementSegments_1.requiresLineBreakAfter)(comments[comments.length - 1])
|
|
887
|
+
? `\n${indent}`
|
|
888
|
+
: ' ';
|
|
889
|
+
return {
|
|
890
|
+
range: [arrow.range[1], arrow.range[1] + spacing.length],
|
|
891
|
+
text: ` ${body}${separator}`,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* The edits that strip one annotation, carrying every comment the strip
|
|
896
|
+
* strands rather than deleting it (#1877). `null` withholds the fix, for a
|
|
897
|
+
* comment whose meaning is its position and which cannot stay where it is.
|
|
898
|
+
*
|
|
899
|
+
* An arrow is the one subject whose annotation sits inside a restricted
|
|
900
|
+
* production: `ArrowParameters [no LineTerminator here] =>` forbids a line
|
|
901
|
+
* terminator between the parameter list and the arrow, and a block comment
|
|
902
|
+
* carrying a line terminator IS one to the grammar. A comment left there — or
|
|
903
|
+
* carried there from inside the annotation — therefore turns the output into a
|
|
904
|
+
* hard SyntaxError that only V8 reports, since `@typescript-eslint/parser`
|
|
905
|
+
* accepts it (#1964). Such a comment is re-emitted past the `=>` instead, the
|
|
906
|
+
* nearest position outside the restricted gap that cannot itself begin one;
|
|
907
|
+
* hoisting it above the enclosing line would anchor an insertion at a column
|
|
908
|
+
* zero that may sit inside a template literal or JSX text, where the comment
|
|
909
|
+
* would become content rather than code.
|
|
910
|
+
*
|
|
911
|
+
* Every other subject ends its parameter list at a body or a semicolon, so its
|
|
912
|
+
* stranded comments stay where they were written.
|
|
913
|
+
*/
|
|
914
|
+
function planAnnotationEdits(source, entry) {
|
|
915
|
+
const range = entry.returnType.range;
|
|
916
|
+
if (entry.node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
917
|
+
const carried = carriedText(source, range);
|
|
918
|
+
return carried === null ? null : [{ range, text: carried }];
|
|
919
|
+
}
|
|
920
|
+
const gapInfo = arrowAnnotationGap(source, entry.returnType);
|
|
921
|
+
if (!gapInfo)
|
|
922
|
+
return null;
|
|
923
|
+
const { gap, arrow } = gapInfo;
|
|
924
|
+
const comments = source
|
|
925
|
+
.getAllComments()
|
|
926
|
+
.filter((comment) => containsRange(gap, comment.range));
|
|
927
|
+
const stranded = comments.filter((comment) => containsRange(range, comment.range));
|
|
928
|
+
// What the plain deletion would leave between the parameters and the arrow.
|
|
929
|
+
// A comment left there contributes its own text, so a line comment or a
|
|
930
|
+
// multi-line block comment shows up here as the line terminator it is.
|
|
931
|
+
const residue = `${textOf(source, [gap[0], range[0]])}${textOf(source, [
|
|
932
|
+
range[1],
|
|
933
|
+
gap[1],
|
|
934
|
+
])}`;
|
|
935
|
+
// The plain deletion is kept wherever it already lands a legal gap and
|
|
936
|
+
// strands nothing, so no output that survives today moves by a byte.
|
|
937
|
+
if (stranded.length === 0 && !LINE_TERMINATOR.test(residue)) {
|
|
938
|
+
return [{ range, text: '' }];
|
|
939
|
+
}
|
|
940
|
+
// Rewriting the gap collapses the lines it spanned, which moves the line a
|
|
941
|
+
// directive inside it points at, so the whole fix is withheld rather than
|
|
942
|
+
// retargeting one. The gap a directive can share with nothing else is left
|
|
943
|
+
// untouched by the branch above.
|
|
944
|
+
if (comments.some(isPositionalDirective))
|
|
945
|
+
return null;
|
|
946
|
+
const hoisted = comments.filter(replacementSegments_1.requiresOwnLine);
|
|
947
|
+
const inline = comments
|
|
948
|
+
.filter((comment) => !(0, replacementSegments_1.requiresOwnLine)(comment))
|
|
949
|
+
.map((comment) => textOf(source, comment.range));
|
|
950
|
+
const edits = [
|
|
951
|
+
{ range: gap, text: inline.length === 0 ? ' ' : ` ${inline.join(' ')} ` },
|
|
952
|
+
];
|
|
953
|
+
if (hoisted.length > 0) {
|
|
954
|
+
edits.push(hoistPastArrow(source, arrow, hoisted));
|
|
955
|
+
}
|
|
956
|
+
return edits;
|
|
957
|
+
}
|
|
849
958
|
/**
|
|
850
959
|
* ESLint applies a fix whole or not at all, and rejects one whose edits
|
|
851
960
|
* overlap. Two spans planned independently — an annotation and the declaration
|
|
@@ -866,14 +975,23 @@ function isDisjoint(edits) {
|
|
|
866
975
|
* outright. The planner computes spans that reach across separators, so a
|
|
867
976
|
* comment among the specifiers sits inside one; declining on that comment is no
|
|
868
977
|
* remedy, since it lets a comment decide whether the annotations are stripped at
|
|
869
|
-
* all, which is a comment changing the transform just the same (#1877).
|
|
978
|
+
* all, which is a comment changing the transform just the same (#1877). The
|
|
979
|
+
* annotation spans are carried the same way by {@link planAnnotationEdits},
|
|
980
|
+
* which additionally answers for the arrow whose annotation sits inside a
|
|
981
|
+
* restricted production.
|
|
870
982
|
*/
|
|
871
983
|
function planRemoval(source, removalSource, batch) {
|
|
872
984
|
const annotations = batch.map((entry) => entry.returnType.range);
|
|
873
985
|
const cleanups = (0, importRemoval_1.planOrphanedBindingRemoval)(removalSource, annotations, (variables, planned) => (0, typeDeclarationRemoval_1.planTypeDeclarationRemoval)(removalSource, variables, planned));
|
|
874
986
|
if (!cleanups)
|
|
875
987
|
return null;
|
|
876
|
-
const edits =
|
|
988
|
+
const edits = [];
|
|
989
|
+
for (const entry of batch) {
|
|
990
|
+
const planned = planAnnotationEdits(source, entry);
|
|
991
|
+
if (planned === null)
|
|
992
|
+
return null;
|
|
993
|
+
edits.push(...planned);
|
|
994
|
+
}
|
|
877
995
|
for (const range of cleanups) {
|
|
878
996
|
if (removesWholeStatement(source, range)) {
|
|
879
997
|
edits.push({ range, text: '' });
|
|
@@ -551,11 +551,13 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
|
|
|
551
551
|
// line is hoisted onto a full line of its own ABOVE the line the
|
|
552
552
|
// call starts on. That insertion can never split a token pair, and
|
|
553
553
|
// it lands a `-next-line` directive exactly one line above the
|
|
554
|
-
// statement that now hosts its subject.
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// the expression
|
|
558
|
-
|
|
554
|
+
// statement that now hosts its subject. A block comment carrying a
|
|
555
|
+
// line terminator demands a line the same way, because the grammar
|
|
556
|
+
// reads it AS a line terminator (#1963). Everything else stays
|
|
557
|
+
// inline: a single-line block comment beside the expression, and a
|
|
558
|
+
// trailing line-bound comment followed by a line break, which is
|
|
559
|
+
// safe after the expression has begun.
|
|
560
|
+
const hoistedComments = leadingComments.filter(replacementSegments_1.requiresOwnLine);
|
|
559
561
|
if (hoistedComments.length > 0) {
|
|
560
562
|
const lineStartIndex = sourceCode.getIndexFromLoc({
|
|
561
563
|
line: node.loc.start.line,
|
|
@@ -571,7 +573,7 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
|
|
|
571
573
|
}
|
|
572
574
|
const segments = [
|
|
573
575
|
...leadingComments
|
|
574
|
-
.filter((comment) => !(0, replacementSegments_1.
|
|
576
|
+
.filter((comment) => !(0, replacementSegments_1.requiresOwnLine)(comment))
|
|
575
577
|
.map(toSegment),
|
|
576
578
|
{ text: replacementText, breakAfter: false },
|
|
577
579
|
...trailingComments.map(toSegment),
|
|
@@ -43,7 +43,30 @@ const TESTS_DIR = path.join(__dirname, '..', 'tests');
|
|
|
43
43
|
* (`const jsx = ruleTesterJsx`) before calling `run`, so a call-site pattern
|
|
44
44
|
* drops it.
|
|
45
45
|
*/
|
|
46
|
-
exports.IMPORTS_SHARED_TESTER = /from\s+'
|
|
46
|
+
exports.IMPORTS_SHARED_TESTER = /from\s+'(?:\.\.\/)+utils\/ruleTester'/;
|
|
47
|
+
/**
|
|
48
|
+
* Every suite file under the tests root, as a path relative to it.
|
|
49
|
+
*
|
|
50
|
+
* The enumeration is recursive because a suite in a subdirectory is still a
|
|
51
|
+
* suite: `src/tests/rules/` holds three that jest runs and that every
|
|
52
|
+
* harvest-based gate used to miss, while their rules' top-level namesakes kept
|
|
53
|
+
* the per-rule closure green — so the gap read as coverage from every angle
|
|
54
|
+
* that was checked.
|
|
55
|
+
*
|
|
56
|
+
* Paths stay relative rather than collapsing to a basename so that two suites
|
|
57
|
+
* with the same name (`no-circular-references.test.ts` exists at both depths)
|
|
58
|
+
* remain distinguishable, which is what keeps a per-file baseline or a dedupe
|
|
59
|
+
* key from silently merging them.
|
|
60
|
+
*/
|
|
61
|
+
function suiteFilesUnder(root) {
|
|
62
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
63
|
+
const full = path.join(dir, entry.name);
|
|
64
|
+
if (entry.isDirectory())
|
|
65
|
+
return walk(full);
|
|
66
|
+
return entry.name.endsWith('.test.ts') ? [path.relative(root, full)] : [];
|
|
67
|
+
});
|
|
68
|
+
return walk(root).sort();
|
|
69
|
+
}
|
|
47
70
|
/**
|
|
48
71
|
* Jest registers a test for every `describe`/`it` a loaded module calls, so
|
|
49
72
|
* loading 271 suites inside a suite would graft their entire test list onto
|
|
@@ -156,10 +179,7 @@ function harvestRuleTesterCases() {
|
|
|
156
179
|
const realCwd = process.cwd();
|
|
157
180
|
process.chdir(scratchRoot);
|
|
158
181
|
try {
|
|
159
|
-
const files =
|
|
160
|
-
.readdirSync(TESTS_DIR)
|
|
161
|
-
.filter((file) => file.endsWith('.test.ts'))
|
|
162
|
-
.sort();
|
|
182
|
+
const files = suiteFilesUnder(TESTS_DIR);
|
|
163
183
|
for (const file of files) {
|
|
164
184
|
const fullPath = path.join(TESTS_DIR, file);
|
|
165
185
|
if (!exports.IMPORTS_SHARED_TESTER.test(fs.readFileSync(fullPath, 'utf8'))) {
|
|
@@ -16,6 +16,25 @@ import { TSESTree } from '@typescript-eslint/utils';
|
|
|
16
16
|
* silently retarget one line past its subject.
|
|
17
17
|
*/
|
|
18
18
|
export declare function requiresLineBreakAfter(comment: TSESTree.Comment): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A comment that cannot be folded onto the code that follows it, so a fixer
|
|
21
|
+
* placing it ahead of an expression must give it a line of its own.
|
|
22
|
+
*
|
|
23
|
+
* Two kinds qualify. One is the line-bound comment {@link requiresLineBreakAfter}
|
|
24
|
+
* describes, whose meaning is tied to the line it occupies. The other is a block
|
|
25
|
+
* comment containing a line terminator: the syntactic grammar treats such a
|
|
26
|
+
* comment as a LineTerminator in its own right, so it triggers every restricted
|
|
27
|
+
* production a raw newline would. Measured with `node --check`, a block comment
|
|
28
|
+
* on one line between arrow parameters and their arrow parses, while the same
|
|
29
|
+
* comment broken across two lines is a SyntaxError; ahead of a `return`
|
|
30
|
+
* argument, the multi-line form is worse still — it parses, and ASI silently
|
|
31
|
+
* replaces the returned value with `undefined` (#1963).
|
|
32
|
+
*
|
|
33
|
+
* Only fixers emitting text where a newline is meaningful need this;
|
|
34
|
+
* a replacement wrapped in parentheses can never trip a restricted production
|
|
35
|
+
* and can keep such a comment inline.
|
|
36
|
+
*/
|
|
37
|
+
export declare function requiresOwnLine(comment: TSESTree.Comment): boolean;
|
|
19
38
|
export type ReplacementSegment = {
|
|
20
39
|
text: string;
|
|
21
40
|
breakAfter: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.joinSegments = exports.joinSegmentBody = exports.requiresLineBreakAfter = void 0;
|
|
3
|
+
exports.joinSegments = exports.joinSegmentBody = exports.requiresOwnLine = exports.requiresLineBreakAfter = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const disableDirectives_1 = require("./disableDirectives");
|
|
6
6
|
/**
|
|
@@ -26,6 +26,35 @@ function requiresLineBreakAfter(comment) {
|
|
|
26
26
|
return (0, disableDirectives_1.parseDisableDirectives)([comment]).some((directive) => directive.kind === 'disable-next-line');
|
|
27
27
|
}
|
|
28
28
|
exports.requiresLineBreakAfter = requiresLineBreakAfter;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a comment's own text carries a line terminator, which only a block
|
|
31
|
+
* comment can do.
|
|
32
|
+
*/
|
|
33
|
+
function spansMultipleLines(comment) {
|
|
34
|
+
return comment.loc.start.line !== comment.loc.end.line;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A comment that cannot be folded onto the code that follows it, so a fixer
|
|
38
|
+
* placing it ahead of an expression must give it a line of its own.
|
|
39
|
+
*
|
|
40
|
+
* Two kinds qualify. One is the line-bound comment {@link requiresLineBreakAfter}
|
|
41
|
+
* describes, whose meaning is tied to the line it occupies. The other is a block
|
|
42
|
+
* comment containing a line terminator: the syntactic grammar treats such a
|
|
43
|
+
* comment as a LineTerminator in its own right, so it triggers every restricted
|
|
44
|
+
* production a raw newline would. Measured with `node --check`, a block comment
|
|
45
|
+
* on one line between arrow parameters and their arrow parses, while the same
|
|
46
|
+
* comment broken across two lines is a SyntaxError; ahead of a `return`
|
|
47
|
+
* argument, the multi-line form is worse still — it parses, and ASI silently
|
|
48
|
+
* replaces the returned value with `undefined` (#1963).
|
|
49
|
+
*
|
|
50
|
+
* Only fixers emitting text where a newline is meaningful need this;
|
|
51
|
+
* a replacement wrapped in parentheses can never trip a restricted production
|
|
52
|
+
* and can keep such a comment inline.
|
|
53
|
+
*/
|
|
54
|
+
function requiresOwnLine(comment) {
|
|
55
|
+
return requiresLineBreakAfter(comment) || spansMultipleLines(comment);
|
|
56
|
+
}
|
|
57
|
+
exports.requiresOwnLine = requiresOwnLine;
|
|
29
58
|
/**
|
|
30
59
|
* Joins the inlined expression and its carried comments into one run of text,
|
|
31
60
|
* keeping each comment on the side of the expression it was written on and
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.142",
|
|
4
|
+
"date": "2026-08-12T07:04:58.677Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-explicit-return-type",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1964
|
|
11
|
+
],
|
|
12
|
+
"summary": "keep the annotation's comments clear of the arrow gap (closes #1964)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-usememo-for-pass-by-value",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1963
|
|
19
|
+
],
|
|
20
|
+
"summary": "hoist a carried multi-line comment clear of the return (closes #1963)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.20.141",
|
|
4
26
|
"date": "2026-08-12T00:57:09.340Z",
|