Skip to content

Commit 0ce72ef

Browse files
authoredSep 29, 2022
Add option to OrganizeImports for removal only (#50931)
* Remove unused imports * Lint * Update baselines * Make mode paramter required * Clean up
1 parent 42f9143 commit 0ce72ef

11 files changed

+101
-29
lines changed
 

‎src/harness/fourslashImpl.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,8 @@ namespace FourSlash {
529529
}
530530
}
531531

532-
public verifyOrganizeImports(newContent: string) {
533-
const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file" }, this.formatCodeSettings, ts.emptyOptions);
532+
public verifyOrganizeImports(newContent: string, mode?: ts.OrganizeImportsMode) {
533+
const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file", mode }, this.formatCodeSettings, ts.emptyOptions);
534534
this.applyChanges(changes);
535535
this.verifyFileContent(this.activeFile.fileName, newContent);
536536
}

‎src/harness/fourslashInterfaceImpl.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,8 @@ namespace FourSlashInterface {
624624
this.state.noMoveToNewFile();
625625
}
626626

627-
public organizeImports(newContent: string) {
628-
this.state.verifyOrganizeImports(newContent);
627+
public organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void {
628+
this.state.verifyOrganizeImports(newContent, mode);
629629
}
630630
}
631631

‎src/server/protocol.ts

+8
Original file line numberDiff line numberDiff line change
@@ -681,9 +681,17 @@ namespace ts.server.protocol {
681681

682682
export type OrganizeImportsScope = GetCombinedCodeFixScope;
683683

684+
export const enum OrganizeImportsMode {
685+
All = "All",
686+
SortAndCombine = "SortAndCombine",
687+
RemoveUnused = "RemoveUnused",
688+
}
689+
684690
export interface OrganizeImportsRequestArgs {
685691
scope: OrganizeImportsScope;
692+
/** @deprecated Use `mode` instead */
686693
skipDestructiveCodeActions?: boolean;
694+
mode?: OrganizeImportsMode;
687695
}
688696

689697
export interface OrganizeImportsResponse extends Response {

‎src/server/session.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2533,7 +2533,7 @@ namespace ts.server {
25332533
const changes = project.getLanguageService().organizeImports(
25342534
{
25352535
fileName: file,
2536-
skipDestructiveCodeActions: args.skipDestructiveCodeActions,
2536+
mode: args.mode as OrganizeImportsMode | undefined ?? (args.skipDestructiveCodeActions ? OrganizeImportsMode.SortAndCombine : undefined),
25372537
type: "file",
25382538
},
25392539
this.getFormatOptions(file),

‎src/services/organizeImports.ts

+34-22
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,51 @@ namespace ts.OrganizeImports {
1313
host: LanguageServiceHost,
1414
program: Program,
1515
preferences: UserPreferences,
16-
skipDestructiveCodeActions?: boolean
16+
mode: OrganizeImportsMode,
1717
) {
1818
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences });
19-
20-
const coalesceAndOrganizeImports = (importGroup: readonly ImportDeclaration[]) => stableSort(
21-
coalesceImports(removeUnusedImports(importGroup, sourceFile, program, skipDestructiveCodeActions)),
22-
(s1, s2) => compareImportsOrRequireStatements(s1, s2));
19+
const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All;
20+
const shouldCombine = shouldSort; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future.
21+
const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All;
22+
const maybeRemove = shouldRemove ? removeUnusedImports : identity;
23+
const maybeCoalesce = shouldCombine ? coalesceImports : identity;
24+
const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => {
25+
const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program));
26+
return shouldSort
27+
? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2))
28+
: processedDeclarations;
29+
};
2330

2431
// All of the old ImportDeclarations in the file, in syntactic order.
2532
const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration));
26-
topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports));
33+
topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier));
2734

28-
// All of the old ExportDeclarations in the file, in syntactic order.
29-
const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration);
30-
organizeImportsWorker(topLevelExportDecls, coalesceExports);
35+
// Exports are always used
36+
if (mode !== OrganizeImportsMode.RemoveUnused) {
37+
// All of the old ExportDeclarations in the file, in syntactic order.
38+
const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration);
39+
organizeImportsWorker(topLevelExportDecls, coalesceExports);
40+
}
3141

3242
for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) {
3343
if (!ambientModule.body) continue;
3444

3545
const ambientModuleImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(isImportDeclaration));
36-
ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, coalesceAndOrganizeImports));
46+
ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier));
3747

38-
const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration);
39-
organizeImportsWorker(ambientModuleExportDecls, coalesceExports);
48+
// Exports are always used
49+
if (mode !== OrganizeImportsMode.RemoveUnused) {
50+
const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration);
51+
organizeImportsWorker(ambientModuleExportDecls, coalesceExports);
52+
}
4053
}
4154

4255
return changeTracker.getChanges();
4356

