mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-10 18:04:18 -05:00
Introduce an organizeImports command
In phase 1, it coalesces imports from the same module and sorts the results, but does not remove unused imports. Some trivia is lost during coalescing, but none should be duplicated.
This commit is contained in:
@@ -141,6 +141,7 @@ var harnessSources = harnessCoreSources.concat([
|
||||
"typingsInstaller.ts",
|
||||
"projectErrors.ts",
|
||||
"matchFiles.ts",
|
||||
"organizeImports.ts",
|
||||
"initializeTSConfig.ts",
|
||||
"extractConstants.ts",
|
||||
"extractFunctions.ts",
|
||||
|
||||
@@ -522,6 +522,9 @@ namespace Harness.LanguageService {
|
||||
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
|
||||
throw new Error("Not supported on the shim.");
|
||||
}
|
||||
organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray<ts.FileTextChanges> {
|
||||
throw new Error("Not supported on the shim.");
|
||||
}
|
||||
getEmitOutput(fileName: string): ts.EmitOutput {
|
||||
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"./unittests/tsserverProjectSystem.ts",
|
||||
"./unittests/tscWatchMode.ts",
|
||||
"./unittests/matchFiles.ts",
|
||||
"./unittests/organizeImports.ts",
|
||||
"./unittests/initializeTSConfig.ts",
|
||||
"./unittests/compileOnSave.ts",
|
||||
"./unittests/typingsInstaller.ts",
|
||||
|
||||
426
src/harness/unittests/organizeImports.ts
Normal file
426
src/harness/unittests/organizeImports.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
/// <reference path="..\..\..\src\harness\harness.ts" />
|
||||
/// <reference path="..\..\..\src\harness\virtualFileSystem.ts" />
|
||||
|
||||
|
||||
namespace ts {
|
||||
describe("Organize imports", () => {
|
||||
describe("Sort imports", () => {
|
||||
it("No imports", () => {
|
||||
assert.isEmpty(sortImports([]));
|
||||
});
|
||||
|
||||
it("One import", () => {
|
||||
const unsortedImports = parseImports(`import "lib";`);
|
||||
const actualSortedImports = sortImports(unsortedImports);
|
||||
const expectedSortedImports = unsortedImports;
|
||||
assertListEqual(expectedSortedImports, actualSortedImports);
|
||||
});
|
||||
|
||||
it("Stable - import kind", () => {
|
||||
assertUnaffectedBySort(
|
||||
`import "lib";`,
|
||||
`import * as x from "lib";`,
|
||||
`import x from "lib";`,
|
||||
`import {x} from "lib";`);
|
||||
});
|
||||
|
||||
it("Stable - default property alias", () => {
|
||||
assertUnaffectedBySort(
|
||||
`import x from "lib";`,
|
||||
`import y from "lib";`);
|
||||
});
|
||||
|
||||
it("Stable - module alias", () => {
|
||||
assertUnaffectedBySort(
|
||||
`import * as x from "lib";`,
|
||||
`import * as y from "lib";`);
|
||||
});
|
||||
|
||||
it("Stable - symbol", () => {
|
||||
assertUnaffectedBySort(
|
||||
`import {x} from "lib";`,
|
||||
`import {y} from "lib";`);
|
||||
});
|
||||
|
||||
it("Sort - non-relative vs non-relative", () => {
|
||||
assertSortsBefore(
|
||||
`import y from "lib1";`,
|
||||
`import x from "lib2";`);
|
||||
});
|
||||
|
||||
it("Sort - relative vs relative", () => {
|
||||
assertSortsBefore(
|
||||
`import y from "./lib1";`,
|
||||
`import x from "./lib2";`);
|
||||
});
|
||||
|
||||
it("Sort - invalid vs invalid", () => {
|
||||
assertSortsBefore(
|
||||
"import y from `${'lib1'}`;",
|
||||
"import x from `${'lib2'}`;");
|
||||
});
|
||||
|
||||
it("Sort - relative vs non-relative", () => {
|
||||
assertSortsBefore(
|
||||
`import y from "lib";`,
|
||||
`import x from "./lib";`);
|
||||
});
|
||||
|
||||
it("Sort - non-relative vs invalid", () => {
|
||||
assertSortsBefore(
|
||||
`import y from "lib";`,
|
||||
"import x from `${'lib'}`;");
|
||||
});
|
||||
|
||||
it("Sort - relative vs invalid", () => {
|
||||
assertSortsBefore(
|
||||
`import y from "./lib";`,
|
||||
"import x from `${'lib'}`;");
|
||||
});
|
||||
|
||||
function assertUnaffectedBySort(...importStrings: string[]) {
|
||||
const unsortedImports1 = parseImports(...importStrings);
|
||||
assertListEqual(unsortedImports1, sortImports(unsortedImports1));
|
||||
|
||||
const unsortedImports2 = reverse(unsortedImports1);
|
||||
assertListEqual(unsortedImports2, sortImports(unsortedImports2));
|
||||
}
|
||||
|
||||
function assertSortsBefore(importString1: string, importString2: string) {
|
||||
const imports = parseImports(importString1, importString2);
|
||||
assertListEqual(imports, sortImports(imports));
|
||||
assertListEqual(imports, sortImports(reverse(imports)));
|
||||
}
|
||||
});
|
||||
|
||||
describe("Coalesce imports", () => {
|
||||
it("No imports", () => {
|
||||
assert.isEmpty(coalesceImports([]));
|
||||
});
|
||||
|
||||
it("Sort specifiers", () => {
|
||||
const sortedImports = parseImports(`import { default as m, a as n, b, y, z as o } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(`import { a as n, b, default as m, y, z as o } from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine side-effect-only imports", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(`import "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine namespace imports", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import * as x from "lib";`,
|
||||
`import * as y from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine default imports", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import x from "lib";`,
|
||||
`import y from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine property imports", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import { x } from "lib";`,
|
||||
`import { y as z } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine side-effect-only import with namespace import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import * as x from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine side-effect-only import with default import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import x from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine side-effect-only import with property import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import { x } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine namespace import with default import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import * as x from "lib";`,
|
||||
`import y from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(
|
||||
`import y, * as x from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine namespace import with property import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import * as x from "lib";`,
|
||||
`import { y } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine default import with property import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import x from "lib";`,
|
||||
`import { y } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(
|
||||
`import x, { y } from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine many imports", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import * as y from "lib";`,
|
||||
`import w from "lib";`,
|
||||
`import { b } from "lib";`,
|
||||
`import "lib";`,
|
||||
`import * as x from "lib";`,
|
||||
`import z from "lib";`,
|
||||
`import { a } from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(
|
||||
`import "lib";`,
|
||||
`import * as x from "lib";`,
|
||||
`import * as y from "lib";`,
|
||||
`import { a, b, default as w, default as z } from "lib";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
it("Combine imports from different modules", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import { d } from "lib1";`,
|
||||
`import { b } from "lib1";`,
|
||||
`import { c } from "lib2";`,
|
||||
`import { a } from "lib2";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = parseImports(
|
||||
`import { b, d } from "lib1";`,
|
||||
`import { a, c } from "lib2";`);
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
|
||||
// This is descriptive, rather than normative
|
||||
it("Combine two namespace imports with one default import", () => {
|
||||
const sortedImports = parseImports(
|
||||
`import * as x from "lib";`,
|
||||
`import * as y from "lib";`,
|
||||
`import z from "lib";`);
|
||||
const actualCoalescedImports = coalesceImports(sortedImports);
|
||||
const expectedCoalescedImports = sortedImports;
|
||||
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Baselines", () => {
|
||||
|
||||
const libFile = {
|
||||
path: "/lib.ts",
|
||||
content: `
|
||||
export function F1();
|
||||
export default function F2();
|
||||
`,
|
||||
};
|
||||
|
||||
testOrganizeImports("Simple",
|
||||
{
|
||||
path: "/test.ts",
|
||||
content: `
|
||||
import { F1, F2 } from "lib";
|
||||
import * as NS from "lib";
|
||||
import D from "lib";
|
||||
|
||||
NS.F1();
|
||||
D();
|
||||
F1();
|
||||
F2();
|
||||
`,
|
||||
},
|
||||
libFile);
|
||||
|
||||
testOrganizeImports("MoveToTop",
|
||||
{
|
||||
path: "/test.ts",
|
||||
content: `
|
||||
import { F1, F2 } from "lib";
|
||||
F1();
|
||||
F2();
|
||||
import * as NS from "lib";
|
||||
NS.F1();
|
||||
import D from "lib";
|
||||
D();
|
||||
`,
|
||||
},
|
||||
libFile);
|
||||
|
||||
testOrganizeImports("CoalesceTrivia",
|
||||
{
|
||||
path: "/test.ts",
|
||||
content: `
|
||||
/*A*/import /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I
|
||||
/*J*/import /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R
|
||||
|
||||
F1();
|
||||
F2();
|
||||
`,
|
||||
},
|
||||
libFile);
|
||||
|
||||
testOrganizeImports("SortTrivia",
|
||||
{
|
||||
path: "/test.ts",
|
||||
content: `
|
||||
/*A*/import /*B*/ "lib2" /*C*/;/*D*/ //E
|
||||
/*F*/import /*G*/ "lib1" /*H*/;/*I*/ //J
|
||||
`,
|
||||
},
|
||||
{ path: "/lib1.ts", content: "" },
|
||||
{ path: "/lib2.ts", content: "" });
|
||||
|
||||
function testOrganizeImports(testName: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
|
||||
it(testName, () => runBaseline(`organizeImports/${testName}.ts`, testFile, ...otherFiles));
|
||||
}
|
||||
|
||||
function runBaseline(baselinePath: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
|
||||
const { path: testPath, content: testContent } = testFile;
|
||||
const languageService = makeLanguageService(testFile, ...otherFiles);
|
||||
const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions);
|
||||
assert.equal(1, changes.length);
|
||||
assert.equal(testPath, changes[0].fileName);
|
||||
|
||||
Harness.Baseline.runBaseline(baselinePath, () => {
|
||||
const data: string[] = [];
|
||||
data.push(`// ==ORIGINAL==`);
|
||||
data.push(testContent);
|
||||
|
||||
data.push(`// ==ORGANIZED==`);
|
||||
const newText = textChanges.applyChanges(testContent, changes[0].textChanges);
|
||||
data.push(newText);
|
||||
|
||||
return data.join(newLineCharacter);
|
||||
});
|
||||
}
|
||||
|
||||
function makeLanguageService(...files: TestFSWithWatch.FileOrFolder[]) {
|
||||
const host = projectSystem.createServerHost(files);
|
||||
const projectService = projectSystem.createProjectService(host, { useSingleInferredProject: true });
|
||||
files.forEach(f => projectService.openClientFile(f.path));
|
||||
return projectService.inferredProjects[0].getLanguageService();
|
||||
}
|
||||
});
|
||||
|
||||
function parseImports(...importStrings: string[]): ReadonlyArray<ImportDeclaration> {
|
||||
const sourceFile = createSourceFile("a.ts", importStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS);
|
||||
const imports = filter(sourceFile.statements, isImportDeclaration);
|
||||
assert.equal(importStrings.length, imports.length);
|
||||
return imports;
|
||||
}
|
||||
|
||||
function assertEqual(node1?: Node, node2?: Node) {
|
||||
if (node1 === undefined) {
|
||||
assert.isUndefined(node2);
|
||||
return;
|
||||
}
|
||||
else if (node2 === undefined) {
|
||||
assert.isUndefined(node1); // Guaranteed to fail
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(node1.kind, node2.kind);
|
||||
|
||||
switch(node1.kind) {
|
||||
case SyntaxKind.ImportDeclaration:
|
||||
const decl1 = node1 as ImportDeclaration;
|
||||
const decl2 = node2 as ImportDeclaration;
|
||||
assertEqual(decl1.importClause, decl2.importClause);
|
||||
assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier);
|
||||
break;
|
||||
case SyntaxKind.ImportClause:
|
||||
const clause1 = node1 as ImportClause;
|
||||
const clause2 = node2 as ImportClause;
|
||||
assertEqual(clause1.name, clause2.name);
|
||||
assertEqual(clause1.namedBindings, clause2.namedBindings);
|
||||
case SyntaxKind.NamespaceImport:
|
||||
const nsi1 = node1 as NamespaceImport;
|
||||
const nsi2 = node2 as NamespaceImport;
|
||||
assertEqual(nsi1.name, nsi2.name);
|
||||
break;
|
||||
case SyntaxKind.NamedImports:
|
||||
const ni1 = node1 as NamedImports;
|
||||
const ni2 = node2 as NamedImports;
|
||||
assertListEqual(ni1.elements, ni2.elements);
|
||||
break;
|
||||
case SyntaxKind.ImportSpecifier:
|
||||
const is1 = node1 as ImportSpecifier;
|
||||
const is2 = node2 as ImportSpecifier;
|
||||
assertEqual(is1.name, is2.name);
|
||||
assertEqual(is1.propertyName, is2.propertyName);
|
||||
break;
|
||||
case SyntaxKind.Identifier:
|
||||
const id1 = node1 as Identifier;
|
||||
const id2 = node2 as Identifier;
|
||||
assert.equal(id1.text, id2.text);
|
||||
break;
|
||||
case SyntaxKind.StringLiteral:
|
||||
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||
const sl1 = node1 as LiteralLikeNode;
|
||||
const sl2 = node2 as LiteralLikeNode;
|
||||
assert.equal(sl1.text, sl2.text);
|
||||
break;
|
||||
default:
|
||||
assert.equal(node1.getText(), node2.getText());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function assertListEqual(list1: ReadonlyArray<Node>, list2: ReadonlyArray<Node>) {
|
||||
if (list1 === undefined || list2 === undefined) {
|
||||
assert.isUndefined(list1);
|
||||
assert.isUndefined(list2);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(list1.length, list2.length);
|
||||
for (let i = 0; i < list1.length; i++) {
|
||||
assertEqual(list1[i], list2[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function reverse<T>(list: ReadonlyArray<T>) {
|
||||
const result = [];
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
result.push(list[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -262,6 +262,8 @@ namespace ts.server {
|
||||
CommandNames.GetApplicableRefactors,
|
||||
CommandNames.GetEditsForRefactor,
|
||||
CommandNames.GetEditsForRefactorFull,
|
||||
CommandNames.OrganizeImports,
|
||||
CommandNames.OrganizeImportsFull,
|
||||
];
|
||||
|
||||
it("should not throw when commands are executed with invalid arguments", () => {
|
||||
|
||||
@@ -629,6 +629,10 @@ namespace ts.server {
|
||||
};
|
||||
}
|
||||
|
||||
organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
|
||||
return notImplemented();
|
||||
}
|
||||
|
||||
private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
|
||||
return edits.map(edit => {
|
||||
const fileName = edit.fileName;
|
||||
|
||||
@@ -113,6 +113,10 @@ namespace ts.server.protocol {
|
||||
/* @internal */
|
||||
GetEditsForRefactorFull = "getEditsForRefactor-full",
|
||||
|
||||
OrganizeImports = "organizeImports",
|
||||
/* @internal */
|
||||
OrganizeImportsFull = "organizeImports-full",
|
||||
|
||||
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
|
||||
}
|
||||
|
||||
@@ -547,6 +551,27 @@ namespace ts.server.protocol {
|
||||
renameFilename?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize imports by:
|
||||
* 1) Removing unused imports
|
||||
* 2) Coalescing imports from the same module
|
||||
* 3) Sorting imports
|
||||
*/
|
||||
export interface OrganizeImportsRequest extends Request {
|
||||
command: CommandTypes.OrganizeImports;
|
||||
arguments: OrganizeImportsRequestArgs;
|
||||
}
|
||||
|
||||
export type OrganizeImportsScope = GetCombinedCodeFixScope;
|
||||
|
||||
export interface OrganizeImportsRequestArgs {
|
||||
scope: OrganizeImportsScope;
|
||||
}
|
||||
|
||||
export interface OrganizeImportsResponse extends Response {
|
||||
edits: ReadonlyArray<FileCodeEdits>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for the available codefixes at a specific position.
|
||||
*/
|
||||
|
||||
@@ -1597,6 +1597,19 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private organizeImports({ scope }: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
|
||||
Debug.assert(scope.type === "file");
|
||||
const { file, project } = this.getFileAndProject(scope.args);
|
||||
const formatOptions = this.projectService.getFormatCodeOptions(file);
|
||||
const changes = project.getLanguageService().organizeImports({ type: "file", fileName: file }, formatOptions);
|
||||
if (simplifiedResult) {
|
||||
return this.mapTextChangesToCodeEdits(project, changes);
|
||||
}
|
||||
else {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
|
||||
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeAction> | ReadonlyArray<CodeAction> {
|
||||
if (args.errorCodes.length === 0) {
|
||||
return undefined;
|
||||
@@ -2041,6 +2054,12 @@ namespace ts.server {
|
||||
},
|
||||
[CommandNames.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => {
|
||||
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false));
|
||||
},
|
||||
[CommandNames.OrganizeImports]: (request: protocol.OrganizeImportsRequest) => {
|
||||
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ true));
|
||||
},
|
||||
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
|
||||
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1848,6 +1848,58 @@ namespace ts {
|
||||
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext });
|
||||
}
|
||||
|
||||
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
|
||||
synchronizeHostData();
|
||||
Debug.assert(scope.type === "file");
|
||||
const sourceFile = getValidSourceFile(scope.fileName);
|
||||
const formatContext = formatting.getFormatContext(formatOptions);
|
||||
|
||||
// All of the (old) ImportDeclarations in the file, in syntactic order.
|
||||
const oldImportDecls: ImportDeclaration[] = [];
|
||||
|
||||
forEachChild(sourceFile, node => {
|
||||
cancellationToken.throwIfCancellationRequested();
|
||||
if (isImportDeclaration(node)) {
|
||||
oldImportDecls.push(node);
|
||||
}
|
||||
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
|
||||
});
|
||||
|
||||
if (oldImportDecls.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const usedImportDecls = removeUnusedImports(oldImportDecls);
|
||||
const sortedImportDecls = sortImports(usedImportDecls);
|
||||
const coalescedImportDecls = coalesceImports(sortedImportDecls);
|
||||
|
||||
// All of the (new) ImportDeclarations in the file, in sorted order.
|
||||
const newImportDecls = coalescedImportDecls;
|
||||
|
||||
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext });
|
||||
|
||||
// NB: Stopping before i === 0
|
||||
for (let i = oldImportDecls.length - 1; i > 0; i--) {
|
||||
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
|
||||
}
|
||||
|
||||
if (newImportDecls.length === 0) {
|
||||
changeTracker.deleteNode(sourceFile, oldImportDecls[0]);
|
||||
}
|
||||
else {
|
||||
// Delete the surrounding trivia because it will have been retained in newImportDecls.
|
||||
const replaceOptions = {
|
||||
useNonAdjustedStartPosition: false,
|
||||
useNonAdjustedEndPosition: false,
|
||||
suffix: getNewLineOrDefaultFromHost(host, formatOptions),
|
||||
};
|
||||
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions);
|
||||
}
|
||||
|
||||
const changes = changeTracker.getChanges();
|
||||
return changes;
|
||||
}
|
||||
|
||||
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
|
||||
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
|
||||
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
@@ -2143,6 +2195,7 @@ namespace ts {
|
||||
getCodeFixesAtPosition,
|
||||
getCombinedCodeFix,
|
||||
applyCodeActionCommand,
|
||||
organizeImports,
|
||||
getEmitOutput,
|
||||
getNonBoundSourceFile,
|
||||
getSourceFile,
|
||||
@@ -2268,4 +2321,238 @@ namespace ts {
|
||||
}
|
||||
|
||||
objectAllocator = getServicesObjectAllocator();
|
||||
|
||||
function removeUnusedImports(oldImports: ReadonlyArray<ImportDeclaration>) {
|
||||
return oldImports; // TODO (https://github.com/Microsoft/TypeScript/issues/10020)
|
||||
}
|
||||
|
||||
/* @internal */ // Internal for testing
|
||||
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) {
|
||||
if (oldImports.length < 2) {
|
||||
return oldImports;
|
||||
}
|
||||
|
||||
// NB: declaration order determines sort order
|
||||
const enum ModuleNameKind {
|
||||
NonRelative,
|
||||
Relative,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
const importRecords = oldImports.map(createImportRecord);
|
||||
|
||||
const sortedRecords = stableSort(importRecords, (import1, import2) => {
|
||||
const { name: name1, kind: kind1 } = import1;
|
||||
const { name: name2, kind: kind2 } = import2;
|
||||
|
||||
if (kind1 !== kind2) {
|
||||
return kind1 < kind2
|
||||
? Comparison.LessThan
|
||||
: Comparison.GreaterThan;
|
||||
}
|
||||
|
||||
// Note that we're using simple equality, retaining case-sensitivity.
|
||||
if (name1 !== name2) {
|
||||
return name1 < name2
|
||||
? Comparison.LessThan
|
||||
: Comparison.GreaterThan;
|
||||
}
|
||||
|
||||
return Comparison.EqualTo;
|
||||
});
|
||||
|
||||
return sortedRecords.map(r => r.importDeclaration);
|
||||
|
||||
function createImportRecord(importDeclaration: ImportDeclaration) {
|
||||
const specifier = importDeclaration.moduleSpecifier;
|
||||
const name = getExternalModuleName(specifier);
|
||||
if (name) {
|
||||
const isRelative = isExternalModuleNameRelative(name);
|
||||
return { importDeclaration, name, kind: isRelative ? ModuleNameKind.Relative : ModuleNameKind.NonRelative }
|
||||
}
|
||||
|
||||
return { importDeclaration, name: specifier.getText(), kind: ModuleNameKind.Invalid };
|
||||
}
|
||||
}
|
||||
|
||||
function getExternalModuleName(specifier: Expression) {
|
||||
return isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier)
|
||||
? specifier.text
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sortedImports a non-empty list of ImportDeclarations, sorted by module name.
|
||||
*/
|
||||
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
|
||||
Debug.assert(length(sortedImports) > 0);
|
||||
|
||||
const groups: ImportDeclaration[][] = [];
|
||||
|
||||
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier);
|
||||
let group: ImportDeclaration[] = [];
|
||||
|
||||
for (const importDeclaration of sortedImports) {
|
||||
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier);
|
||||
if (moduleName && moduleName === groupName) {
|
||||
group.push(importDeclaration);
|
||||
}
|
||||
else if (group.length) {
|
||||
groups.push(group);
|
||||
|
||||
groupName = moduleName;
|
||||
group = [importDeclaration];
|
||||
}
|
||||
}
|
||||
|
||||
if (group.length) {
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/* @internal */ // Internal for testing
|
||||
/**
|
||||
* @param sortedImports a list of ImportDeclarations, sorted by module name.
|
||||
*/
|
||||
export function coalesceImports(sortedImports: ReadonlyArray<ImportDeclaration>) {
|
||||
if (sortedImports.length === 0) {
|
||||
return sortedImports;
|
||||
}
|
||||
|
||||
const coalescedImports: ImportDeclaration[] = [];
|
||||
|
||||
const groupedImports = groupSortedImports(sortedImports);
|
||||
for (const importGroup of groupedImports) {
|
||||
|
||||
let seenImportWithoutClause = false;
|
||||
|
||||
const defaultImports: Identifier[] = [];
|
||||
const namespaceImports: NamespaceImport[] = [];
|
||||
const namedImports: NamedImports[] = [];
|
||||
|
||||
for (const importDeclaration of importGroup) {
|
||||
if (importDeclaration.importClause === undefined) {
|
||||
// Only the first such import is interesting - the others are redundant.
|
||||
// Note: Unfortunately, we will lose trivia that was on this node.
|
||||
if (!seenImportWithoutClause) {
|
||||
coalescedImports.push(importDeclaration);
|
||||
}
|
||||
|
||||
seenImportWithoutClause = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const { name, namedBindings } = importDeclaration.importClause;
|
||||
|
||||
if (name) {
|
||||
defaultImports.push(name);
|
||||
}
|
||||
|
||||
if (namedBindings) {
|
||||
if (isNamespaceImport(namedBindings)) {
|
||||
namespaceImports.push(namedBindings);
|
||||
}
|
||||
else {
|
||||
namedImports.push(namedBindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normally, we don't combine default and namespace imports, but it would be silly to
|
||||
// produce two import declarations in this special case.
|
||||
if (defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
|
||||
// Add the namespace import to the existing default ImportDeclaration.
|
||||
const defaultImportClause = defaultImports[0].parent as ImportClause;
|
||||
coalescedImports.push(
|
||||
updateImportDeclarationAndClause(defaultImportClause, defaultImportClause.name, namespaceImports[0]));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// For convenience, we cheat and do a little sorting during coalescing.
|
||||
// Seems reasonable since we're restructuring so much anyway.
|
||||
const sortedNamespaceImports = stableSort(namespaceImports, (n1, n2) => compareIdentifiers(n1.name, n2.name));
|
||||
|
||||
for (const namespaceImport of sortedNamespaceImports) {
|
||||
// Drop the name, if any
|
||||
coalescedImports.push(
|
||||
updateImportDeclarationAndClause(namespaceImport.parent, /*name*/ undefined, namespaceImport));
|
||||
}
|
||||
|
||||
if (defaultImports.length === 0 && namedImports.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newDefaultImport: Identifier = undefined;
|
||||
const newImportSpecifiers: ImportSpecifier[] = [];
|
||||
if (defaultImports.length === 1) {
|
||||
newDefaultImport = defaultImports[0];
|
||||
}
|
||||
else {
|
||||
for (const defaultImport of defaultImports) {
|
||||
newImportSpecifiers.push(
|
||||
createImportSpecifier(createIdentifier("default"), defaultImport));
|
||||
}
|
||||
}
|
||||
|
||||
for (const namedImport of namedImports) {
|
||||
for (const specifier of namedImport.elements) {
|
||||
newImportSpecifiers.push(specifier);
|
||||
}
|
||||
}
|
||||
|
||||
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => {
|
||||
const nameComparison = compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name);
|
||||
return nameComparison != Comparison.EqualTo
|
||||
? nameComparison
|
||||
: compareIdentifiers(s1.name, s2.name);
|
||||
});
|
||||
|
||||
const importClause = defaultImports.length > 0
|
||||
? defaultImports[0].parent as ImportClause
|
||||
: namedImports[0].parent;
|
||||
|
||||
const newNamedImports = sortedImportSpecifiers.length === 0
|
||||
? undefined
|
||||
: namedImports.length === 0
|
||||
? createNamedImports(sortedImportSpecifiers)
|
||||
: updateNamedImports(namedImports[0], sortedImportSpecifiers);
|
||||
|
||||
coalescedImports.push(
|
||||
updateImportDeclarationAndClause(importClause, newDefaultImport, newNamedImports));
|
||||
}
|
||||
|
||||
return coalescedImports;
|
||||
|
||||
// `undefined` is the min value.
|
||||
function compareIdentifiers(s1: Identifier | undefined, s2: Identifier | undefined) {
|
||||
return s1 === undefined
|
||||
? s2 === undefined
|
||||
? Comparison.EqualTo
|
||||
: Comparison.LessThan
|
||||
: s2 === undefined
|
||||
? Comparison.GreaterThan
|
||||
: s1.text < s2.text
|
||||
? Comparison.LessThan
|
||||
: s1.text > s2.text
|
||||
? Comparison.GreaterThan
|
||||
: Comparison.EqualTo;
|
||||
}
|
||||
|
||||
function updateImportDeclarationAndClause(
|
||||
importClause: ImportClause,
|
||||
name: Identifier | undefined,
|
||||
namedBindings: NamedImportBindings | undefined) {
|
||||
|
||||
const importDeclaration = importClause.parent;
|
||||
return updateImportDeclaration(
|
||||
importDeclaration,
|
||||
importDeclaration.decorators,
|
||||
importDeclaration.modifiers,
|
||||
updateImportClause(importClause, name, namedBindings),
|
||||
importDeclaration.moduleSpecifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,7 @@ namespace ts {
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
|
||||
|
||||
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
|
||||
|
||||
@@ -326,6 +327,8 @@ namespace ts {
|
||||
|
||||
export interface CombinedCodeFixScope { type: "file"; fileName: string; }
|
||||
|
||||
export type OrganizeImportsScope = CombinedCodeFixScope;
|
||||
|
||||
export interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
|
||||
@@ -4135,6 +4135,7 @@ declare namespace ts {
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
|
||||
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
|
||||
getProgram(): Program;
|
||||
dispose(): void;
|
||||
@@ -4143,6 +4144,7 @@ declare namespace ts {
|
||||
type: "file";
|
||||
fileName: string;
|
||||
}
|
||||
type OrganizeImportsScope = CombinedCodeFixScope;
|
||||
interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
@@ -5078,6 +5080,7 @@ declare namespace ts.server.protocol {
|
||||
GetSupportedCodeFixes = "getSupportedCodeFixes",
|
||||
GetApplicableRefactors = "getApplicableRefactors",
|
||||
GetEditsForRefactor = "getEditsForRefactor",
|
||||
OrganizeImports = "organizeImports",
|
||||
}
|
||||
/**
|
||||
* A TypeScript Server message
|
||||
@@ -5429,6 +5432,23 @@ declare namespace ts.server.protocol {
|
||||
renameLocation?: Location;
|
||||
renameFilename?: string;
|
||||
}
|
||||
/**
|
||||
* Organize imports by:
|
||||
* 1) Removing unused imports
|
||||
* 2) Coalescing imports from the same module
|
||||
* 3) Sorting imports
|
||||
*/
|
||||
interface OrganizeImportsRequest extends Request {
|
||||
command: CommandTypes.OrganizeImports;
|
||||
arguments: OrganizeImportsRequestArgs;
|
||||
}
|
||||
type OrganizeImportsScope = GetCombinedCodeFixScope;
|
||||
interface OrganizeImportsRequestArgs {
|
||||
scope: OrganizeImportsScope;
|
||||
}
|
||||
interface OrganizeImportsResponse extends Response {
|
||||
edits: ReadonlyArray<FileCodeEdits>;
|
||||
}
|
||||
/**
|
||||
* Request for the available codefixes at a specific position.
|
||||
*/
|
||||
@@ -7282,6 +7302,7 @@ declare namespace ts.server {
|
||||
private extractPositionAndRange(args, scriptInfo);
|
||||
private getApplicableRefactors(args);
|
||||
private getEditsForRefactor(args, simplifiedResult);
|
||||
private organizeImports({scope}, simplifiedResult);
|
||||
private getCodeFixes(args, simplifiedResult);
|
||||
private getCombinedCodeFix({scope, fixId}, simplifiedResult);
|
||||
private applyCodeActionCommand(args);
|
||||
|
||||
@@ -4387,6 +4387,7 @@ declare namespace ts {
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
|
||||
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
|
||||
getProgram(): Program;
|
||||
dispose(): void;
|
||||
@@ -4395,6 +4396,7 @@ declare namespace ts {
|
||||
type: "file";
|
||||
fileName: string;
|
||||
}
|
||||
type OrganizeImportsScope = CombinedCodeFixScope;
|
||||
interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
|
||||
15
tests/baselines/reference/organizeImports/CoalesceTrivia.ts
Normal file
15
tests/baselines/reference/organizeImports/CoalesceTrivia.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// ==ORIGINAL==
|
||||
|
||||
/*A*/import /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I
|
||||
/*J*/import /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R
|
||||
|
||||
F1();
|
||||
F2();
|
||||
|
||||
// ==ORGANIZED==
|
||||
|
||||
/*A*/ import { /*L*/ F1 /*M*/, /*C*/ F2 /*D*/ } /*E*/ from "lib" /*G*/; /*H*/ //I
|
||||
|
||||
|
||||
F1();
|
||||
F2();
|
||||
18
tests/baselines/reference/organizeImports/MoveToTop.ts
Normal file
18
tests/baselines/reference/organizeImports/MoveToTop.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// ==ORIGINAL==
|
||||
|
||||
import { F1, F2 } from "lib";
|
||||
F1();
|
||||
F2();
|
||||
import * as NS from "lib";
|
||||
NS.F1();
|
||||
import D from "lib";
|
||||
D();
|
||||
|
||||
// ==ORGANIZED==
|
||||
|
||||
import * as NS from "lib";
|
||||
import D, { F1, F2 } from "lib";
|
||||
F1();
|
||||
F2();
|
||||
NS.F1();
|
||||
D();
|
||||
20
tests/baselines/reference/organizeImports/Simple.ts
Normal file
20
tests/baselines/reference/organizeImports/Simple.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ==ORIGINAL==
|
||||
|
||||
import { F1, F2 } from "lib";
|
||||
import * as NS from "lib";
|
||||
import D from "lib";
|
||||
|
||||
NS.F1();
|
||||
D();
|
||||
F1();
|
||||
F2();
|
||||
|
||||
// ==ORGANIZED==
|
||||
|
||||
import * as NS from "lib";
|
||||
import D, { F1, F2 } from "lib";
|
||||
|
||||
NS.F1();
|
||||
D();
|
||||
F1();
|
||||
F2();
|
||||
10
tests/baselines/reference/organizeImports/SortTrivia.ts
Normal file
10
tests/baselines/reference/organizeImports/SortTrivia.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// ==ORIGINAL==
|
||||
|
||||
/*A*/import /*B*/ "lib2" /*C*/;/*D*/ //E
|
||||
/*F*/import /*G*/ "lib1" /*H*/;/*I*/ //J
|
||||
|
||||
// ==ORGANIZED==
|
||||
|
||||
/*F*/ import "lib1" /*H*/; /*I*/ //J
|
||||
/*A*/ import "lib2" /*C*/; /*D*/ //E
|
||||
|
||||
Reference in New Issue
Block a user