Skip to content

Commit 1a04b17

Browse files
authored
Fix organize imports overlap (microsoft#43228)
* Fix organize imports overlap * Refactored multiline end position * Added tests for single line trailing trivia * Fix rearranging imports * Fix lint error * PR coments
1 parent c671fe1 commit 1a04b17

File tree

6 files changed

+158
-13
lines changed

6 files changed

+158
-13
lines changed

src/services/organizeImports.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,26 @@ namespace ts.OrganizeImports {
6363
? coalesce(importGroup)
6464
: importGroup);
6565

66-
// Delete or replace the first import.
66+
// Delete all nodes if there are no imports.
6767
if (newImportDecls.length === 0) {
68-
changeTracker.delete(sourceFile, oldImportDecls[0]);
68+
// Consider the first node to have trailingTrivia as we want to exclude the
69+
// "header" comment.
70+
changeTracker.deleteNodes(sourceFile, oldImportDecls, {
71+
trailingTriviaOption: textChanges.TrailingTriviaOption.Include,
72+
}, /*hasTrailingComment*/ true);
6973
}
7074
else {
7175
// Note: Delete the surrounding trivia because it will have been retained in newImportDecls.
72-
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, {
76+
const replaceOptions = {
7377
leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, // Leave header comment in place
7478
trailingTriviaOption: textChanges.TrailingTriviaOption.Include,
7579
suffix: getNewLineOrDefaultFromHost(host, formatContext.options),
76-
});
77-
}
78-
79-
// Delete any subsequent imports.
80-
for (let i = 1; i < oldImportDecls.length; i++) {
81-
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
80+
};
81+
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions);
82+
const hasTrailingComment = changeTracker.nodeHasTrailingComment(sourceFile, oldImportDecls[0], replaceOptions);
83+
changeTracker.deleteNodes(sourceFile, oldImportDecls.slice(1), {
84+
trailingTriviaOption: textChanges.TrailingTriviaOption.Include,
85+
}, hasTrailingComment);
8286
}
8387
}
8488
}

src/services/textChanges.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ namespace ts.textChanges {
168168
return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) };
169169
}
170170

171-
function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStart) {
171+
function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd, hasTrailingComment = false) {
172172
const { leadingTriviaOption } = options;
173173
if (leadingTriviaOption === LeadingTriviaOption.Exclude) {
174174
return node.getStart(sourceFile);
@@ -199,6 +199,17 @@ namespace ts.textChanges {
199199
// when b is deleted - we delete it
200200
return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start;
201201
}
202+
203+
// if node has a trailing comments, use comment end position as the text has already been included.
204+
if (hasTrailingComment) {
205+
// Check first for leading comments as if the node is the first import, we want to exclude the trivia;
206+
// otherwise we get the trailing comments.
207+
const comment = getLeadingCommentRanges(sourceFile.text, fullStart)?.[0] || getTrailingCommentRanges(sourceFile.text, fullStart)?.[0];
208+
if (comment) {
209+
return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
210+
}
211+
}
212+
202213
// get start position of the line following the line that contains fullstart position
203214
// (but only if the fullstart isn't the very beginning of the file)
204215
const nextLineStart = fullStart > 0 ? 1 : 0;
@@ -208,7 +219,38 @@ namespace ts.textChanges {
208219
return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile);
209220
}
210221

211-
function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd) {
222+
/** Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; */
223+
function getEndPositionOfMultilineTrailingComment(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number | undefined {
224+
const { end } = node;
225+
const { trailingTriviaOption } = options;
226+
if (trailingTriviaOption === TrailingTriviaOption.Include) {
227+
// If the trailing comment is a multiline comment that extends to the next lines,
228+
// return the end of the comment and track it for the next nodes to adjust.
229+
const comments = getTrailingCommentRanges(sourceFile.text, end);
230+
if (comments) {
231+
const nodeEndLine = getLineOfLocalPosition(sourceFile, node.end);
232+
for (const comment of comments) {
233+
// Single line can break the loop as trivia will only be this line.
234+
// Comments on subsequest lines are also ignored.
235+
if (comment.kind === SyntaxKind.SingleLineCommentTrivia || getLineOfLocalPosition(sourceFile, comment.pos) > nodeEndLine) {
236+
break;
237+
}
238+
239+
// Get the end line of the comment and compare against the end line of the node.
240+
// If the comment end line position and the multiline comment extends to multiple lines,
241+
// then is safe to return the end position.
242+
const commentEndLine = getLineOfLocalPosition(sourceFile, comment.end);
243+
if (commentEndLine > nodeEndLine) {
244+
return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
245+
}
246+
}
247+
}
248+
}
249+
250+
return undefined;
251+
}
252+
253+
function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number {
212254
const { end } = node;
213255
const { trailingTriviaOption } = options;
214256
if (trailingTriviaOption === TrailingTriviaOption.Exclude) {
@@ -222,6 +264,12 @@ namespace ts.textChanges {
222264
}
223265
return end;
224266
}
267+
268+
const multilineEndPosition = getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
269+
if (multilineEndPosition) {
270+
return multilineEndPosition;
271+
}
272+
225273
const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true);
226274

227275
return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || isLineBreak(sourceFile.text.charCodeAt(newEnd - 1)))
@@ -293,6 +341,18 @@ namespace ts.textChanges {
293341
this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options));
294342
}
295343

