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