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