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