• 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(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(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.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 && !oldFile.commonJsModuleIndicator) {
137            deleteMovedStatements(oldFile, toMove.ranges, changes);
138            return [...prologueDirectives, ...toMove.all];
139        }
140
141        const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator;
142        const quotePreference = getQuotePreference(oldFile, preferences);
143        const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, 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, useEs6ModuleSyntax, quotePreference);
153        const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax);
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                    /*decorators*/ undefined, /*modifiers*/ undefined,
260                    factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)),
261                    newModuleString);
262            case SyntaxKind.ImportEqualsDeclaration:
263                return factory.createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString));
264            case SyntaxKind.VariableDeclaration:
265                return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString));
266            default:
267                return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`);
268        }
269    }
270
271    function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike {
272        return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier
273            : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression
274            : i.initializer.arguments[0]);
275    }
276
277    function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void {
278        if (isImportDeclaration(statement)) {
279            if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport);
280        }
281        else if (isImportEqualsDeclaration(statement)) {
282            if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) {
283                cb(statement as SupportedImport);
284            }
285        }
286        else if (isVariableStatement(statement)) {
287            for (const decl of statement.declarationList.declarations) {
288                if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) {
289                    cb(decl as SupportedImport);
290                }
291            }
292        }
293    }
294
295    type SupportedImport =
296        | ImportDeclaration & { moduleSpecifier: StringLiteralLike }
297        | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } }
298        | VariableDeclaration & { initializer: RequireOrImportCall };
299    type SupportedImportStatement =
300        | ImportDeclaration
301        | ImportEqualsDeclaration
302        | VariableStatement;
303
304    function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
305        let defaultImport: Identifier | undefined;
306        const imports: string[] = [];
307        newFileNeedExport.forEach(symbol => {
308            if (symbol.escapedName === InternalSymbolName.Default) {
309                defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217
310            }
311            else {
312                imports.push(symbol.name);
313            }
314        });
315        return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference);
316    }
317
318    function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
319        path = ensurePathIsNonModuleName(path);
320        if (useEs6Imports) {
321            const specifiers = imports.map(i => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(i)));
322            return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference);
323        }
324        else {
325            Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module.
326            const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i));
327            return bindingElements.length
328                ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path))) as RequireVariableStatement
329                : undefined;
330        }
331    }
332
333    function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) {
334        return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags));
335    }
336
337    function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression {
338        return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]);
339    }
340
341    function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] {
342        return flatMap(toMove, statement => {
343            if (isTopLevelDeclarationStatement(statement) &&
344                !isExported(sourceFile, statement, useEs6Exports) &&
345                forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(d.symbol)))) {
346                const exports = addExport(statement, useEs6Exports);
347                if (exports) return exports;
348            }
349            return statement;
350        });
351    }
352
353    function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
354        switch (importDecl.kind) {
355            case SyntaxKind.ImportDeclaration:
356                deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused);
357                break;
358            case SyntaxKind.ImportEqualsDeclaration:
359                if (isUnused(importDecl.name)) {
360                    changes.delete(sourceFile, importDecl);
361                }
362                break;
363            case SyntaxKind.VariableDeclaration:
364                deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused);
365                break;
366            default:
367                Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`);
368        }
369    }
370    function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
371        if (!importDecl.importClause) return;
372        const { name, namedBindings } = importDecl.importClause;
373        const defaultUnused = !name || isUnused(name);
374        const namedBindingsUnused = !namedBindings ||
375            (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name)));
376        if (defaultUnused && namedBindingsUnused) {
377            changes.delete(sourceFile, importDecl);
378        }
379        else {
380            if (name && defaultUnused) {
381                changes.delete(sourceFile, name);
382            }
383            if (namedBindings) {
384                if (namedBindingsUnused) {
385                    changes.replaceNode(
386                        sourceFile,
387                        importDecl.importClause,
388                        factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined)
389                    );
390                }
391                else if (namedBindings.kind === SyntaxKind.NamedImports) {
392                    for (const element of namedBindings.elements) {
393                        if (isUnused(element.name)) changes.delete(sourceFile, element);
394                    }
395                }
396            }
397        }
398    }
399    function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) {
400        const { name } = varDecl;
401        switch (name.kind) {
402            case SyntaxKind.Identifier:
403                if (isUnused(name)) {
404                    changes.delete(sourceFile, name);
405                }
406                break;
407            case SyntaxKind.ArrayBindingPattern:
408                break;
409            case SyntaxKind.ObjectBindingPattern:
410                if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) {
411                    changes.delete(sourceFile,
412                        isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl);
413                }
414                else {
415                    for (const element of name.elements) {
416                        if (isIdentifier(element.name) && isUnused(element.name)) {
417                            changes.delete(sourceFile, element.name);
418                        }
419                    }
420                }
421                break;
422        }
423    }
424
425    function getNewFileImportsAndAddExportInOldFile(
426        oldFile: SourceFile,
427        importsToCopy: ReadonlySymbolSet,
428        newFileImportsFromOldFile: ReadonlySymbolSet,
429        changes: textChanges.ChangeTracker,
430        checker: TypeChecker,
431        useEs6ModuleSyntax: boolean,
432        quotePreference: QuotePreference,
433    ): readonly SupportedImportStatement[] {
434        const copiedOldImports: SupportedImportStatement[] = [];
435        for (const oldStatement of oldFile.statements) {
436            forEachImportInStatement(oldStatement, i => {
437                append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!)));
438            });
439        }
440
441        // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file.
442        let oldFileDefault: Identifier | undefined;
443        const oldFileNamedImports: string[] = [];
444        const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`.
445        newFileImportsFromOldFile.forEach(symbol => {
446            for (const decl of symbol.declarations) {
447                if (!isTopLevelDeclaration(decl)) continue;
448                const name = nameOfTopLevelDeclaration(decl);
449                if (!name) continue;
450
451                const top = getTopLevelDeclarationStatement(decl);
452                if (markSeenTop(top)) {
453                    addExportToChanges(oldFile, top, changes, useEs6ModuleSyntax);
454                }
455                if (hasSyntacticModifier(decl, ModifierFlags.Default)) {
456                    oldFileDefault = name;
457                }
458                else {
459                    oldFileNamedImports.push(name.text);
460                }
461            }
462        });
463
464        append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, quotePreference));
465        return copiedOldImports;
466    }
467
468    function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: LanguageServiceHost): string {
469        let newModuleName = moduleName;
470        for (let i = 1; ; i++) {
471            const name = combinePaths(inDirectory, newModuleName + extension);
472            if (!host.fileExists!(name)) return newModuleName; // TODO: GH#18217
473            newModuleName = `${moduleName}.${i}`;
474        }
475    }
476
477    function getNewModuleName(movedSymbols: ReadonlySymbolSet): string {
478        return movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile";
479    }
480
481    interface UsageInfo {
482        // Symbols whose declarations are moved from the old file to the new file.
483        readonly movedSymbols: ReadonlySymbolSet;
484
485        // Symbols declared in the old file that must be imported by the new file. (May not already be exported.)
486        readonly newFileImportsFromOldFile: ReadonlySymbolSet;
487        // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back.
488        readonly oldFileImportsFromNewFile: ReadonlySymbolSet;
489
490        readonly oldImportsNeededByNewFile: ReadonlySymbolSet;
491        // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file.
492        readonly unusedImportsFromOldFile: ReadonlySymbolSet;
493    }
494    function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo {
495        const movedSymbols = new SymbolSet();
496        const oldImportsNeededByNewFile = new SymbolSet();
497        const newFileImportsFromOldFile = new SymbolSet();
498
499        const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx));
500        const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx);
501        if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code)
502            oldImportsNeededByNewFile.add(jsxNamespaceSymbol);
503        }
504
505        for (const statement of toMove) {
506            forEachTopLevelDeclaration(statement, decl => {
507                movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here"));
508            });
509        }
510        for (const statement of toMove) {
511            forEachReference(statement, checker, symbol => {
512                if (!symbol.declarations) return;
513                for (const decl of symbol.declarations) {
514                    if (isInImport(decl)) {
515                        oldImportsNeededByNewFile.add(symbol);
516                    }
517                    else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) {
518                        newFileImportsFromOldFile.add(symbol);
519                    }
520                }
521            });
522        }
523
524        const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone();
525
526        const oldFileImportsFromNewFile = new SymbolSet();
527        for (const statement of oldFile.statements) {
528            if (contains(toMove, statement)) continue;
529
530            // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile.
531            if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) {
532                unusedImportsFromOldFile.delete(jsxNamespaceSymbol);
533            }
534
535            forEachReference(statement, checker, symbol => {
536                if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol);
537                unusedImportsFromOldFile.delete(symbol);
538            });
539        }
540
541        return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile };
542
543        function getJsxNamespaceSymbol(containsJsx: Node | undefined) {
544            if (containsJsx === undefined) {
545                return undefined;
546            }
547
548            const jsxNamespace = checker.getJsxNamespace(containsJsx);
549
550            // Strictly speaking, this could resolve to a symbol other than the JSX namespace.
551            // This will produce erroneous output (probably, an incorrectly copied import) but
552            // is expected to be very rare and easily reversible.
553            const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true);
554
555            return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport)
556                ? jsxNamespaceSymbol
557                : undefined;
558        }
559    }
560
561    // Below should all be utilities
562
563    function isInImport(decl: Declaration) {
564        switch (decl.kind) {
565            case SyntaxKind.ImportEqualsDeclaration:
566            case SyntaxKind.ImportSpecifier:
567            case SyntaxKind.ImportClause:
568            case SyntaxKind.NamespaceImport:
569                return true;
570            case SyntaxKind.VariableDeclaration:
571                return isVariableDeclarationInImport(decl as VariableDeclaration);
572            case SyntaxKind.BindingElement:
573                return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent);
574            default:
575                return false;
576        }
577    }
578    function isVariableDeclarationInImport(decl: VariableDeclaration) {
579        return isSourceFile(decl.parent.parent.parent) &&
580            !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true);
581    }
582
583    function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined {
584        switch (i.kind) {
585            case SyntaxKind.ImportDeclaration: {
586                const clause = i.importClause;
587                if (!clause) return undefined;
588                const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined;
589                const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep);
590                return defaultImport || namedBindings
591                    ? factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, factory.createImportClause(/*isTypeOnly*/ false, defaultImport, namedBindings), moduleSpecifier)
592                    : undefined;
593            }
594            case SyntaxKind.ImportEqualsDeclaration:
595                return keep(i.name) ? i : undefined;
596            case SyntaxKind.VariableDeclaration: {
597                const name = filterBindingName(i.name, keep);
598                return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined;
599            }
600            default:
601                return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`);
602        }
603    }
604    function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined {
605        if (namedBindings.kind === SyntaxKind.NamespaceImport) {
606            return keep(namedBindings.name) ? namedBindings : undefined;
607        }
608        else {
609            const newElements = namedBindings.elements.filter(e => keep(e.name));
610            return newElements.length ? factory.createNamedImports(newElements) : undefined;
611        }
612    }
613    function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined {
614        switch (name.kind) {
615            case SyntaxKind.Identifier:
616                return keep(name) ? name : undefined;
617            case SyntaxKind.ArrayBindingPattern:
618                return name;
619            case SyntaxKind.ObjectBindingPattern: {
620                // We can't handle nested destructurings or property names well here, so just copy them all.
621                const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name));
622                return newElements.length ? factory.createObjectBindingPattern(newElements) : undefined;
623            }
624        }
625    }
626
627    function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) {
628        node.forEachChild(function cb(node) {
629            if (isIdentifier(node) && !isDeclarationName(node)) {
630                const sym = checker.getSymbolAtLocation(node);
631                if (sym) onReference(sym);
632            }
633            else {
634                node.forEachChild(cb);
635            }
636        });
637    }
638
639    interface ReadonlySymbolSet {
640        has(symbol: Symbol): boolean;
641        forEach(cb: (symbol: Symbol) => void): void;
642        forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined;
643    }
644    class SymbolSet implements ReadonlySymbolSet {
645        private map = new Map<string, Symbol>();
646        add(symbol: Symbol): void {
647            this.map.set(String(getSymbolId(symbol)), symbol);
648        }
649        has(symbol: Symbol): boolean {
650            return this.map.has(String(getSymbolId(symbol)));
651        }
652        delete(symbol: Symbol): void {
653            this.map.delete(String(getSymbolId(symbol)));
654        }
655        forEach(cb: (symbol: Symbol) => void): void {
656            this.map.forEach(cb);
657        }
658        forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined {
659            return forEachEntry(this.map, cb);
660        }
661        clone(): SymbolSet {
662            const clone = new SymbolSet();
663            copyEntries(this.map, clone.map);
664            return clone;
665        }
666    }
667
668    type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...'
669    type NonVariableTopLevelDeclaration =
670        | FunctionDeclaration
671        | ClassDeclaration
672        | EnumDeclaration
673        | TypeAliasDeclaration
674        | InterfaceDeclaration
675        | ModuleDeclaration
676        | TopLevelExpressionStatement
677        | ImportEqualsDeclaration;
678    type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement;
679    interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; }
680    type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement;
681    function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration {
682        return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent);
683    }
684
685    function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node {
686        return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent;
687    }
688
689    function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement {
690        Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile");
691        return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node);
692    }
693
694    function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration {
695        switch (node.kind) {
696            case SyntaxKind.FunctionDeclaration:
697            case SyntaxKind.ClassDeclaration:
698            case SyntaxKind.ModuleDeclaration:
699            case SyntaxKind.EnumDeclaration:
700            case SyntaxKind.TypeAliasDeclaration:
701            case SyntaxKind.InterfaceDeclaration:
702            case SyntaxKind.ImportEqualsDeclaration:
703                return true;
704            default:
705                return false;
706        }
707    }
708
709    function forEachTopLevelDeclaration<T>(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined {
710        switch (statement.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 cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration);
719
720            case SyntaxKind.VariableStatement:
721                return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb));
722
723            case SyntaxKind.ExpressionStatement: {
724                const { expression } = statement as ExpressionStatement;
725                return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty
726                    ? cb(statement as TopLevelExpressionStatement)
727                    : undefined;
728            }
729        }
730    }
731    function forEachTopLevelDeclarationInBindingName<T>(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined {
732        switch (name.kind) {
733            case SyntaxKind.Identifier:
734                return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x)));
735            case SyntaxKind.ArrayBindingPattern:
736            case SyntaxKind.ObjectBindingPattern:
737                return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb));
738            default:
739                return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`);
740        }
741    }
742
743    function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined {
744        return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier);
745    }
746
747    function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement {
748        switch (d.kind) {
749            case SyntaxKind.VariableDeclaration:
750                return d.parent.parent;
751            case SyntaxKind.BindingElement:
752                return getTopLevelDeclarationStatement(
753                    cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p)));
754            default:
755                return d;
756        }
757    }
758
759    function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void {
760        if (isExported(sourceFile, decl, useEs6Exports)) return;
761        if (useEs6Exports) {
762            if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl);
763        }
764        else {
765            const names = getNamesToExportInCommonJS(decl);
766            if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment));
767        }
768    }
769
770    function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean): boolean {
771        if (useEs6Exports) {
772            return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export);
773        }
774        else {
775            return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name)));
776        }
777    }
778
779    function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined {
780        return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl);
781    }
782    function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement {
783        const modifiers = concatenate([factory.createModifier(SyntaxKind.ExportKeyword)], d.modifiers);
784        switch (d.kind) {
785            case SyntaxKind.FunctionDeclaration:
786                return factory.updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body);
787            case SyntaxKind.ClassDeclaration:
788                return factory.updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members);
789            case SyntaxKind.VariableStatement:
790                return factory.updateVariableStatement(d, modifiers, d.declarationList);
791            case SyntaxKind.ModuleDeclaration:
792                return factory.updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body);
793            case SyntaxKind.EnumDeclaration:
794                return factory.updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members);
795            case SyntaxKind.TypeAliasDeclaration:
796                return factory.updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type);
797            case SyntaxKind.InterfaceDeclaration:
798                return factory.updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members);
799            case SyntaxKind.ImportEqualsDeclaration:
800                return factory.updateImportEqualsDeclaration(d, d.decorators, modifiers, d.isTypeOnly, d.name, d.moduleReference);
801            case SyntaxKind.ExpressionStatement:
802                return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...`
803            default:
804                return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`);
805        }
806    }
807    function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined {
808        return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)];
809    }
810    function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] {
811        switch (decl.kind) {
812            case SyntaxKind.FunctionDeclaration:
813            case SyntaxKind.ClassDeclaration:
814                return [decl.name!.text]; // TODO: GH#18217
815            case SyntaxKind.VariableStatement:
816                return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined);
817            case SyntaxKind.ModuleDeclaration:
818            case SyntaxKind.EnumDeclaration:
819            case SyntaxKind.TypeAliasDeclaration:
820            case SyntaxKind.InterfaceDeclaration:
821            case SyntaxKind.ImportEqualsDeclaration:
822                return emptyArray;
823            case SyntaxKind.ExpressionStatement:
824                return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...`
825            default:
826                return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`);
827        }
828    }
829
830    /** Creates `exports.x = x;` */
831    function createExportAssignment(name: string): Statement {
832        return factory.createExpressionStatement(
833            factory.createBinaryExpression(
834                factory.createPropertyAccessExpression(factory.createIdentifier("exports"), factory.createIdentifier(name)),
835                SyntaxKind.EqualsToken,
836                factory.createIdentifier(name)));
837    }
838}
839