344+
public deleteNodes(sourceFile: SourceFile, nodes: readonly Node[], options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }, hasTrailingComment: boolean): void {
345+
// When deleting multiple nodes we need to track if the end position is including multiline trailing comments.
346+
for (const node of nodes) {
347+
const pos = getAdjustedStartPosition(sourceFile, node, options, hasTrailingComment);
348+
const end = getAdjustedEndPosition(sourceFile, node, options);
349+
350+
this.deleteRange(sourceFile, { pos, end });
351+
352+
hasTrailingComment = !!getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
353+
}
354+
}
355+
296356
public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
297357
this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) });
298358
}
@@ -337,6 +397,10 @@ namespace ts.textChanges {
337397
this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options);
338398
}
339399

400+
public nodeHasTrailingComment(sourceFile: SourceFile, oldNode: Node, configurableEnd: ConfigurableEnd = useNonAdjustedPositions): boolean {
401+
return !!getEndPositionOfMultilineTrailingComment(sourceFile, oldNode, configurableEnd);
402+
}
403+
340404
private nextCommaToken(sourceFile: SourceFile, node: Node): Node | undefined {
341405
const next = findNextToken(node, node.parent, sourceFile);
342406
return next && next.kind === SyntaxKind.CommaToken ? next : undefined;
@@ -1289,8 +1353,9 @@ namespace ts.textChanges {
12891353
case SyntaxKind.ImportEqualsDeclaration:
12901354
const isFirstImport = sourceFile.imports.length && node === first(sourceFile.imports).parent || node === find(sourceFile.statements, isAnyImportSyntax);
12911355
// For first import, leave header comment in place, otherwise only delete JSDoc comments
1292-
deleteNode(changes, sourceFile, node,
1293-
{ leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine });
1356+
deleteNode(changes, sourceFile, node, {
1357+
leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine,
1358+
});
12941359
break;
12951360

12961361
case SyntaxKind.BindingElement:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// Regression test for GH#43107
4+
5+
//// import * as something from "path";/**
6+
//// * some comment here
7+
//// * and there
8+
//// */
9+
//// import * as somethingElse from "anotherpath";
10+
//// import * as AnotherThing from "somepath";/**
11+
//// * some comment here
12+
//// * and there
13+
//// */
14+
//// import * as AnotherThingElse from "someotherpath";
15+
16+
verify.organizeImports('');
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// Regression test for GH#43107
4+
5+
//// import * as something from "path";/**
6+
//// * some comment here
7+
//// * and there
8+
//// */
9+
//// import * as somethingElse from "anotherpath";
10+
//// import * as AnotherThing from "somepath";/**
11+
//// * some comment here
12+
//// * and there
13+
//// */
14+
//// import * as AnotherThingElse from "someotherpath";
15+
16+
verify.organizeImports('');
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// Regression test for GH#43107
4+
5+
//// import * as something from "path"; /* small comment */ // single line one.
6+
//// /* some comment here
7+
//// * and there
8+
//// */
9+
//// import * as somethingElse from "anotherpath";
10+
//// import * as anotherThing from "someopath"; /* small comment */ // single line one.
11+
//// /* some comment here
12+
//// * and there
13+
//// */
14+
//// import * as anotherThingElse from "someotherpath";
15+
////
16+
//// anotherThing;
17+
18+
verify.organizeImports(
19+
`import * as anotherThing from "someopath"; /* small comment */ // single line one.
20+
21+
anotherThing;`);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// Regression test for GH#43107
4+
5+
//// import * as something from "path"; /**
6+
//// * some comment here
7+
//// * and there
8+
//// */
9+
//// import * as somethingElse from "anotherpath";
10+
////
11+
//// something;
12+
//// somethingElse;
13+
14+
verify.organizeImports(
15+
`import * as somethingElse from "anotherpath";
16+
import * as something from "path"; /**
17+
* some comment here
18+
* and there
19+
*/
20+
21+
something;
22+
somethingElse;`
23+
);

0 commit comments

Comments
 (0)