1/* @internal */ 2namespace ts.OrganizeImports { 3 4 /** 5 * Organize imports by: 6 * 1) Removing unused imports 7 * 2) Coalescing imports from the same module 8 * 3) Sorting imports 9 */ 10 export function organizeImports( 11 sourceFile: SourceFile, 12 formatContext: formatting.FormatContext, 13 host: LanguageServiceHost, 14 program: Program, 15 preferences: UserPreferences, 16 mode: OrganizeImportsMode, 17 ) { 18 const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); 19 const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All; 20 const shouldCombine = shouldSort; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. 21 const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; 22 const maybeRemove = shouldRemove ? removeUnusedImports : identity; 23 const maybeCoalesce = shouldCombine ? coalesceImports : identity; 24 const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { 25 const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program)); 26 return shouldSort 27 ? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2)) 28 : processedDeclarations; 29 }; 30 31 // All of the old ImportDeclarations in the file, in syntactic order. 32 const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); 33 topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); 34 35 // Exports are always used 36 if (mode !== OrganizeImportsMode.RemoveUnused) { 37 // All of the old ExportDeclarations in the file, in syntactic order. 38 const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration); 39 organizeImportsWorker(topLevelExportDecls, coalesceExports); 40 } 41 42 for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { 43 if (!ambientModule.body) continue; 44 45 const ambientModuleImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(isImportDeclaration)); 46 ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); 47 48 // Exports are always used 49 if (mode !== OrganizeImportsMode.RemoveUnused) { 50 const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); 51 organizeImportsWorker(ambientModuleExportDecls, coalesceExports); 52 } 53 } 54 55 return changeTracker.getChanges(); 56 57 function organizeImportsWorker<T extends ImportDeclaration | ExportDeclaration>( 58 oldImportDecls: readonly T[], 59 coalesce: (group: readonly T[]) => readonly T[], 60 ) { 61 if (length(oldImportDecls) === 0) { 62 return; 63 } 64 65 // Special case: normally, we'd expect leading and trailing trivia to follow each import 66 // around as it's sorted. However, we do not want this to happen for leading trivia 67 // on the first import because it is probably the header comment for the file. 68 // Consider: we could do a more careful check that this trivia is actually a header, 69 // but the consequences of being wrong are very minor. 70 suppressLeadingTrivia(oldImportDecls[0]); 71 72 const oldImportGroups = shouldCombine 73 ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!) 74 : [oldImportDecls]; 75 const sortedImportGroups = shouldSort 76 ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier)) 77 : oldImportGroups; 78 const newImportDecls = flatMap(sortedImportGroups, importGroup => 79 getExternalModuleName(importGroup[0].moduleSpecifier!) 80 ? coalesce(importGroup) 81 : importGroup); 82 83 // Delete all nodes if there are no imports. 84 if (newImportDecls.length === 0) { 85 // Consider the first node to have trailingTrivia as we want to exclude the 86 // "header" comment. 87 changeTracker.deleteNodes(sourceFile, oldImportDecls, { 88 trailingTriviaOption: textChanges.TrailingTriviaOption.Include, 89 }, /*hasTrailingComment*/ true); 90 } 91 else { 92 // Note: Delete the surrounding trivia because it will have been retained in newImportDecls. 93 const replaceOptions = { 94 leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, // Leave header comment in place 95 trailingTriviaOption: textChanges.TrailingTriviaOption.Include, 96 suffix: getNewLineOrDefaultFromHost(host, formatContext.options), 97 }; 98 changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions); 99 const hasTrailingComment = changeTracker.nodeHasTrailingComment(sourceFile, oldImportDecls[0], replaceOptions); 100 changeTracker.deleteNodes(sourceFile, oldImportDecls.slice(1), { 101 trailingTriviaOption: textChanges.TrailingTriviaOption.Include, 102 }, hasTrailingComment); 103 } 104 } 105 } 106 107 function groupImportsByNewlineContiguous(sourceFile: SourceFile, importDecls: ImportDeclaration[]): ImportDeclaration[][] { 108 const scanner = createScanner(sourceFile.languageVersion, /*skipTrivia*/ false, sourceFile.languageVariant); 109 const groupImports: ImportDeclaration[][] = []; 110 let groupIndex = 0; 111 for (const topLevelImportDecl of importDecls) { 112 if (isNewGroup(sourceFile, topLevelImportDecl, scanner)) { 113 groupIndex++; 114 } 115 116 if (!groupImports[groupIndex]) { 117 groupImports[groupIndex] = []; 118 } 119 120 groupImports[groupIndex].push(topLevelImportDecl); 121 } 122 123 return groupImports; 124 } 125 126 // a new group is created if an import includes at least two new line 127 // new line from multi-line comment doesn't count 128 function isNewGroup(sourceFile: SourceFile, topLevelImportDecl: ImportDeclaration, scanner: Scanner) { 129 const startPos = topLevelImportDecl.getFullStart(); 130 const endPos = topLevelImportDecl.getStart(); 131 scanner.setText(sourceFile.text, startPos, endPos - startPos); 132 133 let numberOfNewLines = 0; 134 while (scanner.getTokenPos() < endPos) { 135 const tokenKind = scanner.scan(); 136 137 if (tokenKind === SyntaxKind.NewLineTrivia) { 138 numberOfNewLines++; 139 140 if (numberOfNewLines >= 2) { 141 return true; 142 } 143 } 144 } 145 146 return false; 147 } 148 149 function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { 150 const typeChecker = program.getTypeChecker(); 151 const compilerOptions = program.getCompilerOptions(); 152 const jsxNamespace = typeChecker.getJsxNamespace(sourceFile); 153 const jsxFragmentFactory = typeChecker.getJsxFragmentFactory(sourceFile); 154 const jsxElementsPresent = !!(sourceFile.transformFlags & TransformFlags.ContainsJsx); 155 156 const usedImports: ImportDeclaration[] = []; 157 158 for (const importDecl of oldImports) { 159 const { importClause, moduleSpecifier } = importDecl; 160 161 if (!importClause) { 162 // Imports without import clauses are assumed to be included for their side effects and are not removed. 163 usedImports.push(importDecl); 164 continue; 165 } 166 167 let { name, namedBindings } = importClause; 168 169 // Default import 170 if (name && !isDeclarationUsed(name)) { 171 name = undefined; 172 } 173 174 if (namedBindings) { 175 if (isNamespaceImport(namedBindings)) { 176 // Namespace import 177 if (!isDeclarationUsed(namedBindings.name)) { 178 namedBindings = undefined; 179 } 180 } 181 else { 182 // List of named imports 183 const newElements = namedBindings.elements.filter(e => isDeclarationUsed(e.name)); 184 if (newElements.length < namedBindings.elements.length) { 185 namedBindings = newElements.length 186 ? factory.updateNamedImports(namedBindings, newElements) 187 : undefined; 188 } 189 } 190 } 191 192 if (name || namedBindings) { 193 usedImports.push(updateImportDeclarationAndClause(importDecl, name, namedBindings)); 194 } 195 // If a module is imported to be augmented, it’s used 196 else if (hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier)) { 197 // If we’re in a declaration file, it’s safe to remove the import clause from it 198 if (sourceFile.isDeclarationFile) { 199 usedImports.push(factory.createImportDeclaration( 200 importDecl.modifiers, 201 /*importClause*/ undefined, 202 moduleSpecifier, 203 /*assertClause*/ undefined)); 204 } 205 // If we’re not in a declaration file, we can’t remove the import clause even though 206 // the imported symbols are unused, because removing them makes it look like the import 207 // declaration has side effects, which will cause it to be preserved in the JS emit. 208 else { 209 usedImports.push(importDecl); 210 } 211 } 212 } 213 214 return usedImports; 215 216 function isDeclarationUsed(identifier: Identifier) { 217 // The JSX factory symbol is always used if JSX elements are present - even if they are not allowed. 218 return jsxElementsPresent && (identifier.text === jsxNamespace || jsxFragmentFactory && identifier.text === jsxFragmentFactory) && jsxModeNeedsExplicitImport(compilerOptions.jsx) || 219 FindAllReferences.Core.isSymbolReferencedInFile(identifier, typeChecker, sourceFile); 220 } 221 } 222 223 function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { 224 const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; 225 return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => 226 isStringLiteral(moduleName) 227 && moduleName.text === moduleSpecifierText); 228 } 229 230 function getExternalModuleName(specifier: Expression) { 231 return specifier !== undefined && isStringLiteralLike(specifier) 232 ? specifier.text 233 : undefined; 234 } 235 236 // Internal for testing 237 /** 238 * @param importGroup a list of ImportDeclarations, all with the same module name. 239 */ 240 export function coalesceImports(importGroup: readonly ImportDeclaration[]) { 241 if (importGroup.length === 0) { 242 return importGroup; 243 } 244 245 const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup); 246 247 const coalescedImports: ImportDeclaration[] = []; 248 249 if (importWithoutClause) { 250 coalescedImports.push(importWithoutClause); 251 } 252 253 for (const group of [regularImports, typeOnlyImports]) { 254 const isTypeOnly = group === typeOnlyImports; 255 const { defaultImports, namespaceImports, namedImports } = group; 256 // Normally, we don't combine default and namespace imports, but it would be silly to 257 // produce two import declarations in this special case. 258 if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) { 259 // Add the namespace import to the existing default ImportDeclaration. 260 const defaultImport = defaultImports[0]; 261 coalescedImports.push( 262 updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217 263 264 continue; 265 } 266 267 const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => 268 compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217 269 270 for (const namespaceImport of sortedNamespaceImports) { 271 // Drop the name, if any 272 coalescedImports.push( 273 updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217 274 } 275 276 if (defaultImports.length === 0 && namedImports.length === 0) { 277 continue; 278 } 279 280 let newDefaultImport: Identifier | undefined; 281 const newImportSpecifiers: ImportSpecifier[] = []; 282 if (defaultImports.length === 1) { 283 newDefaultImport = defaultImports[0].importClause!.name; 284 } 285 else { 286 for (const defaultImport of defaultImports) { 287 newImportSpecifiers.push( 288 factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217 289 } 290 } 291 292 newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); 293 294 const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers); 295 const importDecl = defaultImports.length > 0 296 ? defaultImports[0] 297 : namedImports[0]; 298 299 const newNamedImports = sortedImportSpecifiers.length === 0 300 ? newDefaultImport 301 ? undefined 302 : factory.createNamedImports(emptyArray) 303 : namedImports.length === 0 304 ? factory.createNamedImports(sortedImportSpecifiers) 305 : factory.updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217 306 307 // Type-only imports are not allowed to mix default, namespace, and named imports in any combination. 308 // We could rewrite a default import as a named import (`import { default as name }`), but we currently 309 // choose not to as a stylistic preference. 310 if (isTypeOnly && newDefaultImport && newNamedImports) { 311 coalescedImports.push( 312 updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined)); 313 coalescedImports.push( 314 updateImportDeclarationAndClause(namedImports[0] ?? importDecl, /*name*/ undefined, newNamedImports)); 315 } 316 else { 317 coalescedImports.push( 318 updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports)); 319 } 320 } 321 322 return coalescedImports; 323 324 } 325 326 interface ImportGroup { 327 defaultImports: ImportDeclaration[]; 328 namespaceImports: ImportDeclaration[]; 329 namedImports: ImportDeclaration[]; 330 } 331 332 /* 333 * Returns entire import declarations because they may already have been rewritten and 334 * may lack parent pointers. The desired parts can easily be recovered based on the 335 * categorization. 336 * 337 * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. 338 */ 339 function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { 340 let importWithoutClause: ImportDeclaration | undefined; 341 const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; 342 const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; 343 344 for (const importDeclaration of importGroup) { 345 if (importDeclaration.importClause === undefined) { 346 // Only the first such import is interesting - the others are redundant. 347 // Note: Unfortunately, we will lose trivia that was on this node. 348 importWithoutClause = importWithoutClause || importDeclaration; 349 continue; 350 } 351 352 const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; 353 const { name, namedBindings } = importDeclaration.importClause; 354 355 if (name) { 356 group.defaultImports.push(importDeclaration); 357 } 358 359 if (namedBindings) { 360 if (isNamespaceImport(namedBindings)) { 361 group.namespaceImports.push(importDeclaration); 362 } 363 else { 364 group.namedImports.push(importDeclaration); 365 } 366 } 367 } 368 369 return { 370 importWithoutClause, 371 typeOnlyImports, 372 regularImports, 373 }; 374 } 375 376 // Internal for testing 377 /** 378 * @param exportGroup a list of ExportDeclarations, all with the same module name. 379 */ 380 export function coalesceExports(exportGroup: readonly ExportDeclaration[]) { 381 if (exportGroup.length === 0) { 382 return exportGroup; 383 } 384 385 const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); 386 387 const coalescedExports: ExportDeclaration[] = []; 388 389 if (exportWithoutClause) { 390 coalescedExports.push(exportWithoutClause); 391 } 392 393 for (const exportGroup of [namedExports, typeOnlyExports]) { 394 if (exportGroup.length === 0) { 395 continue; 396 } 397 const newExportSpecifiers: ExportSpecifier[] = []; 398 newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); 399 400 const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); 401 402 const exportDecl = exportGroup[0]; 403 coalescedExports.push( 404 factory.updateExportDeclaration( 405 exportDecl, 406 exportDecl.modifiers, 407 exportDecl.isTypeOnly, 408 exportDecl.exportClause && ( 409 isNamedExports(exportDecl.exportClause) ? 410 factory.updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) : 411 factory.updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name) 412 ), 413 exportDecl.moduleSpecifier, 414 exportDecl.assertClause)); 415 } 416 417 return coalescedExports; 418 419 /* 420 * Returns entire export declarations because they may already have been rewritten and 421 * may lack parent pointers. The desired parts can easily be recovered based on the 422 * categorization. 423 */ 424 function getCategorizedExports(exportGroup: readonly ExportDeclaration[]) { 425 let exportWithoutClause: ExportDeclaration | undefined; 426 const namedExports: ExportDeclaration[] = []; 427 const typeOnlyExports: ExportDeclaration[] = []; 428 429 for (const exportDeclaration of exportGroup) { 430 if (exportDeclaration.exportClause === undefined) { 431 // Only the first such export is interesting - the others are redundant. 432 // Note: Unfortunately, we will lose trivia that was on this node. 433 exportWithoutClause = exportWithoutClause || exportDeclaration; 434 } 435 else if (exportDeclaration.isTypeOnly) { 436 typeOnlyExports.push(exportDeclaration); 437 } 438 else { 439 namedExports.push(exportDeclaration); 440 } 441 } 442 443 return { 444 exportWithoutClause, 445 namedExports, 446 typeOnlyExports, 447 }; 448 } 449 } 450 451 function updateImportDeclarationAndClause( 452 importDeclaration: ImportDeclaration, 453 name: Identifier | undefined, 454 namedBindings: NamedImportBindings | undefined) { 455 456 return factory.updateImportDeclaration( 457 importDeclaration, 458 importDeclaration.modifiers, 459 factory.updateImportClause(importDeclaration.importClause!, importDeclaration.importClause!.isTypeOnly, name, namedBindings), // TODO: GH#18217 460 importDeclaration.moduleSpecifier, 461 importDeclaration.assertClause); 462 } 463 464 function sortSpecifiers<T extends ImportOrExportSpecifier>(specifiers: readonly T[]) { 465 return stableSort(specifiers, compareImportOrExportSpecifiers); 466 } 467 468 export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifier>(s1: T, s2: T) { 469 return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) 470 || compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) 471 || compareIdentifiers(s1.name, s2.name); 472 } 473 474 /* internal */ // Exported for testing 475 export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined) { 476 const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); 477 const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); 478 return compareBooleans(name1 === undefined, name2 === undefined) || 479 compareBooleans(isExternalModuleNameRelative(name1!), isExternalModuleNameRelative(name2!)) || 480 compareStringsCaseInsensitive(name1!, name2!); 481 } 482 483 function compareIdentifiers(s1: Identifier, s2: Identifier) { 484 return compareStringsCaseInsensitive(s1.text, s2.text); 485 } 486 487 function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined { 488 switch (declaration.kind) { 489 case SyntaxKind.ImportEqualsDeclaration: 490 return tryCast(declaration.moduleReference, isExternalModuleReference)?.expression; 491 case SyntaxKind.ImportDeclaration: 492 return declaration.moduleSpecifier; 493 case SyntaxKind.VariableStatement: 494 return declaration.declarationList.declarations[0].initializer.arguments[0]; 495 } 496 } 497 498 export function importsAreSorted(imports: readonly AnyImportOrRequireStatement[]): imports is SortedReadonlyArray<AnyImportOrRequireStatement> { 499 return arrayIsSorted(imports, compareImportsOrRequireStatements); 500 } 501 502 export function importSpecifiersAreSorted(imports: readonly ImportSpecifier[]): imports is SortedReadonlyArray<ImportSpecifier> { 503 return arrayIsSorted(imports, compareImportOrExportSpecifiers); 504 } 505 506 export function getImportDeclarationInsertionIndex(sortedImports: SortedReadonlyArray<AnyImportOrRequireStatement>, newImport: AnyImportOrRequireStatement) { 507 const index = binarySearch(sortedImports, newImport, identity, compareImportsOrRequireStatements); 508 return index < 0 ? ~index : index; 509 } 510 511 export function getImportSpecifierInsertionIndex(sortedImports: SortedReadonlyArray<ImportSpecifier>, newImport: ImportSpecifier) { 512 const index = binarySearch(sortedImports, newImport, identity, compareImportOrExportSpecifiers); 513 return index < 0 ? ~index : index; 514 } 515 516 export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { 517 return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2); 518 } 519 520 function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { 521 return compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); 522 } 523 524 // 1. Side-effect imports 525 // 2. Type-only imports 526 // 3. Namespace imports 527 // 4. Default imports 528 // 5. Named imports 529 // 6. ImportEqualsDeclarations 530 // 7. Require variable statements 531 function getImportKindOrder(s1: AnyImportOrRequireStatement) { 532 switch (s1.kind) { 533 case SyntaxKind.ImportDeclaration: 534 if (!s1.importClause) return 0; 535 if (s1.importClause.isTypeOnly) return 1; 536 if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2; 537 if (s1.importClause.name) return 3; 538 return 4; 539 case SyntaxKind.ImportEqualsDeclaration: 540 return 5; 541 case SyntaxKind.VariableStatement: 542 return 6; 543 } 544 } 545 546 function getNewImportSpecifiers(namedImports: ImportDeclaration[]) { 547 return flatMap(namedImports, namedImport => 548 map(tryGetNamedBindingElements(namedImport), importSpecifier => 549 importSpecifier.name && importSpecifier.propertyName && importSpecifier.name.escapedText === importSpecifier.propertyName.escapedText 550 ? factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name) 551 : importSpecifier 552 ) 553 ); 554 } 555 556 function tryGetNamedBindingElements(namedImport: ImportDeclaration) { 557 return namedImport.importClause?.namedBindings && isNamedImports(namedImport.importClause.namedBindings) 558 ? namedImport.importClause.namedBindings.elements 559 : undefined; 560 } 561} 562