• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.refactor {
3    const refactorName = "Move to a new file";
4    const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
5
6    const moveToNewFileAction = {
7        name: refactorName,
8        description,
9        kind: "refactor.move.newFile",
10    };
11    registerRefactor(refactorName, {
12        kinds: [moveToNewFileAction.kind],
13        getAvailableActions: function getRefactorActionsToMoveToNewFile(context): readonly ApplicableRefactorInfo[] {
14            const statements = getStatementsToMove(context);
15            if (context.preferences.allowTextChangesInNewFiles && statements) {
16                return [{ name: refactorName, description, actions: [moveToNewFileAction] }];
17            }
18            if (context.preferences.provideRefactorNotApplicableReason) {
19                return [{ name: refactorName, description, actions:
20                    [{ ...moveToNewFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }]
21                }];
22            }
23            return emptyArray;
24        },
25        getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): RefactorEditInfo {
26            Debug.assert(actionName === refactorName, "Wrong refactor invoked");
27            const statements = Debug.checkDefined(getStatementsToMove(context));
28            const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences));
29            return { edits, renameFilename: undefined, renameLocation: undefined };
30        }
31    });
32
33    interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; }
34    function getRangeToMove(context: RefactorContext): RangeToMove | undefined {
35        const { file } = context;
36        const range = createTextRangeFromSpan(getRefactorContextSpan(context));
37        const { statements } = file;
38
39        const startNodeIndex = findIndex(statements, s => s.end > range.pos);
40        if (startNodeIndex === -1) return undefined;
41
42        const startStatement = statements[startNodeIndex];
43        if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) {
44            return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] };
45        }
46
47        // Can't only partially include the start node or be partially into the next node
48        if (range.pos > startStatement.getStart(file)) return undefined;
49        const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex);
50        // Can't be partially into the next node
51        if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined;
52
53        return {
54            toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex),
55            afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex],
56        };
57    }
58
59    function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void {
60        const checker = program.getTypeChecker();
61        const usage = getUsageInfo(oldFile, toMove.all, checker);
62
63        const currentDirectory = getDirectoryPath(oldFile.fileName);
64        const extension = extensionFromPath(oldFile.fileName);
65        const newModuleName = makeUniqueModuleName(getNewModuleName(usage.oldFileImportsFromNewFile, usage.movedSymbols), extension, currentDirectory, host);
66        const newFileNameWithExtension = newModuleName + extension;
67
68        // If previous file was global, this is easy.
69        changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences));
70
71        addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host));
72    }
73
74    interface StatementRange {
75        readonly first: Statement;
76        readonly afterLast: Statement | undefined;
77    }
78    interface ToMove {
79        readonly all: readonly Statement[];
80        readonly ranges: readonly StatementRange[];
81    }
82
83    function getStatementsToMove(context: RefactorContext): ToMove | undefined {
84        const rangeToMove = getRangeToMove(context);
85        if (rangeToMove === undefined) return undefined;
86        const all: Statement[] = [];
87        const ranges: StatementRange[] = [];
88        const { toMove, afterLast } = rangeToMove;
89        getRangesWhere(toMove, isAllowedStatementToMove, (start, afterEndIndex) => {
90            for (let i = start; i < afterEndIndex; i++) all.push(toMove[i]);
91            ranges.push({ first: toMove[start], afterLast });
92        });
93        return all.length === 0 ? undefined : { all, ranges };
94    }
95
96    function isAllowedStatementToMove(statement: Statement): boolean {
97        // Filters imports and prologue directives out of the range of statements to move.
98        // Imports will be copied to the new file anyway, and may still be needed in the old file.
99        // Prologue directives will be copied to the new file and should be left in the old file.
100        return !isPureImport(statement) && !isPrologueDirective(statement);
101    }
102
103    function isPureImport(node: Node): boolean {
104        switch (node.kind) {
105            case SyntaxKind.ImportDeclaration:
106                return true;
107            case SyntaxKind.ImportEqualsDeclaration:
108                return !hasSyntacticModifier(node, ModifierFlags.Export);
109            case SyntaxKind.VariableStatement:
110                return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true));
111            default:
112                return false;
113        }
114    }
115
116    function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void {
117        const cfg = program.getCompilerOptions().configFile;
118        if (!cfg) return;
119
120        const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension));
121        const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName);
122
123        const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression);
124        const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment =>
125            isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files");
126        if (filesProp && isArrayLiteralExpression(filesProp.initializer)) {
127            changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), factory.createStringLiteral(newFilePath), filesProp.initializer.elements);
128        }
129    }
130
131    function getNewStatementsAndRemoveFromOldFile(
132        oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences,
133    ) {
134        const checker = program.getTypeChecker();
135        const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective);
136        if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size() === 0) {
137            deleteMovedStatements(oldFile, toMove.ranges, changes);
138            return [...prologueDirectives, ...toMove.all];
139        }
140
141        const useEsModuleSyntax = !!oldFile.externalModuleIndicator;
142        const quotePreference = getQuotePreference(oldFile, preferences);
143        const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEsModuleSyntax, quotePreference);
144        if (importsFromNewFile) {
145            insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true);
146        }
147
148        deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker);
149        deleteMovedStatements(oldFile, toMove.ranges, changes);
150        updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName);
151
152        const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEsModuleSyntax, quotePreference);
153        const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax);
154        if (imports.length && body.length) {
155            return [
156                ...prologueDirectives,
157                ...imports,
158                SyntaxKind.NewLineTrivia as const,
159                ...body
160            ];
161        }
162
163        return [
164            ...prologueDirectives,
165            ...imports,
166            ...body,
167        ];
168    }
169
170    function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) {
171        for (const { first, afterLast } of moved) {
172            changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast);
173        }
174    }
175
176    function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) {
177        for (const statement of oldFile.statements) {
178            if (contains(toMove, statement)) continue;
179            forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!)));
180        }
181    }
182
183    function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void {
184        const checker = program.getTypeChecker();
185        for (const sourceFile of program.getSourceFiles()) {
186            if (sourceFile === oldFile) continue;
187            for (const statement of sourceFile.statements) {
188                forEachImportInStatement(statement, importNode => {
189                    if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return;
190
191                    const shouldMove = (name: Identifier): boolean => {
192                        const symbol = isBindingElement(name.parent)
193                            ? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName)
194                            : skipAlias(checker.getSymbolAtLocation(name)!, checker); // TODO: GH#18217
195                        return !!symbol && movedSymbols.has(symbol);
196                    };
197                    deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file
198                    const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName);
199                    const newImportDeclaration = filterImport(importNode, factory.createStringLiteral(newModuleSpecifier), shouldMove);
200                    if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration);
201
202                    const ns = getNamespaceLikeImport(importNode);
203                    if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleName, newModuleSpecifier, ns, importNode);
204                });
205            }
206        }
207    }
208
209    function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined {
210        switch (node.kind) {
211            case SyntaxKind.ImportDeclaration:
212                return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ?
213                    node.importClause.namedBindings.name : undefined;
214            case SyntaxKind.ImportEqualsDeclaration:
215                return node.name;
216            case SyntaxKind.VariableDeclaration:
217                return tryCast(node.name, isIdentifier);
218            default:
219                return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`);
220        }
221    }
222
223    function updateNamespaceLikeImport(
224        changes: textChanges.ChangeTracker,
225        sourceFile: SourceFile,
226        checker: TypeChecker,
227        movedSymbols: ReadonlySymbolSet,
228        newModuleName: string,
229        newModuleSpecifier: string,
230        oldImportId: Identifier,
231        oldImportNode: SupportedImport,
232    ): void {
233        const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleName, ScriptTarget.ESNext);
234        let needUniqueName = false;
235        const toChange: Identifier[] = [];
236        FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => {
237            if (!isPropertyAccessExpression(ref.parent)) return;
238            needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true);
239            if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) {
240                toChange.push(ref);
241            }
242        });
243
244        if (toChange.length) {
245            const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName;
246            for (const ref of toChange) {
247                changes.replaceNode(sourceFile, ref, factory.createIdentifier(newNamespaceName));
248            }
249            changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier));
250        }
251    }
252
253    function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node {
254        const newNamespaceId = factory.createIdentifier(newNamespaceName);
255        const newModuleString = factory.createStringLiteral(newModuleSpecifier);
256        switch (node.kind) {
257            case SyntaxKind.ImportDeclaration:
258                return factory.createImportDeclaration(
259                    /*modifiers*/ undefined,
260                    factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)),
261                    newModuleString,
262                    /*assertClause*/ undefined);
263            case SyntaxKind.ImportEqualsDeclaration:
264                return factory.createImportEqualsDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString));
265            case SyntaxKind.VariableDeclaration:
266                return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString));
267            default:
268                return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`);
269        }
270    }
271
272    function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike {
273        return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier
274            : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression
275            : i.initializer.arguments[0]);
276    }
277
278    function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void {
279        if (isImportDeclaration(statement)) {
280            if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport);
281        }
282        else if (isImportEqualsDeclaration(statement)) {
283            if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) {
284                cb(statement as SupportedImport);
285            }
286        }
287        else if (isVariableStatement(statement)) {
288            for (const decl of statement.declarationList.declarations) {
289                if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) {
290                    cb(decl as SupportedImport);
291                }
292            }
293        }
294    }
295
296    type SupportedImport =
297        | ImportDeclaration & { moduleSpecifier: StringLiteralLike }
298        | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } }
299        | VariableDeclaration & { initializer: RequireOrImportCall };
300    type SupportedImportStatement =
301        | ImportDeclaration
302        | ImportEqualsDeclaration
303        | VariableStatement;
304
305    function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
306        let defaultImport: Identifier | undefined;
307        const imports: string[] = [];
308        newFileNeedExport.forEach(symbol => {
309            if (symbol.escapedName === InternalSymbolName.Default) {
310                defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217
311            }
312            else {
313                imports.push(symbol.name);
314            }
315        });
316        return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference);
317    }
318
319    function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
320        path = ensurePathIsNonModuleName(path);
321        if (useEs6Imports) {
322            const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i)));
323            return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference);
324        }
325        else {
326            Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module.
327            const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i));
328            return bindingElements.length
329                ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path))) as RequireVariableStatement
330                : undefined;
331        }
332    }
333
334    function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) {
335        return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags));
336    }
337
338    function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression {
339        return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]);
340    }
341
342    function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] {
343        return flatMap(toMove, statement => {
344            if (isTopLevelDeclarationStatement(statement) &&
345                !isExported(sourceFile, statement, useEs6Exports) &&
346                forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(d.symbol)))) {
347                const exports = addExport(statement, useEs6Exports);
348                if (exports) return exports;
349            }
350            return statement;
351        });
352    }
353
354    function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
355        switch (importDecl.kind) {
356            case SyntaxKind.ImportDeclaration:
357                deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused);
358                break;
359            case SyntaxKind.ImportEqualsDeclaration:
360                if (isUnused(importDecl.name)) {
361                    changes.delete(sourceFile, importDecl);
362                }
363                break;
364            case SyntaxKind.VariableDeclaration:
365                deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused);
366                break;
367            default:
368                Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`);
369        }
370    }
371    function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
372        if (!importDecl.importClause) return;
373        const { name, namedBindings } = importDecl.importClause;
374        const defaultUnused = !name || isUnused(name);
375        const namedBindingsUnused = !namedBindings ||
376            (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name)));
377        if (defaultUnused && namedBindingsUnused) {
378            changes.delete(sourceFile, importDecl);
379        }
380        else {
381            if (name && defaultUnused) {
382                changes.delete(sourceFile, name);
383            }
384            if (namedBindings) {
385                if (namedBindingsUnused) {
386                    changes.replaceNode(
387                        sourceFile,
388                        importDecl.importClause,
389                        factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined)
390                    );
391                }
392                else if (namedBindings.kind === SyntaxKind.NamedImports) {
393                    for (const element of namedBindings.elements) {
394                        if (isUnused(element.name)) changes.delete(sourceFile, element);
395                    }
396                }
397            }
398        }
399    }
400    function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) {
401        const { name } = varDecl;
402        switch (name.kind) {
403            case SyntaxKind.Identifier:
404                if (isUnused(name)) {
405                    if (varDecl.initializer && isRequireCall(varDecl.initializer, /*requireStringLiteralLikeArgument*/ true)) {
406                        changes.delete(sourceFile,
407                            isVariableDeclarationList(varDecl.parent) && length(varDecl.parent.declarations) === 1 ? varDecl.parent.parent : varDecl);
408                    }
409                    else {
410                        changes.delete(sourceFile, name);
411                    }
412                }
413                break;
414            case SyntaxKind.ArrayBindingPattern:
415                break;
416            case SyntaxKind.ObjectBindingPattern:
417                if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) {
418                    changes.delete(sourceFile,
419                        isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl);
420                }
421                else {
422                    for (const element of name.elements) {
423                        if (isIdentifier(element.name) && isUnused(element.name)) {
424                            changes.delete(sourceFile, element.name);
425                        }
426                    }
427                }
428                break;
429        }
430    }
431
432    function getNewFileImportsAndAddExportInOldFile(
433        oldFile: SourceFile,
434        importsToCopy: ReadonlySymbolSet,
435        newFileImportsFromOldFile: ReadonlySymbolSet,
436        changes: textChanges.ChangeTracker,
437        checker: TypeChecker,
438        useEsModuleSyntax: boolean,
439        quotePreference: QuotePreference,
440    ): readonly SupportedImportStatement[] {
441        const copiedOldImports: SupportedImportStatement[] = [];
442        for (const oldStatement of oldFile.statements) {
443            forEachImportInStatement(oldStatement, i => {
444                append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!)));
445            });
446        }
447
448        // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file.
449        let oldFileDefault: Identifier | undefined;
450        const oldFileNamedImports: string[] = [];
451        const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`.
452        newFileImportsFromOldFile.forEach(symbol => {
453            if (!symbol.declarations) {
454                return;
455            }
456            for (const decl of symbol.declarations) {
457                if (!isTopLevelDeclaration(decl)) continue;
458                const name = nameOfTopLevelDeclaration(decl);
459                if (!name) continue;
460
461                const top = getTopLevelDeclarationStatement(decl);
462                if (markSeenTop(top)) {
463                    addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax);
464                }
465                if (hasSyntacticModifier(decl, ModifierFlags.Default)) {
466                    oldFileDefault = name;
467                }
468                else {
469                    oldFileNamedImports.push(name.text);
470                }
471            }
472        });
473
474        append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEsModuleSyntax, quotePreference));
475        return copiedOldImports;
476    }
477
478    function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: LanguageServiceHost): string {
479        let newModuleName = moduleName;
480        for (let i = 1; ; i++) {
481            const name = combinePaths(inDirectory, newModuleName + extension);
482            if (!host.fileExists(name)) return newModuleName;
483            newModuleName = `${moduleName}.${i}`;
484        }
485    }
486
487    function getNewModuleName(importsFromNewFile: ReadonlySymbolSet, movedSymbols: ReadonlySymbolSet): string {
488        return importsFromNewFile.forEachEntry(symbolNameNoDefault) || movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile";
489    }
490
491    interface UsageInfo {
492        // Symbols whose declarations are moved from the old file to the new file.
493        readonly movedSymbols: ReadonlySymbolSet;
494
495        // Symbols declared in the old file that must be imported by the new file. (May not already be exported.)
496        readonly newFileImportsFromOldFile: ReadonlySymbolSet;
497        // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back.
498        readonly oldFileImportsFromNewFile: ReadonlySymbolSet;
499
500        readonly oldImportsNeededByNewFile: ReadonlySymbolSet;
501        // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file.
502        readonly unusedImportsFromOldFile: ReadonlySymbolSet;
503    }
504    function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo {
505        const movedSymbols = new SymbolSet();
506        const oldImportsNeededByNewFile = new SymbolSet();
507        const newFileImportsFromOldFile = new SymbolSet();
508
509        const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx));
510        const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx);
511        if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code)
512            oldImportsNeededByNewFile.add(jsxNamespaceSymbol);
513        }
514
515        for (const statement of toMove) {
516            forEachTopLevelDeclaration(statement, decl => {
517                movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here"));
518            });
519        }
520        for (const statement of toMove) {
521            forEachReference(statement, checker, symbol => {
522                if (!symbol.declarations) return;
523                for (const decl of symbol.declarations) {
524                    if (isInImport(decl)) {
525                        oldImportsNeededByNewFile.add(symbol);
526                    }
527                    else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) {
528                        newFileImportsFromOldFile.add(symbol);
529                    }
530                }
531            });
532        }
533
534        const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone();
535
536        const oldFileImportsFromNewFile = new SymbolSet();
537        for (const statement of oldFile.statements) {
538            if (contains(toMove, statement)) continue;
539
540            // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile.
541            if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) {
542                unusedImportsFromOldFile.delete(jsxNamespaceSymbol);
543            }
544
545            forEachReference(statement, checker, symbol => {
546                if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol);
547                unusedImportsFromOldFile.delete(symbol);
548            });
549        }
550
551        return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile };
552
553        function getJsxNamespaceSymbol(containsJsx: Node | undefined) {
554            if (containsJsx === undefined) {
555                return undefined;
556            }
557
558            const jsxNamespace = checker.getJsxNamespace(containsJsx);
559
560            // Strictly speaking, this could resolve to a symbol other than the JSX namespace.
561            // This will produce erroneous output (probably, an incorrectly copied import) but
562            // is expected to be very rare and easily reversible.
563            const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true);
564
565            return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport)
566                ? jsxNamespaceSymbol
567                : undefined;
568        }
569    }
570
571    // Below should all be utilities
572
573    function isInImport(decl: Declaration) {
574        switch (decl.kind) {
575            case SyntaxKind.ImportEqualsDeclaration:
576            case SyntaxKind.ImportSpecifier:
577            case SyntaxKind.ImportClause:
578            case SyntaxKind.NamespaceImport:
579                return true;
580            case SyntaxKind.VariableDeclaration:
581                return isVariableDeclarationInImport(decl as VariableDeclaration);
582            case SyntaxKind.BindingElement:
583                return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent);
584            default:
585                return false;
586        }
587    }
588    function isVariableDeclarationInImport(decl: VariableDeclaration) {
589        return isSourceFile(decl.parent.parent.parent) &&
590            !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true);
591    }
592
593    function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined {
594        switch (i.kind) {
595            case SyntaxKind.ImportDeclaration: {
596                const clause = i.importClause;
597                if (!clause) return undefined;
598                const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined;
599                const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep);
600                return defaultImport || namedBindings
601                    ? factory.createImportDeclaration(/*modifiers*/ undefined, factory.createImportClause(/*isTypeOnly*/ false, defaultImport, namedBindings), moduleSpecifier, /*assertClause*/ undefined)
602                    : undefined;
603            }
604            case SyntaxKind.ImportEqualsDeclaration:
605                return keep(i.name) ? i : undefined;
606            case SyntaxKind.VariableDeclaration: {
607                const name = filterBindingName(i.name, keep);
608                return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined;
609            }
610            default:
611                return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`);
612        }
613    }
614    function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined {
615        if (namedBindings.kind === SyntaxKind.NamespaceImport) {
616            return keep(namedBindings.name) ? namedBindings : undefined;
617        }
618        else {
619            const newElements = namedBindings.elements.filter(e => keep(e.name));
620            return newElements.length ? factory.createNamedImports(newElements) : undefined;
621        }
622    }
623    function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined {
624        switch (name.kind) {
625            case SyntaxKind.Identifier:
626                return keep(name) ? name : undefined;
627            case SyntaxKind.ArrayBindingPattern:
628                return name;
629            case SyntaxKind.ObjectBindingPattern: {
630                // We can't handle nested destructurings or property names well here, so just copy them all.
631                const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name));
632                return newElements.length ? factory.createObjectBindingPattern(newElements) : undefined;
633            }
634        }
635    }
636
637    function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) {
638        node.forEachChild(function cb(node) {
639            if (isIdentifier(node) && !isDeclarationName(node)) {
640                const sym = checker.getSymbolAtLocation(node);
641                if (sym) onReference(sym);
642            }
643            else {
644                node.forEachChild(cb);
645            }
646        });
647    }
648
649    interface ReadonlySymbolSet {
650        size(): number;
651        has(symbol: Symbol): boolean;
652        forEach(cb: (symbol: Symbol) => void): void;
653        forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined;
654    }
655
656    class SymbolSet implements ReadonlySymbolSet {
657        private map = new Map<string, Symbol>();
658        add(symbol: Symbol): void {
659            this.map.set(String(getSymbolId(symbol)), symbol);
660        }
661        has(symbol: Symbol): boolean {
662            return this.map.has(String(getSymbolId(symbol)));
663        }
664        delete(symbol: Symbol): void {
665            this.map.delete(String(getSymbolId(symbol)));
666        }
667        forEach(cb: (symbol: Symbol) => void): void {
668            this.map.forEach(cb);
669        }
670        forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined {
671            return forEachEntry(this.map, cb);
672        }
673        clone(): SymbolSet {
674            const clone = new SymbolSet();
675            copyEntries(this.map, clone.map);
676            return clone;
677        }
678        size() {
679            return this.map.size;
680        }
681    }
682
683    type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...'
684    type NonVariableTopLevelDeclaration =
685        | FunctionDeclaration
686        | ClassDeclaration
687        | EnumDeclaration
688        | TypeAliasDeclaration
689        | InterfaceDeclaration
690        | ModuleDeclaration
691        | TopLevelExpressionStatement
692        | ImportEqualsDeclaration;
693    type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement;
694    interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; }
695    type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement;
696    function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration {
697        return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent);
698    }
699
700    function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node {
701        return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent;
702    }
703
704    function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement {
705        Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile");
706        return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node);
707    }
708
709    function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration {
710        switch (node.kind) {
711            case SyntaxKind.FunctionDeclaration:
712            case SyntaxKind.ClassDeclaration:
713            case SyntaxKind.ModuleDeclaration:
714            case SyntaxKind.EnumDeclaration:
715            case SyntaxKind.TypeAliasDeclaration:
716            case SyntaxKind.InterfaceDeclaration:
717            case SyntaxKind.ImportEqualsDeclaration:
718                return true;
719            default:
720                return false;
721        }
722    }
723
724    function forEachTopLevelDeclaration<T>(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined {
725        switch (statement.kind) {
726            case SyntaxKind.FunctionDeclaration:
727            case SyntaxKind.ClassDeclaration:
728            case SyntaxKind.ModuleDeclaration:
729            case SyntaxKind.EnumDeclaration:
730            case SyntaxKind.TypeAliasDeclaration:
731            case SyntaxKind.InterfaceDeclaration:
732            case SyntaxKind.ImportEqualsDeclaration:
733                return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration);
734
735            case SyntaxKind.VariableStatement:
736                return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb));
737
738            case SyntaxKind.ExpressionStatement: {
739                const { expression } = statement as ExpressionStatement;
740                return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty
741                    ? cb(statement as TopLevelExpressionStatement)
742                    : undefined;
743            }
744        }
745    }
746    function forEachTopLevelDeclarationInBindingName<T>(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined {
747        switch (name.kind) {
748            case SyntaxKind.Identifier:
749                return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x)));
750            case SyntaxKind.ArrayBindingPattern:
751            case SyntaxKind.ObjectBindingPattern:
752                return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb));
753            default:
754                return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`);
755        }
756    }
757
758    function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined {
759        return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier);
760    }
761
762    function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement {
763        switch (d.kind) {
764            case SyntaxKind.VariableDeclaration:
765                return d.parent.parent;
766            case SyntaxKind.BindingElement:
767                return getTopLevelDeclarationStatement(
768                    cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p)));
769            default:
770                return d;
771        }
772    }
773
774    function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void {
775        if (isExported(sourceFile, decl, useEs6Exports, name)) return;
776        if (useEs6Exports) {
777            if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl);
778        }
779        else {
780            const names = getNamesToExportInCommonJS(decl);
781            if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment));
782        }
783    }
784
785    function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean {
786        if (useEs6Exports) {
787            return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol.exports?.has(name.escapedText));
788        }
789        return !!sourceFile.symbol && !!sourceFile.symbol.exports &&
790            getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name)));
791    }
792
793    function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined {
794        return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl);
795    }
796    function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement {
797        const modifiers = canHaveModifiers(d) ? concatenate([factory.createModifier(SyntaxKind.ExportKeyword)], getModifiers(d)) : undefined;
798        switch (d.kind) {
799            case SyntaxKind.FunctionDeclaration:
800                return factory.updateFunctionDeclaration(d, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body);
801            case SyntaxKind.ClassDeclaration:
802                const decorators = canHaveDecorators(d) ? getDecorators(d) : undefined;
803                return factory.updateClassDeclaration(d, concatenate<ModifierLike>(decorators, modifiers), d.name, d.typeParameters, d.heritageClauses, d.members);
804            case SyntaxKind.VariableStatement:
805                return factory.updateVariableStatement(d, modifiers, d.declarationList);
806            case SyntaxKind.ModuleDeclaration:
807                return factory.updateModuleDeclaration(d, modifiers, d.name, d.body);
808            case SyntaxKind.EnumDeclaration:
809                return factory.updateEnumDeclaration(d, modifiers, d.name, d.members);
810            case SyntaxKind.TypeAliasDeclaration:
811                return factory.updateTypeAliasDeclaration(d, modifiers, d.name, d.typeParameters, d.type);
812            case SyntaxKind.InterfaceDeclaration:
813                return factory.updateInterfaceDeclaration(d, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members);
814            case SyntaxKind.ImportEqualsDeclaration:
815                return factory.updateImportEqualsDeclaration(d, modifiers, d.isTypeOnly, d.name, d.moduleReference);
816            case SyntaxKind.ExpressionStatement:
817                return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...`
818            default:
819                return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`);
820        }
821    }
822    function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined {
823        return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)];
824    }
825    function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] {
826        switch (decl.kind) {
827            case SyntaxKind.FunctionDeclaration:
828            case SyntaxKind.ClassDeclaration:
829                return [decl.name!.text]; // TODO: GH#18217
830            case SyntaxKind.VariableStatement:
831                return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined);
832            case SyntaxKind.ModuleDeclaration:
833            case SyntaxKind.EnumDeclaration:
834            case SyntaxKind.TypeAliasDeclaration:
835            case SyntaxKind.InterfaceDeclaration:
836            case SyntaxKind.ImportEqualsDeclaration:
837                return emptyArray;
838            case SyntaxKind.ExpressionStatement:
839                return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...`
840            default:
841                return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`);
842        }
843    }
844
845    /** Creates `exports.x = x;` */
846    function createExportAssignment(name: string): Statement {
847        return factory.createExpressionStatement(
848            factory.createBinaryExpression(
849                factory.createPropertyAccessExpression(factory.createIdentifier("exports"), factory.createIdentifier(name)),
850                SyntaxKind.EqualsToken,
851                factory.createIdentifier(name)));
852    }
853}
854