Fix organizeImports with type-only imports (#36807)

This commit is contained in:
Andrew Branch
2020-02-14 15:24:39 -08:00
committed by GitHub
parent 9b518c8f53
commit b82d3207e8
3 changed files with 201 additions and 108 deletions

View File

@@ -174,7 +174,7 @@ namespace ts.OrganizeImports {
return importGroup;
}
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getCategorizedImports(importGroup);
const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup);
const coalescedImports: ImportDeclaration[] = [];
@@ -182,107 +182,126 @@ namespace ts.OrganizeImports {
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 defaultImport = defaultImports[0];
coalescedImports.push(
updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217
for (const group of [regularImports, typeOnlyImports]) {
const isTypeOnly = group === typeOnlyImports;
const { defaultImports, namespaceImports, namedImports } = group;
// Normally, we don't combine default and namespace imports, but it would be silly to
// produce two import declarations in this special case.
if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
// Add the namespace import to the existing default ImportDeclaration.
const defaultImport = defaultImports[0];
coalescedImports.push(
updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217
return coalescedImports;
}
continue;
}
const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) =>
compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217
const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) =>
compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217
for (const namespaceImport of sortedNamespaceImports) {
// Drop the name, if any
coalescedImports.push(
updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217
}
for (const namespaceImport of sortedNamespaceImports) {
// Drop the name, if any
coalescedImports.push(
updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217
}
if (defaultImports.length === 0 && namedImports.length === 0) {
return coalescedImports;
}
if (defaultImports.length === 0 && namedImports.length === 0) {
continue;
}
let newDefaultImport: Identifier | undefined;
const newImportSpecifiers: ImportSpecifier[] = [];
if (defaultImports.length === 1) {
newDefaultImport = defaultImports[0].importClause!.name;
}
else {
for (const defaultImport of defaultImports) {
newImportSpecifiers.push(
createImportSpecifier(createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217
let newDefaultImport: Identifier | undefined;
const newImportSpecifiers: ImportSpecifier[] = [];
if (defaultImports.length === 1) {
newDefaultImport = defaultImports[0].importClause!.name;
}
else {
for (const defaultImport of defaultImports) {
newImportSpecifiers.push(
createImportSpecifier(createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217
}
}
newImportSpecifiers.push(...flatMap(namedImports, i => (i.importClause!.namedBindings as NamedImports).elements)); // TODO: GH#18217
const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers);
const importDecl = defaultImports.length > 0
? defaultImports[0]
: namedImports[0];
const newNamedImports = sortedImportSpecifiers.length === 0
? newDefaultImport
? undefined
: createNamedImports(emptyArray)
: namedImports.length === 0
? createNamedImports(sortedImportSpecifiers)
: updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217
// Type-only imports are not allowed to combine
if (isTypeOnly && newDefaultImport && newNamedImports) {
coalescedImports.push(
updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined));
coalescedImports.push(
updateImportDeclarationAndClause(namedImports[0] ?? importDecl, /*name*/ undefined, newNamedImports));
}
else {
coalescedImports.push(
updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports));
}
}
newImportSpecifiers.push(...flatMap(namedImports, i => (i.importClause!.namedBindings as NamedImports).elements)); // TODO: GH#18217
const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers);
const importDecl = defaultImports.length > 0
? defaultImports[0]
: namedImports[0];
const newNamedImports = sortedImportSpecifiers.length === 0
? newDefaultImport
? undefined
: createNamedImports(emptyArray)
: namedImports.length === 0
? createNamedImports(sortedImportSpecifiers)
: updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217
coalescedImports.push(
updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports));
return coalescedImports;
/*
* Returns entire import declarations because they may already have been rewritten and
* may lack parent pointers. The desired parts can easily be recovered based on the
* categorization.
*
* NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`.
*/
function getCategorizedImports(importGroup: readonly ImportDeclaration[]) {
let importWithoutClause: ImportDeclaration | undefined;
const defaultImports: ImportDeclaration[] = [];
const namespaceImports: ImportDeclaration[] = [];
const namedImports: ImportDeclaration[] = [];
}
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;
}
interface ImportGroup {
defaultImports: ImportDeclaration[];
namespaceImports: ImportDeclaration[];
namedImports: ImportDeclaration[];
}
const { name, namedBindings } = importDeclaration.importClause;
/*
* Returns entire import declarations because they may already have been rewritten and
* may lack parent pointers. The desired parts can easily be recovered based on the
* categorization.
*
* NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`.
*/
function getCategorizedImports(importGroup: readonly ImportDeclaration[]) {
let importWithoutClause: ImportDeclaration | undefined;
const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] };
const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] };
if (name) {
defaultImports.push(importDeclaration);
}
if (namedBindings) {
if (isNamespaceImport(namedBindings)) {
namespaceImports.push(importDeclaration);
}
else {
namedImports.push(importDeclaration);
}
}
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;
}
return {
importWithoutClause,
defaultImports,
namespaceImports,
namedImports,
};
const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports;
const { name, namedBindings } = importDeclaration.importClause;
if (name) {
group.defaultImports.push(importDeclaration);
}
if (namedBindings) {
if (isNamespaceImport(namedBindings)) {
group.namespaceImports.push(importDeclaration);
}
else {
group.namedImports.push(importDeclaration);
}
}
}
return {
importWithoutClause,
typeOnlyImports,
regularImports,
};
}
// Internal for testing
@@ -294,7 +313,7 @@ namespace ts.OrganizeImports {
return exportGroup;
}
const { exportWithoutClause, namedExports } = getCategorizedExports(exportGroup);
const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup);
const coalescedExports: ExportDeclaration[] = [];
@@ -302,29 +321,30 @@ namespace ts.OrganizeImports {
coalescedExports.push(exportWithoutClause);
}
if (namedExports.length === 0) {
return coalescedExports;
for (const exportGroup of [namedExports, typeOnlyExports]) {
if (exportGroup.length === 0) {
continue;
}
const newExportSpecifiers: ExportSpecifier[] = [];
newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray));
const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers);
const exportDecl = exportGroup[0];
coalescedExports.push(
updateExportDeclaration(
exportDecl,
exportDecl.decorators,
exportDecl.modifiers,
exportDecl.exportClause && (
isNamedExports(exportDecl.exportClause) ?
updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) :
updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name)
),
exportDecl.moduleSpecifier,
exportDecl.isTypeOnly));
}
const newExportSpecifiers: ExportSpecifier[] = [];
newExportSpecifiers.push(...flatMap(namedExports, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray));
const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers);
const exportDecl = namedExports[0];
coalescedExports.push(
updateExportDeclaration(
exportDecl,
exportDecl.decorators,
exportDecl.modifiers,
exportDecl.exportClause && (
isNamedExports(exportDecl.exportClause) ?
updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) :
updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name)
),
exportDecl.moduleSpecifier,
exportDecl.isTypeOnly));
return coalescedExports;
/*
@@ -335,6 +355,7 @@ namespace ts.OrganizeImports {
function getCategorizedExports(exportGroup: readonly ExportDeclaration[]) {
let exportWithoutClause: ExportDeclaration | undefined;
const namedExports: ExportDeclaration[] = [];
const typeOnlyExports: ExportDeclaration[] = [];
for (const exportDeclaration of exportGroup) {
if (exportDeclaration.exportClause === undefined) {
@@ -342,6 +363,9 @@ namespace ts.OrganizeImports {
// Note: Unfortunately, we will lose trivia that was on this node.
exportWithoutClause = exportWithoutClause || exportDeclaration;
}
else if (exportDeclaration.isTypeOnly) {
typeOnlyExports.push(exportDeclaration);
}
else {
namedExports.push(exportDeclaration);
}
@@ -350,6 +374,7 @@ namespace ts.OrganizeImports {
return {
exportWithoutClause,
namedExports,
typeOnlyExports,
};
}
}