-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce an organizeImports command #21909
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
/* @internal */ | ||
namespace ts.OrganizeImports { | ||
export function organizeImports( | ||
sourceFile: SourceFile, | ||
formatContext: formatting.FormatContext, | ||
host: LanguageServiceHost) { | ||
|
||
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule) | ||
|
||
// All of the old ImportDeclarations in the file, in syntactic order. | ||
const oldImportDecls = sourceFile.statements.filter(isImportDeclaration); | ||
|
||
if (oldImportDecls.length === 0) { | ||
return []; | ||
} | ||
|
||
const oldValidImportDecls = oldImportDecls.filter(importDecl => getExternalModuleName(importDecl.moduleSpecifier)); | ||
const oldInvalidImportDecls = oldImportDecls.filter(importDecl => !getExternalModuleName(importDecl.moduleSpecifier)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this is needed -- we're basically just deleting these and adding them back, could just ignore them entirely and make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If they are scattered throughout the file, this will group them together at the top. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. For the case where you do keep the invalid ones, could use a helper function and write |
||
|
||
// All of the new ImportDeclarations in the file, in sorted order. | ||
const newImportDecls = coalesceImports(sortImports(removeUnusedImports(oldValidImportDecls))).concat(oldInvalidImportDecls); | ||
|
||
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext }); | ||
|
||
// Delete or replace the first import. | ||
if (newImportDecls.length === 0) { | ||
changeTracker.deleteNode(sourceFile, oldImportDecls[0]); | ||
} | ||
else { | ||
// Note: Delete the surrounding trivia because it will have been retained in newImportDecls. | ||
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, { | ||
useNonAdjustedStartPosition: false, | ||
useNonAdjustedEndPosition: false, | ||
suffix: getNewLineOrDefaultFromHost(host, formatContext.options), | ||
}); | ||
} | ||
|
||
// Delete any subsequent imports. | ||
for (let i = 1; i < oldImportDecls.length; i++) { | ||
changeTracker.deleteNode(sourceFile, oldImportDecls[i]); | ||
} | ||
|
||
return changeTracker.getChanges(); | ||
} | ||
|
||
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>) { | ||
return stableSort(oldImports, (import1, import2) => { | ||
const name1 = getExternalModuleName(import1.moduleSpecifier); | ||
const name2 = getExternalModuleName(import2.moduleSpecifier); | ||
Debug.assert(name1 !== undefined); | ||
Debug.assert(name2 !== undefined); | ||
return compareBooleans(isExternalModuleNameRelative(name1), isExternalModuleNameRelative(name2)) || | ||
compareStringsCaseSensitive(name1, name2); | ||
}); | ||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
return groupBy(sortedImports, i => getExternalModuleName(i.moduleSpecifier));
}
function groupBy<T>(values: ReadonlyArray<T>, by: (value: T) => string): ReadonlyArray<ReadonlyArray<T>> {
Debug.assert(values.length !== 0);
const groups: T[][] = [];
let groupName = by(values[0]);
let group: T[] = [];
for (const value of values) {
const b = by(value);
if (b === groupName) {
group.push(value);
}
else {
if (group.length) {
groups.push(group);
}
groupName = b;
group = [value];
}
}
if (group.length) {
groups.push(group);
}
return groups;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe better: function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
return groupBy(sortedImports, (a, b) => getExternalModuleName(a.moduleSpecifier) === getExternalModuleName(b.moduleSpecifier));
}
function groupBy<T>(values: ReadonlyArray<T>, areSameGroup: (a: T, b: T) => boolean): ReadonlyArray<ReadonlyArray<T>> {
Debug.assert(values.length !== 0);
const groups: T[][] = [];
let group: T[] = [];
for (const value of values) {
if (group.length && !areSameGroup(value, group[0])) {
groups.push(group);
group = [];
}
group.push(value);
}
if (group.length) {
groups.push(group);
}
return groups;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, your improvement was pulling out a generic helper? The actual behavior is the same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, assuming invalid imports are filtered out (so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just noticed we don't need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that this isn't true group-by - it requires that the input be sorted. Is it still worth pulling out a generic helper for specialized functionality? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, this doesn't just rely on it being sorted, but on it being sorted by the same mechanism as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's why it's a private helper function. At the moment, there doesn't seem to be a need for something more general, does there? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you requested, we are now grouping before performing the other operations. |
||
|
||
const groups: ImportDeclaration[][] = []; | ||
|
||
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier); | ||
Debug.assert(groupName !== undefined); | ||
let group: ImportDeclaration[] = []; | ||
|
||
for (const importDeclaration of sortedImports) { | ||
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier); | ||
Debug.assert(moduleName !== undefined); | ||
if (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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You already tested for length at the top of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this also runs after unused import removal. |
||
return sortedImports; | ||
} | ||
|
||
const coalescedImports: ImportDeclaration[] = []; | ||
|
||
const groupedImports = groupSortedImports(sortedImports); | ||
for (const importGroup of groupedImports) { | ||
|
||
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts(importGroup); | ||
|
||
if (importWithoutClause) { | ||
coalescedImports.push(importWithoutClause); | ||
} | ||
|
||
// 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; | ||
} | ||
|
||
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)); | ||
} | ||
} | ||
|
||
newImportSpecifiers.push(...flatMap(namedImports, n => n.elements)); | ||
|
||
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => | ||
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) || | ||
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; | ||
|
||
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>) { | ||
let importWithoutClause: ImportDeclaration | undefined; | ||
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. | ||
importWithoutClause = importWithoutClause || importDeclaration; | ||
continue; | ||
} | ||
|
||
const { name, namedBindings } = importDeclaration.importClause; | ||
|
||
if (name) { | ||
defaultImports.push(name); | ||
} | ||
|
||
if (namedBindings) { | ||
if (isNamespaceImport(namedBindings)) { | ||
namespaceImports.push(namedBindings); | ||
} | ||
else { | ||
namedImports.push(namedBindings); | ||
} | ||
} | ||
} | ||
|
||
return { | ||
importWithoutClause, | ||
defaultImports, | ||
namespaceImports, | ||
namedImports, | ||
}; | ||
} | ||
|
||
function compareIdentifiers(s1: Identifier, s2: Identifier) { | ||
return compareStringsCaseSensitive(s1.text, s2.text); | ||
} | ||
|
||
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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
/// <reference path='jsTyping.ts' /> | ||
/// <reference path='navigateTo.ts' /> | ||
/// <reference path='navigationBar.ts' /> | ||
/// <reference path='organizeImports.ts' /> | ||
/// <reference path='outliningElementsCollector.ts' /> | ||
/// <reference path='patternMatcher.ts' /> | ||
/// <reference path='preProcess.ts' /> | ||
|
@@ -1848,6 +1849,15 @@ namespace ts { | |
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext }); | ||
} | ||
|
||
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these changes are substantial enough to deserve an |
||
synchronizeHostData(); | ||
Debug.assert(scope.type === "file"); | ||
const sourceFile = getValidSourceFile(scope.fileName); | ||
const formatContext = formatting.getFormatContext(formatOptions); | ||
|
||
return OrganizeImports.organizeImports(sourceFile, formatContext, host); | ||
} | ||
|
||
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>; | ||
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>; | ||
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>; | ||
|
@@ -2143,6 +2153,7 @@ namespace ts { | |
getCodeFixesAtPosition, | ||
getCombinedCodeFix, | ||
applyCodeActionCommand, | ||
organizeImports, | ||
getEmitOutput, | ||
getNonBoundSourceFile, | ||
getSourceFile, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment might be better in
organizeImports.ts
(the non-test one).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can move it when I implement remove-unused.