• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.refactor {
3    const refactorName = "Convert import";
4
5    const namespaceToNamedAction = {
6        name: "Convert namespace import to named imports",
7        description: Diagnostics.Convert_namespace_import_to_named_imports.message,
8        kind: "refactor.rewrite.import.named",
9    };
10    const namedToNamespaceAction = {
11        name: "Convert named imports to namespace import",
12        description: Diagnostics.Convert_named_imports_to_namespace_import.message,
13        kind: "refactor.rewrite.import.namespace",
14    };
15
16    registerRefactor(refactorName, {
17        kinds: [
18            namespaceToNamedAction.kind,
19            namedToNamespaceAction.kind
20        ],
21        getAvailableActions(context): readonly ApplicableRefactorInfo[] {
22            const info = getImportToConvert(context, context.triggerReason === "invoked");
23            if (!info) return emptyArray;
24
25            if (!isRefactorErrorInfo(info)) {
26                const namespaceImport = info.kind === SyntaxKind.NamespaceImport;
27                const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction;
28                return [{ name: refactorName, description: action.description, actions: [action] }];
29            }
30
31            if (context.preferences.provideRefactorNotApplicableReason) {
32                return [
33                    { name: refactorName, description: namespaceToNamedAction.description,
34                        actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] },
35                    { name: refactorName, description: namedToNamespaceAction.description,
36                        actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] }
37                ];
38            }
39
40            return emptyArray;
41        },
42        getEditsForAction(context, actionName): RefactorEditInfo {
43            Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name");
44            const info = getImportToConvert(context);
45            Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
46            const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
47            return { edits, renameFilename: undefined, renameLocation: undefined };
48        }
49    });
50
51    // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
52    function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined {
53        const { file } = context;
54        const span = getRefactorContextSpan(context);
55        const token = getTokenAtPosition(file, span.start);
56        const importDecl = considerPartialSpans ? findAncestor(token, isImportDeclaration) : getParentNodeInSpan(token, file, span);
57        if (!importDecl || !isImportDeclaration(importDecl)) return { error: "Selection is not an import declaration." };
58        if (importDecl.getEnd() < span.start + span.length) return undefined;
59
60        const { importClause } = importDecl;
61        if (!importClause) {
62            return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_import_clause) };
63        }
64
65        if (!importClause.namedBindings) {
66            return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
67        }
68
69        return importClause.namedBindings;
70    }
71
72    function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
73        const checker = program.getTypeChecker();
74        if (toConvert.kind === SyntaxKind.NamespaceImport) {
75            doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
76        }
77        else {
78            doChangeNamedToNamespace(sourceFile, checker, changes, toConvert);
79        }
80    }
81
82    function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void {
83        let usedAsNamespaceOrDefault = false;
84
85        const nodesToReplace: (PropertyAccessExpression | QualifiedName)[] = [];
86        const conflictingNames = new Map<string, true>();
87
88        FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => {
89            if (!isPropertyAccessOrQualifiedName(id.parent)) {
90                usedAsNamespaceOrDefault = true;
91            }
92            else {
93                const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text;
94                if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) {
95                    conflictingNames.set(exportName, true);
96                }
97                Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id");
98                nodesToReplace.push(id.parent);
99            }
100        });
101
102        // We may need to change `mod.x` to `_x` to avoid a name conflict.
103        const exportNameToImportName = new Map<string, string>();
104
105        for (const propertyAccessOrQualifiedName of nodesToReplace) {
106            const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text;
107            let importName = exportNameToImportName.get(exportName);
108            if (importName === undefined) {
109                exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName);
110            }
111            changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, factory.createIdentifier(importName));
112        }
113
114        const importSpecifiers: ImportSpecifier[] = [];
115        exportNameToImportName.forEach((name, propertyName) => {
116            importSpecifiers.push(factory.createImportSpecifier(name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)));
117        });
118
119        const importDecl = toConvert.parent.parent;
120        if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) {
121            // Need to leave the namespace import alone
122            changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers));
123        }
124        else {
125            changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers));
126        }
127    }
128
129    function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) {
130        return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right;
131    }
132
133    function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) {
134        return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left;
135    }
136
137    function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void {
138        const importDecl = toConvert.parent.parent;
139        const { moduleSpecifier } = importDecl;
140
141        const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
142        const namespaceNameConflicts = toConvert.elements.some(element =>
143            FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id =>
144                !!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false);
145        const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName;
146
147        const neededNamedImports: ImportSpecifier[] = [];
148
149        for (const element of toConvert.elements) {
150            const propertyName = (element.propertyName || element.name).text;
151            FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => {
152                const access = factory.createPropertyAccessExpression(factory.createIdentifier(namespaceImportName), propertyName);
153                if (isShorthandPropertyAssignment(id.parent)) {
154                    changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access));
155                }
156                else if (isExportSpecifier(id.parent) && !id.parent.propertyName) {
157                    if (!neededNamedImports.some(n => n.name === element.name)) {
158                        neededNamedImports.push(factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
159                    }
160                }
161                else {
162                    changes.replaceNode(sourceFile, id, access);
163                }
164            });
165        }
166
167        changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
168        if (neededNamedImports.length) {
169            changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports));
170        }
171    }
172
173    function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration {
174        return factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined,
175            factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier);
176    }
177}
178