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