4457
function organizeImportsWorker<T extends ImportDeclaration | ExportDeclaration>(
4558
oldImportDecls: readonly T[],
46-
coalesce: (group: readonly T[]) => readonly T[]) {
47-
59+
coalesce: (group: readonly T[]) => readonly T[],
60+
) {
4861
if (length(oldImportDecls) === 0) {
4962
return;
5063
}
@@ -56,8 +69,12 @@ namespace ts.OrganizeImports {
5669
// but the consequences of being wrong are very minor.
5770
suppressLeadingTrivia(oldImportDecls[0]);
5871

59-
const oldImportGroups = group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!);
60-
const sortedImportGroups = stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier));
72+
const oldImportGroups = shouldCombine
73+
? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!)
74+
: [oldImportDecls];
75+
const sortedImportGroups = shouldSort
76+
? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier))
77+
: oldImportGroups;
6178
const newImportDecls = flatMap(sortedImportGroups, importGroup =>
6279
getExternalModuleName(importGroup[0].moduleSpecifier!)
6380
? coalesce(importGroup)
@@ -129,12 +146,7 @@ namespace ts.OrganizeImports {
129146
return false;
130147
}
131148

132-
function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program, skipDestructiveCodeActions: boolean | undefined) {
133-
// As a precaution, consider unused import detection to be destructive (GH #43051)
134-
if (skipDestructiveCodeActions) {
135-
return oldImports;
136-
}
137-
149+
function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) {
138150
const typeChecker = program.getTypeChecker();
139151
const compilerOptions = program.getCompilerOptions();
140152
const jsxNamespace = typeChecker.getJsxNamespace(sourceFile);

‎src/services/services.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2076,7 +2076,8 @@ namespace ts {
20762076
const sourceFile = getValidSourceFile(args.fileName);
20772077
const formatContext = formatting.getFormatContext(formatOptions, host);
20782078

2079-
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences, args.skipDestructiveCodeActions);
2079+
const mode = args.mode ?? (args.skipDestructiveCodeActions ? OrganizeImportsMode.SortAndCombine : OrganizeImportsMode.All);
2080+
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences, mode);
20802081
}
20812082

20822083
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] {

‎src/services/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,16 @@ namespace ts {
575575

576576
export interface CombinedCodeFixScope { type: "file"; fileName: string; }
577577

578+
export const enum OrganizeImportsMode {
579+
All = "All",
580+
SortAndCombine = "SortAndCombine",
581+
RemoveUnused = "RemoveUnused",
582+
}
583+
578584
export interface OrganizeImportsArgs extends CombinedCodeFixScope {
585+
/** @deprecated Use `mode` instead */
579586
skipDestructiveCodeActions?: boolean;
587+
mode?: OrganizeImportsMode;
580588
}
581589

582590
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";

‎tests/baselines/reference/api/tsserverlibrary.d.ts

+14
Original file line numberDiff line numberDiff line change
@@ -6032,8 +6032,15 @@ declare namespace ts {
60326032
type: "file";
60336033
fileName: string;
60346034
}
6035+
enum OrganizeImportsMode {
6036+
All = "All",
6037+
SortAndCombine = "SortAndCombine",
6038+
RemoveUnused = "RemoveUnused"
6039+
}
60356040
interface OrganizeImportsArgs extends CombinedCodeFixScope {
6041+
/** @deprecated Use `mode` instead */
60366042
skipDestructiveCodeActions?: boolean;
6043+
mode?: OrganizeImportsMode;
60376044
}
60386045
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
60396046
enum CompletionTriggerKind {
@@ -7616,9 +7623,16 @@ declare namespace ts.server.protocol {
76167623
arguments: OrganizeImportsRequestArgs;
76177624
}
76187625
type OrganizeImportsScope = GetCombinedCodeFixScope;
7626+
enum OrganizeImportsMode {
7627+
All = "All",
7628+
SortAndCombine = "SortAndCombine",
7629+
RemoveUnused = "RemoveUnused"
7630+
}
76197631
interface OrganizeImportsRequestArgs {
76207632
scope: OrganizeImportsScope;
7633+
/** @deprecated Use `mode` instead */
76217634
skipDestructiveCodeActions?: boolean;
7635+
mode?: OrganizeImportsMode;
76227636
}
76237637
interface OrganizeImportsResponse extends Response {
76247638
body: readonly FileCodeEdits[];

‎tests/baselines/reference/api/typescript.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -6032,8 +6032,15 @@ declare namespace ts {
60326032
type: "file";
60336033
fileName: string;
60346034
}
6035+
enum OrganizeImportsMode {
6036+
All = "All",
6037+
SortAndCombine = "SortAndCombine",
6038+
RemoveUnused = "RemoveUnused"
6039+
}
60356040
interface OrganizeImportsArgs extends CombinedCodeFixScope {
6041+
/** @deprecated Use `mode` instead */
60366042
skipDestructiveCodeActions?: boolean;
6043+
mode?: OrganizeImportsMode;
60376044
}
60386045
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
60396046
enum CompletionTriggerKind {

‎tests/cases/fourslash/fourslash.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ declare module ts {
9191
Message
9292
}
9393

94+
enum OrganizeImportsMode {
95+
All = "All",
96+
SortAndCombine = "SortAndCombine",
97+
RemoveUnused = "RemoveUnused",
98+
}
99+
94100
interface DiagnosticMessage {
95101
key: string;
96102
category: DiagnosticCategory;
@@ -442,7 +448,7 @@ declare namespace FourSlashInterface {
442448

443449
generateTypes(...options: GenerateTypesOptions[]): void;
444450

445-
organizeImports(newContent: string): void;
451+
organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void;
446452

447453
toggleLineComment(newFileContent: string): void;
448454
toggleMultilineComment(newFileContent: string): void;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// import { c, b, a } from "foo";
4+
//// import d, { e } from "bar";
5+
//// import * as f from "baz";
6+
//// import { g } from "foo";
7+
////
8+
//// export { g, e, b, c };
9+
10+
verify.organizeImports(
11+
`import { c, b } from "foo";
12+
import { e } from "bar";
13+
import { g } from "foo";
14+
15+
export { g, e, b, c };`,
16+
ts.OrganizeImportsMode.RemoveUnused);

0 commit comments

Comments
 (0)
Please sign in to comment.