• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    registerCodeFix({
4        errorCodes: [Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module.code],
5        getCodeActions(context) {
6            const { sourceFile, program, preferences } = context;
7            const changes = textChanges.ChangeTracker.with(context, changes => {
8                const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreference(sourceFile, preferences));
9                if (moduleExportsChangedToDefault) {
10                    for (const importingFile of program.getSourceFiles()) {
11                        fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences));
12                    }
13                }
14            });
15            // No support for fix-all since this applies to the whole file at once anyway.
16            return [createCodeFixActionWithoutFixAll("convertToEsModule", changes, Diagnostics.Convert_to_ES_module)];
17        },
18    });
19
20    function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, quotePreference: QuotePreference) {
21        for (const moduleSpecifier of importingFile.imports) {
22            const imported = getResolvedModule(importingFile, moduleSpecifier.text, getModeForUsageLocation(importingFile, moduleSpecifier));
23            if (!imported || imported.resolvedFileName !== exportingFile.fileName) {
24                continue;
25            }
26
27            const importNode = importFromModuleSpecifier(moduleSpecifier);
28            switch (importNode.kind) {
29                case SyntaxKind.ImportEqualsDeclaration:
30                    changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, quotePreference));
31                    break;
32                case SyntaxKind.CallExpression:
33                    if (isRequireCall(importNode, /*checkArgumentIsStringLiteralLike*/ false)) {
34                        changes.replaceNode(importingFile, importNode, factory.createPropertyAccessExpression(getSynthesizedDeepClone(importNode), "default"));
35                    }
36                    break;
37            }
38        }
39    }
40
41    /** @returns Whether we converted a `module.exports =` to a default export. */
42    function convertFileToEsModule(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, quotePreference: QuotePreference): ModuleExportsChanged {
43        const identifiers: Identifiers = { original: collectFreeIdentifiers(sourceFile), additional: new Set() };
44        const exports = collectExportRenames(sourceFile, checker, identifiers);
45        convertExportsAccesses(sourceFile, exports, changes);
46        let moduleExportsChangedToDefault = false;
47        let useSitesToUnqualify: ESMap<Node, Node> | undefined;
48        // Process variable statements first to collect use sites that need to be updated inside other transformations
49        for (const statement of filter(sourceFile.statements, isVariableStatement)) {
50            const newUseSites = convertVariableStatement(sourceFile, statement, changes, checker, identifiers, target, quotePreference);
51            if (newUseSites) {
52                copyEntries(newUseSites, useSitesToUnqualify ??= new Map());
53            }
54        }
55        // `convertStatement` will delete entries from `useSitesToUnqualify` when containing statements are replaced
56        for (const statement of filter(sourceFile.statements, s => !isVariableStatement(s))) {
57            const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, useSitesToUnqualify, quotePreference);
58            moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged;
59        }
60        // Remaining use sites can be changed directly
61        useSitesToUnqualify?.forEach((replacement, original) => {
62            changes.replaceNode(sourceFile, original, replacement);
63        });
64
65        return moduleExportsChangedToDefault;
66    }
67
68    /**
69     * Contains an entry for each renamed export.
70     * This is necessary because `exports.x = 0;` does not declare a local variable.
71     * Converting this to `export const x = 0;` would declare a local, so we must be careful to avoid shadowing.
72     * If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must convert to:
73     *     const _x = 0;
74     *     export { _x as x };
75     * This conversion also must place if the exported name is not a valid identifier, e.g. `exports.class = 0;`.
76     */
77    type ExportRenames = ReadonlyESMap<string, string>;
78
79    function collectExportRenames(sourceFile: SourceFile, checker: TypeChecker, identifiers: Identifiers): ExportRenames {
80        const res = new Map<string, string>();
81        forEachExportReference(sourceFile, node => {
82            const { text, originalKeywordKind } = node.name;
83            if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind)
84                || checker.resolveName(text, node, SymbolFlags.Value, /*excludeGlobals*/ true))) {
85                // Unconditionally add an underscore in case `text` is a keyword.
86                res.set(text, makeUniqueName(`_${text}`, identifiers));
87            }
88        });
89        return res;
90    }
91
92    function convertExportsAccesses(sourceFile: SourceFile, exports: ExportRenames, changes: textChanges.ChangeTracker): void {
93        forEachExportReference(sourceFile, (node, isAssignmentLhs) => {
94            if (isAssignmentLhs) {
95                return;
96            }
97            const { text } = node.name;
98            changes.replaceNode(sourceFile, node, factory.createIdentifier(exports.get(text) || text));
99        });
100    }
101
102    function forEachExportReference(sourceFile: SourceFile, cb: (node: (PropertyAccessExpression & { name: Identifier }), isAssignmentLhs: boolean) => void): void {
103        sourceFile.forEachChild(function recur(node) {
104            if (isPropertyAccessExpression(node) && isExportsOrModuleExportsOrAlias(sourceFile, node.expression) && isIdentifier(node.name)) {
105                const { parent } = node;
106                cb(node as typeof node & { name: Identifier }, isBinaryExpression(parent) && parent.left === node && parent.operatorToken.kind === SyntaxKind.EqualsToken);
107            }
108            node.forEachChild(recur);
109        });
110    }
111
112    /** Whether `module.exports =` was changed to `export default` */
113    type ModuleExportsChanged = boolean;
114
115    function convertStatement(
116        sourceFile: SourceFile,
117        statement: Statement,
118        checker: TypeChecker,
119        changes: textChanges.ChangeTracker,
120        identifiers: Identifiers,
121        target: ScriptTarget,
122        exports: ExportRenames,
123        useSitesToUnqualify: ESMap<Node, Node> | undefined,
124        quotePreference: QuotePreference
125    ): ModuleExportsChanged {
126        switch (statement.kind) {
127            case SyntaxKind.VariableStatement:
128                convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, quotePreference);
129                return false;
130            case SyntaxKind.ExpressionStatement: {
131                const { expression } = statement as ExpressionStatement;
132                switch (expression.kind) {
133                    case SyntaxKind.CallExpression: {
134                        if (isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true)) {
135                            // For side-effecting require() call, just make a side-effecting import.
136                            changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], quotePreference));
137                        }
138                        return false;
139                    }
140                    case SyntaxKind.BinaryExpression: {
141                        const { operatorToken } = expression as BinaryExpression;
142                        return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, expression as BinaryExpression, changes, exports, useSitesToUnqualify);
143                    }
144                }
145            }
146            // falls through
147            default:
148                return false;
149        }
150    }
151
152    function convertVariableStatement(
153        sourceFile: SourceFile,
154        statement: VariableStatement,
155        changes: textChanges.ChangeTracker,
156        checker: TypeChecker,
157        identifiers: Identifiers,
158        target: ScriptTarget,
159        quotePreference: QuotePreference,
160    ): ESMap<Node, Node> | undefined {
161        const { declarationList } = statement;
162        let foundImport = false;
163        const converted = map(declarationList.declarations, decl => {
164            const { name, initializer } = decl;
165            if (initializer) {
166                if (isExportsOrModuleExportsOrAlias(sourceFile, initializer)) {
167                    // `const alias = module.exports;` can be removed.
168                    foundImport = true;
169                    return convertedImports([]);
170                }
171                else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) {
172                    foundImport = true;
173                    return convertSingleImport(name, initializer.arguments[0], checker, identifiers, target, quotePreference);
174                }
175                else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) {
176                    foundImport = true;
177                    return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, quotePreference);
178                }
179            }
180            // Move it out to its own variable statement. (This will not be used if `!foundImport`)
181            return convertedImports([factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([decl], declarationList.flags))]);
182        });
183        if (foundImport) {
184            // useNonAdjustedEndPosition to ensure we don't eat the newline after the statement.
185            changes.replaceNodeWithNodes(sourceFile, statement, flatMap(converted, c => c.newImports));
186            let combinedUseSites: ESMap<Node, Node> | undefined;
187            forEach(converted, c => {
188                if (c.useSitesToUnqualify) {
189                    copyEntries(c.useSitesToUnqualify, combinedUseSites ??= new Map());
190                }
191            });
192
193            return combinedUseSites;
194        }
195    }
196
197    /** Converts `const name = require("moduleSpecifier").propertyName` */
198    function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports {
199        switch (name.kind) {
200            case SyntaxKind.ObjectBindingPattern:
201            case SyntaxKind.ArrayBindingPattern: {
202                // `const [a, b] = require("c").d` --> `import { d } from "c"; const [a, b] = d;`
203                const tmp  = makeUniqueName(propertyName, identifiers);
204                return convertedImports([
205                    makeSingleImport(tmp, propertyName, moduleSpecifier, quotePreference),
206                    makeConst(/*modifiers*/ undefined, name, factory.createIdentifier(tmp)),
207                ]);
208            }
209            case SyntaxKind.Identifier:
210                // `const a = require("b").c` --> `import { c as a } from "./b";
211                return convertedImports([makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)]);
212            default:
213                return Debug.assertNever(name, `Convert to ES module got invalid syntax form ${(name as BindingName).kind}`);
214        }
215    }
216
217    function convertAssignment(
218        sourceFile: SourceFile,
219        checker: TypeChecker,
220        assignment: BinaryExpression,
221        changes: textChanges.ChangeTracker,
222        exports: ExportRenames,
223        useSitesToUnqualify: ESMap<Node, Node> | undefined,
224    ): ModuleExportsChanged {
225        const { left, right } = assignment;
226        if (!isPropertyAccessExpression(left)) {
227            return false;
228        }
229
230        if (isExportsOrModuleExportsOrAlias(sourceFile, left)) {
231            if (isExportsOrModuleExportsOrAlias(sourceFile, right)) {
232                // `const alias = module.exports;` or `module.exports = alias;` can be removed.
233                changes.delete(sourceFile, assignment.parent);
234            }
235            else {
236                const replacement = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right, useSitesToUnqualify)
237                    : isRequireCall(right, /*checkArgumentIsStringLiteralLike*/ true) ? convertReExportAll(right.arguments[0], checker)
238                    : undefined;
239                if (replacement) {
240                    changes.replaceNodeWithNodes(sourceFile, assignment.parent, replacement[0]);
241                    return replacement[1];
242                }
243                else {
244                    changes.replaceRangeWithText(sourceFile, createRange(left.getStart(sourceFile), right.pos), "export default");
245                    return true;
246                }
247            }
248        }
249        else if (isExportsOrModuleExportsOrAlias(sourceFile, left.expression)) {
250            convertNamedExport(sourceFile, assignment as BinaryExpression & { left: PropertyAccessExpression }, changes, exports);
251        }
252
253        return false;
254    }
255
256    /**
257     * Convert `module.exports = { ... }` to individual exports..
258     * We can't always do this if the module has interesting members -- then it will be a default export instead.
259     */
260    function tryChangeModuleExportsObject(object: ObjectLiteralExpression, useSitesToUnqualify: ESMap<Node, Node> | undefined): [readonly Statement[], ModuleExportsChanged] | undefined {
261        const statements = mapAllOrFail(object.properties, prop => {
262            switch (prop.kind) {
263                case SyntaxKind.GetAccessor:
264                case SyntaxKind.SetAccessor:
265                // TODO: Maybe we should handle this? See fourslash test `refactorConvertToEs6Module_export_object_shorthand.ts`.
266                // falls through
267                case SyntaxKind.ShorthandPropertyAssignment:
268                case SyntaxKind.SpreadAssignment:
269                    return undefined;
270                case SyntaxKind.PropertyAssignment:
271                    return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals_replaceNode(prop.name.text, prop.initializer, useSitesToUnqualify);
272                case SyntaxKind.MethodDeclaration:
273                    return !isIdentifier(prop.name) ? undefined : functionExpressionToDeclaration(prop.name.text, [factory.createToken(SyntaxKind.ExportKeyword)], prop, useSitesToUnqualify);
274                default:
275                    Debug.assertNever(prop, `Convert to ES6 got invalid prop kind ${(prop as ObjectLiteralElementLike).kind}`);
276            }
277        });
278        return statements && [statements, false];
279    }
280
281    function convertNamedExport(
282        sourceFile: SourceFile,
283        assignment: BinaryExpression & { left: PropertyAccessExpression },
284        changes: textChanges.ChangeTracker,
285        exports: ExportRenames,
286    ): void {
287        // If "originalKeywordKind" was set, this is e.g. `exports.
288        const { text } = assignment.left.name;
289        const rename = exports.get(text);
290        if (rename !== undefined) {
291            /*
292            const _class = 0;
293            export { _class as class };
294            */
295            const newNodes = [
296                makeConst(/*modifiers*/ undefined, rename, assignment.right),
297                makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, rename, text)]),
298            ];
299            changes.replaceNodeWithNodes(sourceFile, assignment.parent, newNodes);
300        }
301        else {
302            convertExportsPropertyAssignment(assignment, sourceFile, changes);
303        }
304    }
305
306    function convertReExportAll(reExported: StringLiteralLike, checker: TypeChecker): [readonly Statement[], ModuleExportsChanged] {
307        // `module.exports = require("x");` ==> `export * from "x"; export { default } from "x";`
308        const moduleSpecifier = reExported.text;
309        const moduleSymbol = checker.getSymbolAtLocation(reExported);
310        const exports = moduleSymbol ? moduleSymbol.exports! : emptyMap as ReadonlyCollection<__String>;
311        return exports.has(InternalSymbolName.ExportEquals) ? [[reExportDefault(moduleSpecifier)], true] :
312            !exports.has(InternalSymbolName.Default) ? [[reExportStar(moduleSpecifier)], false] :
313            // If there's some non-default export, must include both `export *` and `export default`.
314            exports.size > 1 ? [[reExportStar(moduleSpecifier), reExportDefault(moduleSpecifier)], true] : [[reExportDefault(moduleSpecifier)], true];
315    }
316    function reExportStar(moduleSpecifier: string): ExportDeclaration {
317        return makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier);
318    }
319    function reExportDefault(moduleSpecifier: string): ExportDeclaration {
320        return makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, "default")], moduleSpecifier);
321    }
322
323    function convertExportsPropertyAssignment({ left, right, parent }: BinaryExpression & { left: PropertyAccessExpression }, sourceFile: SourceFile, changes: textChanges.ChangeTracker): void {
324        const name = left.name.text;
325        if ((isFunctionExpression(right) || isArrowFunction(right) || isClassExpression(right)) && (!right.name || right.name.text === name)) {
326            // `exports.f = function() {}` -> `export function f() {}` -- Replace `exports.f = ` with `export `, and insert the name after `function`.
327            changes.replaceRange(sourceFile, { pos: left.getStart(sourceFile), end: right.getStart(sourceFile) }, factory.createToken(SyntaxKind.ExportKeyword), { suffix: " " });
328
329            if (!right.name) changes.insertName(sourceFile, right, name);
330
331            const semi = findChildOfKind(parent, SyntaxKind.SemicolonToken, sourceFile);
332            if (semi) changes.delete(sourceFile, semi);
333        }
334        else {
335            // `exports.f = function g() {}` -> `export const f = function g() {}` -- just replace `exports.` with `export const `
336            changes.replaceNodeRangeWithNodes(sourceFile, left.expression, findChildOfKind(left, SyntaxKind.DotToken, sourceFile)!,
337                [factory.createToken(SyntaxKind.ExportKeyword), factory.createToken(SyntaxKind.ConstKeyword)],
338                { joiner: " ", suffix: " " });
339        }
340    }
341
342    // TODO: GH#22492 this will cause an error if a change has been made inside the body of the node.
343    function convertExportsDotXEquals_replaceNode(name: string | undefined, exported: Expression, useSitesToUnqualify: ESMap<Node, Node> | undefined): Statement {
344        const modifiers = [factory.createToken(SyntaxKind.ExportKeyword)];
345        switch (exported.kind) {
346            case SyntaxKind.FunctionExpression: {
347                const { name: expressionName } = exported as FunctionExpression;
348                if (expressionName && expressionName.text !== name) {
349                    // `exports.f = function g() {}` -> `export const f = function g() {}`
350                    return exportConst();
351                }
352            }
353
354            // falls through
355            case SyntaxKind.ArrowFunction:
356                // `exports.f = function() {}` --> `export function f() {}`
357                return functionExpressionToDeclaration(name, modifiers, exported as FunctionExpression | ArrowFunction, useSitesToUnqualify);
358            case SyntaxKind.ClassExpression:
359                // `exports.C = class {}` --> `export class C {}`
360                return classExpressionToDeclaration(name, modifiers, exported as ClassExpression, useSitesToUnqualify);
361            default:
362                return exportConst();
363        }
364
365        function exportConst() {
366            // `exports.x = 0;` --> `export const x = 0;`
367            return makeConst(modifiers, factory.createIdentifier(name!), replaceImportUseSites(exported, useSitesToUnqualify)); // TODO: GH#18217
368        }
369    }
370
371    function replaceImportUseSites<T extends Node>(node: T, useSitesToUnqualify: ESMap<Node, Node> | undefined): T;
372    function replaceImportUseSites<T extends Node>(nodes: NodeArray<T>, useSitesToUnqualify: ESMap<Node, Node> | undefined): NodeArray<T>;
373    function replaceImportUseSites<T extends Node>(nodeOrNodes: T | NodeArray<T>, useSitesToUnqualify: ESMap<Node, Node> | undefined) {
374        if (!useSitesToUnqualify || !some(arrayFrom(useSitesToUnqualify.keys()), original => rangeContainsRange(nodeOrNodes, original))) {
375            return nodeOrNodes;
376        }
377
378        return isArray(nodeOrNodes)
379            ? getSynthesizedDeepClonesWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode)
380            : getSynthesizedDeepCloneWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode);
381
382        function replaceNode(original: Node) {
383            // We are replacing `mod.SomeExport` wih `SomeExport`, so we only need to look at PropertyAccessExpressions
384            if (original.kind === SyntaxKind.PropertyAccessExpression) {
385                const replacement = useSitesToUnqualify!.get(original);
386                // Remove entry from `useSitesToUnqualify` so the refactor knows it's taken care of by the parent statement we're replacing
387                useSitesToUnqualify!.delete(original);
388                return replacement;
389            }
390        }
391    }
392
393    /**
394     * Converts `const <<name>> = require("x");`.
395     * Returns nodes that will replace the variable declaration for the commonjs import.
396     * May also make use `changes` to remove qualifiers at the use sites of imports, to change `mod.x` to `x`.
397     */
398    function convertSingleImport(
399        name: BindingName,
400        moduleSpecifier: StringLiteralLike,
401        checker: TypeChecker,
402        identifiers: Identifiers,
403        target: ScriptTarget,
404        quotePreference: QuotePreference,
405    ): ConvertedImports {
406        switch (name.kind) {
407            case SyntaxKind.ObjectBindingPattern: {
408                const importSpecifiers = mapAllOrFail(name.elements, e =>
409                    e.dotDotDotToken || e.initializer || e.propertyName && !isIdentifier(e.propertyName) || !isIdentifier(e.name)
410                        ? undefined
411                        : makeImportSpecifier(e.propertyName && e.propertyName.text, e.name.text));
412                if (importSpecifiers) {
413                    return convertedImports([makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)]);
414                }
415            }
416            // falls through -- object destructuring has an interesting pattern and must be a variable declaration
417            case SyntaxKind.ArrayBindingPattern: {
418                /*
419                import x from "x";
420                const [a, b, c] = x;
421                */
422                const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers);
423                return convertedImports([
424                    makeImport(factory.createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, quotePreference),
425                    makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), factory.createIdentifier(tmp)),
426                ]);
427            }
428            case SyntaxKind.Identifier:
429                return convertSingleIdentifierImport(name, moduleSpecifier, checker, identifiers, quotePreference);
430            default:
431                return Debug.assertNever(name, `Convert to ES module got invalid name kind ${(name as BindingName).kind}`);
432        }
433    }
434
435    /**
436     * Convert `import x = require("x").`
437     * Also:
438     * - Convert `x.default()` to `x()` to handle ES6 default export
439     * - Converts uses like `x.y()` to `y()` and uses a named import.
440     */
441    function convertSingleIdentifierImport(name: Identifier, moduleSpecifier: StringLiteralLike, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports {
442        const nameSymbol = checker.getSymbolAtLocation(name);
443        // Maps from module property name to name actually used. (The same if there isn't shadowing.)
444        const namedBindingsNames = new Map<string, string>();
445        // True if there is some non-property use like `x()` or `f(x)`.
446        let needDefaultImport = false;
447        let useSitesToUnqualify: ESMap<Node, Node> | undefined;
448
449        for (const use of identifiers.original.get(name.text)!) {
450            if (checker.getSymbolAtLocation(use) !== nameSymbol || use === name) {
451                // This was a use of a different symbol with the same name, due to shadowing. Ignore.
452                continue;
453            }
454
455            const { parent } = use;
456            if (isPropertyAccessExpression(parent)) {
457                const { name: { text: propertyName } } = parent;
458                if (propertyName === "default") {
459                    needDefaultImport = true;
460
461                    const importDefaultName = use.getText();
462                    (useSitesToUnqualify ??= new Map()).set(parent, factory.createIdentifier(importDefaultName));
463                }
464                else {
465                    Debug.assert(parent.expression === use, "Didn't expect expression === use"); // Else shouldn't have been in `collectIdentifiers`
466                    let idName = namedBindingsNames.get(propertyName);
467                    if (idName === undefined) {
468                        idName = makeUniqueName(propertyName, identifiers);
469                        namedBindingsNames.set(propertyName, idName);
470                    }
471
472                    (useSitesToUnqualify ??= new Map()).set(parent, factory.createIdentifier(idName));
473                }
474            }
475            else {
476                needDefaultImport = true;
477            }
478        }
479
480        const namedBindings = namedBindingsNames.size === 0 ? undefined : arrayFrom(mapIterator(namedBindingsNames.entries(), ([propertyName, idName]) =>
481            factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === idName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(idName))));
482        if (!namedBindings) {
483            // If it was unused, ensure that we at least import *something*.
484            needDefaultImport = true;
485        }
486        return convertedImports(
487            [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)],
488            useSitesToUnqualify
489        );
490    }
491
492    // Identifiers helpers
493
494    function makeUniqueName(name: string, identifiers: Identifiers): string {
495        while (identifiers.original.has(name) || identifiers.additional.has(name)) {
496            name = `_${name}`;
497        }
498        identifiers.additional.add(name);
499        return name;
500    }
501
502    /**
503     * Helps us create unique identifiers.
504     * `original` refers to the local variable names in the original source file.
505     * `additional` is any new unique identifiers we've generated. (e.g., we'll generate `_x`, then `__x`.)
506     */
507    interface Identifiers {
508        readonly original: FreeIdentifiers;
509        // Additional identifiers we've added. Mutable!
510        readonly additional: Set<string>;
511    }
512
513    type FreeIdentifiers = ReadonlyESMap<string, readonly Identifier[]>;
514    function collectFreeIdentifiers(file: SourceFile): FreeIdentifiers {
515        const map = createMultiMap<Identifier>();
516        forEachFreeIdentifier(file, id => map.add(id.text, id));
517        return map;
518    }
519
520    /**
521     * A free identifier is an identifier that can be accessed through name lookup as a local variable.
522     * In the expression `x.y`, `x` is a free identifier, but `y` is not.
523     */
524    function forEachFreeIdentifier(node: Node, cb: (id: Identifier) => void): void {
525        if (isIdentifier(node) && isFreeIdentifier(node)) cb(node);
526        node.forEachChild(child => forEachFreeIdentifier(child, cb));
527    }
528
529    function isFreeIdentifier(node: Identifier): boolean {
530        const { parent } = node;
531        switch (parent.kind) {
532            case SyntaxKind.PropertyAccessExpression:
533                return (parent as PropertyAccessExpression).name !== node;
534            case SyntaxKind.BindingElement:
535                return (parent as BindingElement).propertyName !== node;
536            case SyntaxKind.ImportSpecifier:
537                return (parent as ImportSpecifier).propertyName !== node;
538            default:
539                return true;
540        }
541    }
542
543    // Node helpers
544
545    function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], fn: FunctionExpression | ArrowFunction | MethodDeclaration, useSitesToUnqualify: ESMap<Node, Node> | undefined): FunctionDeclaration {
546        return factory.createFunctionDeclaration(
547            concatenate(additionalModifiers, getSynthesizedDeepClones(fn.modifiers)),
548            getSynthesizedDeepClone(fn.asteriskToken),
549            name,
550            getSynthesizedDeepClones(fn.typeParameters),
551            getSynthesizedDeepClones(fn.parameters),
552            getSynthesizedDeepClone(fn.type),
553            factory.converters.convertToFunctionBlock(replaceImportUseSites(fn.body!, useSitesToUnqualify)));
554    }
555
556    function classExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], cls: ClassExpression, useSitesToUnqualify: ESMap<Node, Node> | undefined): ClassDeclaration {
557        return factory.createClassDeclaration(
558            concatenate(additionalModifiers, getSynthesizedDeepClones(cls.modifiers)),
559            name,
560            getSynthesizedDeepClones(cls.typeParameters),
561            getSynthesizedDeepClones(cls.heritageClauses),
562            replaceImportUseSites(cls.members, useSitesToUnqualify));
563    }
564
565    function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, quotePreference: QuotePreference): ImportDeclaration {
566        return propertyName === "default"
567            ? makeImport(factory.createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, quotePreference)
568            : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, quotePreference);
569    }
570
571    function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {
572        return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName !== undefined && propertyName !== name ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(name));
573    }
574
575    function makeConst(modifiers: readonly Modifier[] | undefined, name: string | BindingName, init: Expression): VariableStatement {
576        return factory.createVariableStatement(
577            modifiers,
578            factory.createVariableDeclarationList(
579                [factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, /*type*/ undefined, init)],
580                NodeFlags.Const));
581    }
582
583    function makeExportDeclaration(exportSpecifiers: ExportSpecifier[] | undefined, moduleSpecifier?: string): ExportDeclaration {
584        return factory.createExportDeclaration(
585            /*modifiers*/ undefined,
586            /*isTypeOnly*/ false,
587            exportSpecifiers && factory.createNamedExports(exportSpecifiers),
588            moduleSpecifier === undefined ? undefined : factory.createStringLiteral(moduleSpecifier));
589    }
590
591    interface ConvertedImports {
592        newImports: readonly Node[];
593        useSitesToUnqualify?: ESMap<Node, Node>;
594    }
595
596    function convertedImports(newImports: readonly Node[], useSitesToUnqualify?: ESMap<Node, Node>): ConvertedImports {
597        return {
598            newImports,
599            useSitesToUnqualify
600        };
601    }
602}
603