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