diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index dab5aeed461a0..662baa16089f3 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4831,5 +4831,9 @@ "Enable the 'experimentalDecorators' option in your configuration file": { "category": "Message", "code": 95074 + }, + "Merge duplicate import declaration": { + "category": "Message", + "code": 95075 } } diff --git a/src/services/refactors/mergeImportDeclaration.ts b/src/services/refactors/mergeImportDeclaration.ts new file mode 100644 index 0000000000000..7418fb246bac7 --- /dev/null +++ b/src/services/refactors/mergeImportDeclaration.ts @@ -0,0 +1,94 @@ +/* @internal */ +namespace ts.refactor.mergeImportDeclaration { + const refactorName = "Merge duplicate import declaration"; + registerRefactor(refactorName, { + getAvailableActions(context): ApplicableRefactorInfo[] { + const i = getImportToMerge(context); + if (!i) return []; + const description = Diagnostics.Merge_duplicate_import_declaration.message; + + return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === refactorName); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, t, Debug.assertDefined(getImportToMerge(context)))); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } + }); + + interface Info { + declaration: ImportDeclaration; + otherDeclarations: ImportClause[]; + } + function getImportToMerge(context: RefactorContext): Info | undefined { + const { file } = context; + const span = getRefactorContextSpan(context); + const token = getTokenAtPosition(file, span.start); + const importDecl = getParentNodeInSpan(token, file, span); + if (!importDecl || !isImportDeclaration(importDecl) || !isMergeableImport(importDecl)) return undefined; + const otherDeclarations = filter(file.statements, stmt => + !!(stmt !== importDecl && isImportDeclaration(stmt) && isMergeableImport(stmt) && (stmt.moduleSpecifier).text === (importDecl.moduleSpecifier).text) + ).map(x => cast(x, isImportDeclaration).importClause) as ImportClause[]; + return length(otherDeclarations) && allMergeDefaultImport(importDecl.importClause!, otherDeclarations) ? { + otherDeclarations, + declaration: importDecl + } : undefined; + } + + function isMergeableImport(declaration: ImportDeclaration) { + return !!(declaration.importClause && declaration.importClause.namedBindings && isNamedImports(declaration.importClause.namedBindings) && isStringLiteral(declaration.moduleSpecifier)); + } + + function allMergeDefaultImport(source: ImportClause, targets: ImportClause[]) { + if (source.name) { + return every(targets, decl => !decl.name || decl.name.text === source.name!.text); + } + else { + let firstDefaultImport: string | undefined; + return every(targets, decl => { + if (firstDefaultImport && decl.name) { + return firstDefaultImport === decl.name.text; + } + else if (decl.name) { + firstDefaultImport = decl.name.text; + } + return true; + }); + } + } + + function getImportSpecifiernName(specifier: ImportSpecifier) { + return specifier.propertyName ? specifier.propertyName.text : specifier.name.text; + } + + function doChange(sourceFile: SourceFile, changes: textChanges.ChangeTracker, info: Info): void { + const { declaration, otherDeclarations } = info; + const seensNameBindings = createMap(); + let defaultImportName = declaration.importClause!.name; + forEach((declaration.importClause!.namedBindings).elements, element => { + seensNameBindings.set(getImportSpecifiernName(element), element); + }); + forEach(otherDeclarations, decl => { + if (!declaration.importClause!.name && decl.name) { + defaultImportName = decl.name; + } + forEach((decl.namedBindings).elements, element => { + if (!seensNameBindings.has(getImportSpecifiernName(element))) { + seensNameBindings.set(getImportSpecifiernName(element), element); + } + }); + }); + changes.replaceNode(sourceFile, declaration, updateImportDeclaration( + declaration, + declaration.decorators, + declaration.modifiers, + updateImportClause( + declaration.importClause!, + defaultImportName, + createNamedImports(arrayFrom(seensNameBindings.values())) + ), + declaration.moduleSpecifier + )); + otherDeclarations.forEach(decl => { changes.delete(sourceFile, decl.parent); }); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 21be663055ae4..c7ba1a89f0b20 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -84,6 +84,7 @@ "refactors/generateGetAccessorAndSetAccessor.ts", "refactors/moveToNewFile.ts", "refactors/addOrRemoveBracesToArrowFunction.ts", + "refactors/mergeImportDeclaration.ts", "services.ts", "breakpoints.ts", "transform.ts", diff --git a/tests/cases/fourslash/refactorToMergeDuplicateImport1.ts b/tests/cases/fourslash/refactorToMergeDuplicateImport1.ts new file mode 100644 index 0000000000000..b10177083144a --- /dev/null +++ b/tests/cases/fourslash/refactorToMergeDuplicateImport1.ts @@ -0,0 +1,143 @@ +/// + +// @Filename: a.ts +//// export const a = 1; +//// export const b = 1; +//// export const c = 1; +//// export const d = 1; +//// export const e = 1; +//// export default { }; + +// @Filename: b.ts +//// /*a*/import A, { a, b, c } from './a';/*b*/ +//// import { c, d, e } from './a'; + +// @Filename: c.ts +//// /*c*/import A, { a, b, c } from './a';/*d*/ +//// import A, { c, d, e } from './a'; + +// @Filename: d.ts +//// /*e*/import { a, b, c } from './a';/*f*/ +//// import A, { c, d, e } from './a'; + +// @Filename: e.ts +//// /*g*/import A, { a, b, c } from './a';/*h*/ +//// import { c, d } from './a'; +//// import { e, d } from './a'; + +// @Filename: f.ts +//// /*i*/import A, { a, b, c } from './a';/*j*/ +//// import A, { c, d } from './a'; +//// import A, { e, d } from './a'; + +// @Filename: g.ts +//// /*k*/import A, { a, b, c } from './a';/*l*/ +//// import { c, d } from './a'; +//// import A, { e, d } from './a'; + +// @Filename: h.ts +//// /*m*/import A, { a, b, c } from './a';/*n*/ +//// import B, { c, d } from './a'; + +// @Filename: i.ts +//// /*o*/import A, { a, b, c } from './a';/*p*/ +//// import A, { c, d } from './a'; +//// import B, { c, d } from './a'; + +// @Filename: j.ts +//// /*q*/import { a, b, c } from './a';/*r*/ +//// import B, { c, d } from './a'; +//// import B, { c, d } from './a'; + +// @Filename: k.ts +//// /*s*/import { a, b, c } from './a';/*t*/ +//// import { c, d, e } from './b'; + +goTo.file("b.ts"); +goTo.select("a", "b"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("c.ts"); +goTo.select("c", "d"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("d.ts"); +goTo.select("e", "f"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("e.ts"); +goTo.select("g", "h"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("f.ts"); +goTo.select("i", "j"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("g.ts"); +goTo.select("k", "l"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import A, { a, b, c, d, e } from './a'; +`, +}); + +goTo.file("h.ts"); +goTo.select("m", "n"); +verify.not.refactorAvailable(); + +goTo.file("i.ts"); +goTo.select("o", "p"); +verify.not.refactorAvailable(); + +goTo.file("j.ts"); +goTo.select("q", "r"); +verify.refactorAvailable("Merge duplicate import declaration"); +edit.applyRefactor({ + refactorName: "Merge duplicate import declaration", + actionName: "Merge duplicate import declaration", + actionDescription: "Merge duplicate import declaration", + newContent: `import B, { a, b, c, d } from './a'; +`, +}); + +goTo.file("k.ts"); +goTo.select("s", "t"); +verify.not.refactorAvailable()