• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.refactor {
3    const refactorName = "Convert import";
4
5    const actions = {
6        [ImportKind.Named]: {
7            name: "Convert namespace import to named imports",
8            description: Diagnostics.Convert_namespace_import_to_named_imports.message,
9            kind: "refactor.rewrite.import.named",
10        },
11        [ImportKind.Namespace]: {
12            name: "Convert named imports to namespace import",
13            description: Diagnostics.Convert_named_imports_to_namespace_import.message,
14            kind: "refactor.rewrite.import.namespace",
15        },
16        [ImportKind.Default]: {
17            name: "Convert named imports to default import",
18            description: Diagnostics.Convert_named_imports_to_default_import.message,
19            kind: "refactor.rewrite.import.default",
20        },
21    };
22
23    registerRefactor(refactorName, {
24        kinds: getOwnValues(actions).map(a => a.kind),
25        getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ApplicableRefactorInfo[] {
26            const info = getImportConversionInfo(context, context.triggerReason === "invoked");
27            if (!info) return emptyArray;
28
29            if (!isRefactorErrorInfo(info)) {
30                const action = actions[info.convertTo];
31                return [{ name: refactorName, description: action.description, actions: [action] }];
32            }
33
34            if (context.preferences.provideRefactorNotApplicableReason) {
35                return getOwnValues(actions).map(action => ({
36                    name: refactorName,
37                    description: action.description,
38                    actions: [{ ...action, notApplicableReason: info.error }]
39                }));
40            }
41
42            return emptyArray;
43        },
44        getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): RefactorEditInfo {
45            Debug.assert(some(getOwnValues(actions), action => action.name === actionName), "Unexpected action name");
46            const info = getImportConversionInfo(context);
47            Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
48            const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
49            return { edits, renameFilename: undefined, renameLocation: undefined };
50        }
51    });
52
53    // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
54    type ImportConversionInfo =
55        | { convertTo: ImportKind.Default, import: NamedImports }
56        | { convertTo: ImportKind.Namespace, import: NamedImports }
57        | { convertTo: ImportKind.Named, import: NamespaceImport };
58
59    function getImportConversionInfo(context: RefactorContext, considerPartialSpans = true): ImportConversionInfo | RefactorErrorInfo | undefined {
60        const { file } = context;
61        const span = getRefactorContextSpan(context);
62        const token = getTokenAtPosition(file, span.start);
63        const importDecl = considerPartialSpans ? findAncestor(token, isImportDeclaration) : getParentNodeInSpan(token, file, span);
64        if (!importDecl || !isImportDeclaration(importDecl)) return { error: "Selection is not an import declaration." };
65
66        const end = span.start + span.length;
67        const nextToken = findNextToken(importDecl, importDecl.parent, file);
68        if (nextToken && end > nextToken.getStart()) return undefined;
69
70        const { importClause } = importDecl;
71        if (!importClause) {
72            return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_import_clause) };
73        }
74
75        if (!importClause.namedBindings) {
76            return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
77        }
78
79        if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) {
80            return { convertTo: ImportKind.Named, import: importClause.namedBindings };
81        }
82        const shouldUseDefault = getShouldUseDefault(context.program, importClause);
83
84        return shouldUseDefault
85            ? { convertTo: ImportKind.Default, import: importClause.namedBindings }
86            : { convertTo: ImportKind.Namespace, import: importClause.namedBindings };
87    }
88
89    function getShouldUseDefault(program: Program, importClause: ImportClause) {
90        return getAllowSyntheticDefaultImports(program.getCompilerOptions())
91            && isExportEqualsModule(importClause.parent.moduleSpecifier, program.getTypeChecker());
92    }
93
94    function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, info: ImportConversionInfo): void {
95        const checker = program.getTypeChecker();
96        if (info.convertTo === ImportKind.Named) {
97            doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
98        }
99        else {
100            doChangeNamedToNamespaceOrDefault(sourceFile, program, changes, info.import, info.convertTo === ImportKind.Default);
101        }
102    }
103
104    function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void {
105        let usedAsNamespaceOrDefault = false;
106
107        const nodesToReplace: (PropertyAccessExpression | QualifiedName)[] = [];
108        const conflictingNames = new Map<string, true>();
109
110        FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => {
111            if (!isPropertyAccessOrQualifiedName(id.parent)) {
112                usedAsNamespaceOrDefault = true;
113            }
114            else {
115                const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text;
116                if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) {
117                    conflictingNames.set(exportName, true);
118                }
119                Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id");
120                nodesToReplace.push(id.parent);
121            }
122        });
123
124        // We may need to change `mod.x` to `_x` to avoid a name conflict.
125        const exportNameToImportName = new Map<string, string>();
126
127        for (const propertyAccessOrQualifiedName of nodesToReplace) {
128            const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text;
129            let importName = exportNameToImportName.get(exportName);
130            if (importName === undefined) {
131                exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName);
132            }
133            changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, factory.createIdentifier(importName));
134        }
135
136        const importSpecifiers: ImportSpecifier[] = [];
137        exportNameToImportName.forEach((name, propertyName) => {
138            importSpecifiers.push(factory.createImportSpecifier(/*isTypeOnly*/ false, name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)));
139        });
140
141        const importDecl = toConvert.parent.parent;
142        if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) {
143            // Need to leave the namespace import alone
144            changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers));
145        }
146        else {
147            changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers));
148        }
149    }
150
151    function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) {
152        return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right;
153    }
154
155    function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) {
156        return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left;
157    }
158
159    export function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImports, shouldUseDefault = getShouldUseDefault(program, toConvert.parent)): void {
160        const checker = program.getTypeChecker();
161        const importDecl = toConvert.parent.parent;
162        const { moduleSpecifier } = importDecl;
163
164        const toConvertSymbols: Set<Symbol> = new Set();
165        toConvert.elements.forEach(namedImport => {
166            const symbol = checker.getSymbolAtLocation(namedImport.name);
167            if (symbol) {
168                toConvertSymbols.add(symbol);
169            }
170        });
171        const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
172        function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean {
173            // We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict.
174            // A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope.
175            // We are going to use the namespace name in the scopes the named imports being refactored are referenced,
176            // so we look for conflicts by looking at every reference to those named imports.
177            return !!FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => {
178                const symbol = checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true);
179                if (symbol) { // There already is a symbol with the same name as the preferred namespace name.
180                    if (toConvertSymbols.has(symbol)) { // `preferredName` resolves to a symbol for one of the named import references we are going to transform into namespace import references...
181                        return isExportSpecifier(id.parent); // ...but if this reference is an export specifier, it will not be transformed, so it is a conflict; otherwise, it will be renamed and is not a conflict.
182                    }
183                    return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict.
184                }
185                return false; // There is no symbol with the same name as the preferred namespace name, so no conflict.
186            });
187        }
188        const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict);
189        const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName;
190
191        // Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics.
192        // More specifically, those are named imports that appear in named exports in the original code, e.g. `a` in `import { a } from "m"; export { a }`.
193        const neededNamedImports: Set<ImportSpecifier> = new Set();
194
195        for (const element of toConvert.elements) {
196            const propertyName = (element.propertyName || element.name).text;
197            FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => {
198                const access = factory.createPropertyAccessExpression(factory.createIdentifier(namespaceImportName), propertyName);
199                if (isShorthandPropertyAssignment(id.parent)) {
200                    changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access));
201                }
202                else if (isExportSpecifier(id.parent)) {
203                    neededNamedImports.add(element);
204                }
205                else {
206                    changes.replaceNode(sourceFile, id, access);
207                }
208            });
209        }
210
211        changes.replaceNode(sourceFile, toConvert, shouldUseDefault
212            ? factory.createIdentifier(namespaceImportName)
213            : factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
214        if (neededNamedImports.size) {
215            const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element =>
216                factory.createImportSpecifier(element.isTypeOnly, element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
217            changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports));
218        }
219    }
220
221    function isExportEqualsModule(moduleSpecifier: Expression, checker: TypeChecker) {
222        const externalModule = checker.resolveExternalModuleName(moduleSpecifier);
223        if (!externalModule) return false;
224        const exportEquals = checker.resolveExternalModuleSymbol(externalModule);
225        return externalModule !== exportEquals;
226    }
227
228    function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration {
229        return factory.createImportDeclaration(/*modifiers*/ undefined,
230            factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined);
231    }
232}
233