Skip to content

Commit 9569e8a

Browse files
authored
Fix newline issues when adding multiple imports (#38119)
* Add new import declarations in a single TextChanges call * Refactor
1 parent c28bd65 commit 9569e8a

File tree

7 files changed

+96
-23
lines changed

7 files changed

+96
-23
lines changed

src/compiler/core.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,28 @@ namespace ts {
844844
return to;
845845
}
846846

847+
/**
848+
* Combines two arrays, values, or undefineds into the smallest container that can accommodate the resulting set:
849+
*
850+
* ```
851+
* undefined -> undefined -> undefined
852+
* T -> undefined -> T
853+
* T -> T -> T[]
854+
* T[] -> undefined -> T[] (no-op)
855+
* T[] -> T -> T[] (append)
856+
* T[] -> T[] -> T[] (concatenate)
857+
* ```
858+
*/
859+
export function combine<T>(xs: T | readonly T[] | undefined, ys: T | readonly T[] | undefined): T | readonly T[] | undefined;
860+
export function combine<T>(xs: T | T[] | undefined, ys: T | T[] | undefined): T | T[] | undefined;
861+
export function combine<T>(xs: T | T[] | undefined, ys: T | T[] | undefined) {
862+
if (xs === undefined) return ys;
863+
if (ys === undefined) return xs;
864+
if (isArray(xs)) return isArray(ys) ? concatenate(xs, ys) : append(xs, ys);
865+
if (isArray(ys)) return append(ys, xs);
866+
return [xs, ys];
867+
}
868+
847869
/**
848870
* Gets the actual offset into an array for a relative offset. Negative offsets indicate a
849871
* position offset from the end of the array.

src/services/codefixes/importFixes.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ namespace ts.codefix {
4545
// Keys are import clause node IDs.
4646
const addToExisting = createMap<{ readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>();
4747
const newImports = createMap<Mutable<ImportsCollection & { useRequire: boolean }>>();
48-
let lastModuleSpecifier: string | undefined;
4948
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes };
5049

5150
function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) {
@@ -97,7 +96,6 @@ namespace ts.codefix {
9796
let entry = newImports.get(moduleSpecifier);
9897
if (!entry) {
9998
newImports.set(moduleSpecifier, entry = { namedImports: [], namespaceLikeImport: undefined, typeOnly, useRequire });
100-
lastModuleSpecifier = moduleSpecifier;
10199
}
102100
else {
103101
// An import clause can only be type-only if every import fix contributing to it can be type-only.
@@ -135,10 +133,15 @@ namespace ts.codefix {
135133
addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport }) => {
136134
doAddExistingFix(changeTracker, sourceFile, importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport);
137135
});
136+
137+
let newDeclarations: Statement | readonly Statement[] | undefined;
138138
newImports.forEach(({ useRequire, ...imports }, moduleSpecifier) => {
139-
const addDeclarations = useRequire ? addNewRequires : addNewImports;
140-
addDeclarations(changeTracker, sourceFile, moduleSpecifier, quotePreference, imports, /*blankLineBetween*/ lastModuleSpecifier === moduleSpecifier);
139+
const getDeclarations = useRequire ? getNewRequires : getNewImports;
140+
newDeclarations = combine(newDeclarations, getDeclarations(moduleSpecifier, quotePreference, imports));
141141
});
142+
if (newDeclarations) {
143+
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
144+
}
142145
}
143146
}
144147

@@ -631,11 +634,11 @@ namespace ts.codefix {
631634
}
632635
case ImportFixKind.AddNew: {
633636
const { importKind, moduleSpecifier, typeOnly, useRequire } = fix;
634-
const addDeclarations = useRequire ? addNewRequires : addNewImports;
637+
const getDeclarations = useRequire ? getNewRequires : getNewImports;
635638
const importsCollection = importKind === ImportKind.Default ? { defaultImport: symbolName, typeOnly } :
636639
importKind === ImportKind.Named ? { namedImports: [symbolName], typeOnly } :
637640
{ namespaceLikeImport: { importKind, name: symbolName }, typeOnly };
638-
addDeclarations(changes, sourceFile, moduleSpecifier, quotePreference, importsCollection, /*blankLineBetween*/ true);
641+
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, importsCollection), /*blankLineBetween*/ true);
639642
return [importKind === ImportKind.Default ? Diagnostics.Import_default_0_from_module_1 : Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier];
640643
}
641644
default:
@@ -717,13 +720,13 @@ namespace ts.codefix {
717720
readonly name: string;
718721
};
719722
}
720-
function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection, blankLineBetween: boolean): void {
723+
function getNewImports(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): Statement | readonly Statement[] {
721724
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
725+
let statements: Statement | readonly Statement[] | undefined;
722726
if (imports.defaultImport !== undefined || imports.namedImports?.length) {
723-
insertImport(changes, sourceFile,
724-
makeImport(
725-
imports.defaultImport === undefined ? undefined : createIdentifier(imports.defaultImport),
726-
imports.namedImports?.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference, imports.typeOnly), /*blankLineBetween*/ blankLineBetween);
727+
statements = combine(statements, makeImport(
728+
imports.defaultImport === undefined ? undefined : createIdentifier(imports.defaultImport),
729+
imports.namedImports?.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference, imports.typeOnly));
727730
}
728731
const { namespaceLikeImport, typeOnly } = imports;
729732
if (namespaceLikeImport) {
@@ -741,26 +744,29 @@ namespace ts.codefix {
741744
createNamespaceImport(createIdentifier(namespaceLikeImport.name)),
742745
typeOnly),
743746
quotedModuleSpecifier);
744-
insertImport(changes, sourceFile, declaration, /*blankLineBetween*/ blankLineBetween);
747+
statements = combine(statements, declaration);
745748
}
749+
return Debug.checkDefined(statements);
746750
}
747751

