@blumintinc/eslint-plugin-blumint 1.20.140 → 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/enforce-centralized-mock-firestore.js +6 -11
- package/lib/rules/enforce-early-destructuring.js +12 -3
- package/lib/rules/enforce-memoize-async.js +1 -1
- package/lib/rules/enforce-memoize-getters.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/rules/prefer-use-deep-compare-memo.js +6 -3
- package/lib/utils/harvestRuleTesterCases.js +25 -5
- package/lib/utils/importInsertion.d.ts +17 -3
- package/lib/utils/importInsertion.js +24 -7
- package/lib/utils/replacementSegments.d.ts +19 -0
- package/lib/utils/replacementSegments.js +30 -1
- package/package.json +1 -1
- package/release-manifest.json +60 -0
package/lib/index.js
CHANGED
|
@@ -218,19 +218,14 @@ function editOffset(anchor) {
|
|
|
218
218
|
*
|
|
219
219
|
* Widening to the anchor's line start is what lets the emitted
|
|
220
220
|
* `${indent}import …\n` leave the displaced statement on the indentation it
|
|
221
|
-
* already had
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* retirement
|
|
225
|
-
*
|
|
226
|
-
* to that edit's start, since ESLint rejects a fix nested inside another.
|
|
221
|
+
* already had; `importAnchorLineStartIfOwned` restricts that to the anchors it
|
|
222
|
+
* is sound for. When the resulting position still falls inside a retirement —
|
|
223
|
+
* the anchor statement is itself the declaration being retired, and the
|
|
224
|
+
* retirement claims the indentation ahead of it — the insertion moves to that
|
|
225
|
+
* edit's start, since ESLint rejects a fix nested inside another.
|
|
227
226
|
*/
|
|
228
227
|
function importPlacement(sourceCode, anchor, edits) {
|
|
229
|
-
const
|
|
230
|
-
const lineStart = sourceCode.text.lastIndexOf('\n', anchorStart - 1) + 1;
|
|
231
|
-
const placement = /^[ \t]*$/.test(sourceCode.text.slice(lineStart, anchorStart))
|
|
232
|
-
? (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor)
|
|
233
|
-
: anchor;
|
|
228
|
+
const placement = (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor);
|
|
234
229
|
const offset = editOffset(placement);
|
|
235
230
|
const enclosing = edits.find((edit) => edit.start < offset && offset < edit.end);
|
|
236
231
|
return enclosing ? { kind: 'index', index: enclosing.start } : placement;
|
|
@@ -894,7 +894,7 @@ function generateHoistingFixes(groups, callback, depsArray, depTexts, insertionS
|
|
|
894
894
|
for (const group of groups.values()) {
|
|
895
895
|
const sortedProps = Array.from(group.properties.values()).sort((a, b) => a.order - b.order);
|
|
896
896
|
const pattern = `{ ${sortedProps.map((p) => p.text).join(', ')} }`;
|
|
897
|
-
hoistedLines.push(
|
|
897
|
+
hoistedLines.push(`const ${pattern} = ${nullishSourceText(group.objectText, group.inits[0])} ?? {};`);
|
|
898
898
|
}
|
|
899
899
|
reservedNamesByScope.set(scope, updatedReservedNames);
|
|
900
900
|
const newDepSet = new Set(newDepTexts);
|
|
@@ -904,9 +904,18 @@ function generateHoistingFixes(groups, callback, depsArray, depTexts, insertionS
|
|
|
904
904
|
newDepSet.add(name);
|
|
905
905
|
}
|
|
906
906
|
}
|
|
907
|
-
|
|
907
|
+
// Anchoring to the start of the statement's LINE is the same offset as the
|
|
908
|
+
// statement itself only while the statement opens that line. When it does not
|
|
909
|
+
// — a body collapsed onto one line, or a sibling declared ahead of it — that
|
|
910
|
+
// offset sits outside the enclosing function, so a declaration reading the
|
|
911
|
+
// function's own parameters would be hoisted out of the scope that binds them.
|
|
912
|
+
const text = sourceCode.getText();
|
|
913
|
+
const lineStart = text.lastIndexOf('\n', insertionStatement.range[0]) + 1;
|
|
914
|
+
const ownsItsLine = /^[\t ]*$/.test(text.slice(lineStart, insertionStatement.range[0]));
|
|
908
915
|
const fixes = [
|
|
909
|
-
|
|
916
|
+
ownsItsLine
|
|
917
|
+
? fixer.insertTextBeforeRange([lineStart, lineStart], `${hoistedLines.map((line) => `${indent}${line}`).join('\n')}\n`)
|
|
918
|
+
: fixer.insertTextBefore(insertionStatement, `${hoistedLines.join(' ')} `),
|
|
910
919
|
fixer.replaceText(depsArray, `[${newDepTexts.join(', ')}]`),
|
|
911
920
|
];
|
|
912
921
|
for (const decl of declarationsToRemove) {
|
|
@@ -459,7 +459,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
459
459
|
// a header comment — while still placing it above the first
|
|
460
460
|
// existing import.
|
|
461
461
|
const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
|
|
462
|
-
fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.
|
|
462
|
+
fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), `${(0, importInsertion_1.importAnchorIndent)(sourceCode, anchor)}${importStatement}\n`));
|
|
463
463
|
scheduledImportFix = true;
|
|
464
464
|
}
|
|
465
465
|
// Anchor the decorator on the method — its first token, so ahead of
|
|
@@ -507,7 +507,7 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
507
507
|
// with the statement it displaces; widening to the line start
|
|
508
508
|
// then leaves that statement's indentation intact.
|
|
509
509
|
const indent = (0, importInsertion_1.importAnchorIndent)(sourceCode, anchor);
|
|
510
|
-
fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.
|
|
510
|
+
fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), `${indent}import { Memoize } from '${MEMOIZE_PREFERRED_MODULE}';\n`));
|
|
511
511
|
scheduledImportFix = true;
|
|
512
512
|
}
|
|
513
513
|
// Anchor the decorator on the member — its first token, so ahead of
|
|
@@ -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),
|
|
@@ -258,10 +258,13 @@ function ensureDeepCompareImportFixes(context, fixer) {
|
|
|
258
258
|
const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
|
|
259
259
|
const indent = (0, importInsertion_1.importAnchorIndent)(sourceCode, anchor);
|
|
260
260
|
const importText = `${indent}import { ${DEEP_COMPARE_HOOK} } from '${DEEP_COMPARE_MODULE}';\n`;
|
|
261
|
-
// Widened to the anchor's line start
|
|
262
|
-
// its own indentation
|
|
261
|
+
// Widened to the anchor's line start where the anchor opens it, because the
|
|
262
|
+
// emitted statement carries its own indentation and so leaves the displaced
|
|
263
|
+
// anchor sitting on the original. Where the anchor shares its line the
|
|
264
|
+
// widening is declined: the offset would otherwise fall ahead of the prologue
|
|
265
|
+
// this anchor was chosen to sit below.
|
|
263
266
|
return [
|
|
264
|
-
(0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.
|
|
267
|
+
(0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), importText),
|
|
265
268
|
];
|
|
266
269
|
}
|
|
267
270
|
function isImportedIdentifier(context, name) {
|
|
@@ -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'))) {
|
|
@@ -40,11 +40,25 @@ export declare function importInsertionAnchor(sourceCode: ImportInsertionSource)
|
|
|
40
40
|
*/
|
|
41
41
|
export declare function insertAtImportAnchor(sourceCode: ImportInsertionSource, fixer: TSESLint.RuleFixer, anchor: ImportInsertionAnchor, text: string): TSESLint.RuleFix;
|
|
42
42
|
/**
|
|
43
|
-
* Widens `anchor` to the start of its line
|
|
44
|
-
*
|
|
43
|
+
* Widens `anchor` to the start of its line only where whitespace is all that
|
|
44
|
+
* precedes it there, and leaves it alone otherwise.
|
|
45
|
+
*
|
|
45
46
|
* Raw-offset anchors pass through: they never sit inside an indented line.
|
|
47
|
+
*
|
|
48
|
+
* The widening buys one thing — an emitted `${indent}import …\n` leaves the
|
|
49
|
+
* displaced anchor on the indentation it already had — and that reasoning holds
|
|
50
|
+
* only while the anchor opens its line. When something else opens that line the
|
|
51
|
+
* widened offset sits ahead of it, and the anchor resolves past the prologue
|
|
52
|
+
* precisely because the prologue is what precedes it: a `'use client'` is a
|
|
53
|
+
* directive only while it is the first statement, so an import spliced above it
|
|
54
|
+
* demotes it to an inert expression statement that no bundler reads. Nothing
|
|
55
|
+
* downstream reports that — the output re-lints clean and the demoted directive
|
|
56
|
+
* is still valid TypeScript.
|
|
57
|
+
*
|
|
58
|
+
* Declining to widen costs the displaced anchor its indentation and keeps the
|
|
59
|
+
* file's meaning, which is the safe direction of that trade.
|
|
46
60
|
*/
|
|
47
|
-
export declare function
|
|
61
|
+
export declare function importAnchorLineStartIfOwned(sourceCode: ImportInsertionSource, anchor: ImportInsertionAnchor): ImportInsertionAnchor;
|
|
48
62
|
/**
|
|
49
63
|
* The whitespace prefix of the anchor's line, for fixers that replicate the
|
|
50
64
|
* anchor's indentation on the inserted import.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.importAnchorIndent = exports.
|
|
3
|
+
exports.importAnchorIndent = exports.importAnchorLineStartIfOwned = exports.insertAtImportAnchor = exports.importInsertionAnchor = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const disableDirectives_1 = require("./disableDirectives");
|
|
6
6
|
/**
|
|
@@ -95,18 +95,35 @@ function insertAtImportAnchor(sourceCode, fixer, anchor, text) {
|
|
|
95
95
|
}
|
|
96
96
|
exports.insertAtImportAnchor = insertAtImportAnchor;
|
|
97
97
|
/**
|
|
98
|
-
* Widens `anchor` to the start of its line
|
|
99
|
-
*
|
|
98
|
+
* Widens `anchor` to the start of its line only where whitespace is all that
|
|
99
|
+
* precedes it there, and leaves it alone otherwise.
|
|
100
|
+
*
|
|
100
101
|
* Raw-offset anchors pass through: they never sit inside an indented line.
|
|
102
|
+
*
|
|
103
|
+
* The widening buys one thing — an emitted `${indent}import …\n` leaves the
|
|
104
|
+
* displaced anchor on the indentation it already had — and that reasoning holds
|
|
105
|
+
* only while the anchor opens its line. When something else opens that line the
|
|
106
|
+
* widened offset sits ahead of it, and the anchor resolves past the prologue
|
|
107
|
+
* precisely because the prologue is what precedes it: a `'use client'` is a
|
|
108
|
+
* directive only while it is the first statement, so an import spliced above it
|
|
109
|
+
* demotes it to an inert expression statement that no bundler reads. Nothing
|
|
110
|
+
* downstream reports that — the output re-lints clean and the demoted directive
|
|
111
|
+
* is still valid TypeScript.
|
|
112
|
+
*
|
|
113
|
+
* Declining to widen costs the displaced anchor its indentation and keeps the
|
|
114
|
+
* file's meaning, which is the safe direction of that trade.
|
|
101
115
|
*/
|
|
102
|
-
function
|
|
116
|
+
function importAnchorLineStartIfOwned(sourceCode, anchor) {
|
|
103
117
|
if (anchor.kind === 'index') {
|
|
104
118
|
return anchor;
|
|
105
119
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
120
|
+
const start = anchor.target.range[0];
|
|
121
|
+
const lineStart = sourceCode.text.lastIndexOf('\n', start - 1) + 1;
|
|
122
|
+
return /^[\t ]*$/.test(sourceCode.text.slice(lineStart, start))
|
|
123
|
+
? { kind: 'index', index: lineStart }
|
|
124
|
+
: anchor;
|
|
108
125
|
}
|
|
109
|
-
exports.
|
|
126
|
+
exports.importAnchorLineStartIfOwned = importAnchorLineStartIfOwned;
|
|
110
127
|
/**
|
|
111
128
|
* The whitespace prefix of the anchor's line, for fixers that replicate the
|
|
112
129
|
* anchor's indentation on the inserted import.
|
|
@@ -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,64 @@
|
|
|
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
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.141",
|
|
26
|
+
"date": "2026-08-12T00:57:09.340Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-early-destructuring",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1956
|
|
33
|
+
],
|
|
34
|
+
"summary": "anchor the hoist to the hook call, not to its line (closes #1956)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "enforce-memoize-async",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1957
|
|
41
|
+
],
|
|
42
|
+
"summary": "keep the injected import below a shared-line directive (closes #1957)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "enforce-memoize-getters",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1958
|
|
49
|
+
],
|
|
50
|
+
"summary": "keep the injected import below a shared-line directive (closes #1958)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "prefer-use-deep-compare-memo",
|
|
54
|
+
"changeType": "fix",
|
|
55
|
+
"issues": [
|
|
56
|
+
1959
|
|
57
|
+
],
|
|
58
|
+
"summary": "keep the injected import below a shared-line directive (closes #1959)"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
2
62
|
{
|
|
3
63
|
"version": "1.20.140",
|
|
4
64
|
"date": "2026-08-11T22:49:18.139Z",
|