• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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