748-
function addNewRequires(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection, blankLineBetween: boolean) {
752+
function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): Statement | readonly Statement[] {
749753
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
754+
let statements: Statement | readonly Statement[] | undefined;
750755
// const { default: foo, bar, etc } = require('./mod');
751756
if (imports.defaultImport || imports.namedImports?.length) {
752757
const bindingElements = imports.namedImports?.map(name => createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || [];
753758
if (imports.defaultImport) {
754759
bindingElements.unshift(createBindingElement(/*dotDotDotToken*/ undefined, "default", imports.defaultImport));
755760
}
756761
const declaration = createConstEqualsRequireDeclaration(createObjectBindingPattern(bindingElements), quotedModuleSpecifier);
757-
insertImport(changes, sourceFile, declaration, blankLineBetween);
762+
statements = combine(statements, declaration);
758763
}
759764
// const foo = require('./mod');
760765
if (imports.namespaceLikeImport) {
761766
const declaration = createConstEqualsRequireDeclaration(imports.namespaceLikeImport.name, quotedModuleSpecifier);
762-
insertImport(changes, sourceFile, declaration, blankLineBetween);
767+
statements = combine(statements, declaration);
763768
}
769+
return Debug.checkDefined(statements);
764770
}
765771

766772
function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern, quotedModuleSpecifier: StringLiteral): VariableStatement {

src/services/refactors/moveToNewFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ namespace ts.refactor {
121121
const quotePreference = getQuotePreference(oldFile, preferences);
122122
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, quotePreference);
123123
if (importsFromNewFile) {
124-
insertImport(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true);
124+
insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true);
125125
}
126126

127127
deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker);

src/services/textChanges.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,25 @@ namespace ts.textChanges {
349349
}
350350

351351
public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void {
352+
this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween);
353+
}
354+
355+
public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void {
356+
this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween);
357+
}
358+
359+
private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void {
352360
const pos = getInsertionPositionAtSourceFileTop(sourceFile);
353-
this.insertNodeAt(sourceFile, pos, newNode, {
361+
const options = {
354362
prefix: pos === 0 ? undefined : this.newLineCharacter,
355363
suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""),
356-
});
364+
};
365+
if (isArray(insert)) {
366+
this.insertNodesAt(sourceFile, pos, insert, options);
367+
}
368+
else {
369+
this.insertNodeAt(sourceFile, pos, insert, options);
370+
}
357371
}
358372

359373
public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray<ParameterDeclaration>, newParam: ParameterDeclaration): void {

src/services/utilities.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,14 +1871,23 @@ namespace ts {
18711871
return node.modifiers && find(node.modifiers, m => m.kind === kind);
18721872
}
18731873

1874-
export function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement, blankLineBetween: boolean): void {
1875-
const importKindPredicate = importDecl.kind === SyntaxKind.VariableStatement ? isRequireVariableDeclarationStatement : isAnyImportSyntax;
1874+
export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: Statement | readonly Statement[], blankLineBetween: boolean): void {
1875+
const decl = isArray(imports) ? imports[0] : imports;
1876+
const importKindPredicate = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableDeclarationStatement : isAnyImportSyntax;
18761877
const lastImportDeclaration = findLast(sourceFile.statements, statement => importKindPredicate(statement));
18771878
if (lastImportDeclaration) {
1878-
changes.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl);
1879+
if (isArray(imports)) {
1880+
changes.insertNodesAfter(sourceFile, lastImportDeclaration, imports);
1881+
}
1882+
else {
1883+
changes.insertNodeAfter(sourceFile, lastImportDeclaration, imports);
1884+
}
1885+
}
1886+
else if (isArray(imports)) {
1887+
changes.insertNodesAtTopOfFile(sourceFile, imports, blankLineBetween);
18791888
}
18801889
else {
1881-
changes.insertNodeAtTopOfFile(sourceFile, importDecl, blankLineBetween);
1890+
changes.insertNodeAtTopOfFile(sourceFile, imports, blankLineBetween);
18821891
}
18831892
}
18841893

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+
// @Filename: /path.ts
4+
////export declare function join(): void;
5+
6+
// @Filename: /os.ts
7+
////export declare function homedir(): void;
8+
9+
// @Filename: /index.ts
10+
////
11+
////join();
12+
////homedir();
13+
14+
goTo.file("/index.ts");
15+
verify.codeFixAll({
16+
fixId: "fixMissingImport",
17+
fixAllDescription: "Add all missing imports",
18+
newFileContent: `import { join } from "./path";
19+
import { homedir } from "./os";
20+
21+
join();
22+
homedir();`
23+
});

tests/cases/fourslash/importNameCodeFix_exportEquals.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ verify.codeFixAll({
1717
fixAllDescription: "Add all missing imports",
1818
newFileContent:
1919
`import { b } from "./a";
20-
2120
import a = require("./a");
2221
2322
a;

0 commit comments

Comments
 (0)