Skip to content

Commit ee2090d

Browse files
authored
fix 57215 -- add support for import attributes to OrganizeImports (#57250)
1 parent f1c841b commit ee2090d

File tree

5 files changed

+175
-78
lines changed

5 files changed

+175
-78
lines changed

src/services/organizeImports.ts

Lines changed: 94 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
Scanner,
6060
setEmitFlags,
6161
some,
62+
sort,
6263
SortKind,
6364
SourceFile,
6465
stableSort,
@@ -322,96 +323,111 @@ function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], compar
322323
return importGroup;
323324
}
324325

325-
const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup);
326-
const coalescedImports: ImportDeclaration[] = [];
327-
328-
if (importWithoutClause) {
329-
coalescedImports.push(importWithoutClause);
330-
}
331-
332-
for (const group of [regularImports, typeOnlyImports]) {
333-
const isTypeOnly = group === typeOnlyImports;
334-
const { defaultImports, namespaceImports, namedImports } = group;
335-
// Normally, we don't combine default and namespace imports, but it would be silly to
336-
// produce two import declarations in this special case.
337-
if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
338-
// Add the namespace import to the existing default ImportDeclaration.
339-
const defaultImport = defaultImports[0];
340-
coalescedImports.push(
341-
updateImportDeclarationAndClause(defaultImport, defaultImport.importClause.name, namespaceImports[0].importClause.namedBindings),
342-
);
343-
344-
continue;
326+
const importGroupsByAttributes = groupBy(importGroup, decl => {
327+
if (decl.attributes) {
328+
let attrs = decl.attributes.token + " ";
329+
for (const x of sort(decl.attributes.elements, (x, y) => compareStringsCaseSensitive(x.name.text, y.name.text))) {
330+
attrs += x.name.text + ":";
331+
attrs += isStringLiteralLike(x.value) ? `"${x.value.text}"` : x.value.getText() + " ";
332+
}
333+
return attrs;
345334
}
335+
return "";
336+
});
346337

347-
const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => comparer(i1.importClause.namedBindings.name.text, i2.importClause.namedBindings.name.text));
338+
const coalescedImports: ImportDeclaration[] = [];
339+
for (const attribute in importGroupsByAttributes) {
340+
const importGroupSameAttrs = importGroupsByAttributes[attribute] as ImportDeclaration[];
341+
const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroupSameAttrs);
342+
343+
if (importWithoutClause) {
344+
coalescedImports.push(importWithoutClause);
345+
}
346+
347+
for (const group of [regularImports, typeOnlyImports]) {
348+
const isTypeOnly = group === typeOnlyImports;
349+
const { defaultImports, namespaceImports, namedImports } = group;
350+
// Normally, we don't combine default and namespace imports, but it would be silly to
351+
// produce two import declarations in this special case.
352+
if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
353+
// Add the namespace import to the existing default ImportDeclaration.
354+
const defaultImport = defaultImports[0];
355+
coalescedImports.push(
356+
updateImportDeclarationAndClause(defaultImport, defaultImport.importClause.name, namespaceImports[0].importClause.namedBindings),
357+
);
348358

349-
for (const namespaceImport of sortedNamespaceImports) {
350-
// Drop the name, if any
351-
coalescedImports.push(
352-
updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause.namedBindings),
353-
);
354-
}
359+
continue;
360+
}
355361

356-
const firstDefaultImport = firstOrUndefined(defaultImports);
357-
const firstNamedImport = firstOrUndefined(namedImports);
358-
const importDecl = firstDefaultImport ?? firstNamedImport;
359-
if (!importDecl) {
360-
continue;
361-
}
362+
const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => comparer(i1.importClause.namedBindings.name.text, i2.importClause.namedBindings.name.text));
362363

363-
let newDefaultImport: Identifier | undefined;
364-
const newImportSpecifiers: ImportSpecifier[] = [];
365-
if (defaultImports.length === 1) {
366-
newDefaultImport = defaultImports[0].importClause.name;
367-
}
368-
else {
369-
for (const defaultImport of defaultImports) {
370-
newImportSpecifiers.push(
371-
factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause.name),
364+
for (const namespaceImport of sortedNamespaceImports) {
365+
// Drop the name, if any
366+
coalescedImports.push(
367+
updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause.namedBindings),
372368
);
373369
}
374-
}
375370

376-
newImportSpecifiers.push(...getNewImportSpecifiers(namedImports));
371+
const firstDefaultImport = firstOrUndefined(defaultImports);
372+
const firstNamedImport = firstOrUndefined(namedImports);
373+
const importDecl = firstDefaultImport ?? firstNamedImport;
374+
if (!importDecl) {
375+
continue;
376+
}
377377

378-
const sortedImportSpecifiers = factory.createNodeArray(
379-
sortSpecifiers(newImportSpecifiers, comparer, preferences),
380-
firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma,
381-
);
378+
let newDefaultImport: Identifier | undefined;
379+
const newImportSpecifiers: ImportSpecifier[] = [];
380+
if (defaultImports.length === 1) {
381+
newDefaultImport = defaultImports[0].importClause.name;
382+
}
383+
else {
384+
for (const defaultImport of defaultImports) {
385+
newImportSpecifiers.push(
386+
factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause.name),
387+
);
388+
}
389+
}
382390

383-
const newNamedImports = sortedImportSpecifiers.length === 0
384-
? newDefaultImport
385-
? undefined
386-
: factory.createNamedImports(emptyArray)
387-
: firstNamedImport
388-
? factory.updateNamedImports(firstNamedImport.importClause.namedBindings, sortedImportSpecifiers)
389-
: factory.createNamedImports(sortedImportSpecifiers);
390-
391-
if (
392-
sourceFile &&
393-
newNamedImports &&
394-
firstNamedImport?.importClause.namedBindings &&
395-
!rangeIsOnSingleLine(firstNamedImport.importClause.namedBindings, sourceFile)
396-
) {
397-
setEmitFlags(newNamedImports, EmitFlags.MultiLine);
398-
}
391+
newImportSpecifiers.push(...getNewImportSpecifiers(namedImports));
399392

400-
// Type-only imports are not allowed to mix default, namespace, and named imports in any combination.
401-
// We could rewrite a default import as a named import (`import { default as name }`), but we currently
402-
// choose not to as a stylistic preference.
403-
if (isTypeOnly && newDefaultImport && newNamedImports) {
404-
coalescedImports.push(
405-
updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined),
406-
);
407-
coalescedImports.push(
408-
updateImportDeclarationAndClause(firstNamedImport ?? importDecl, /*name*/ undefined, newNamedImports),
409-
);
410-
}
411-
else {
412-
coalescedImports.push(
413-
updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports),
393+
const sortedImportSpecifiers = factory.createNodeArray(
394+
sortSpecifiers(newImportSpecifiers, comparer, preferences),
395+
firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma,
414396
);
397+
398+
const newNamedImports = sortedImportSpecifiers.length === 0
399+
? newDefaultImport
400+
? undefined
401+
: factory.createNamedImports(emptyArray)
402+
: firstNamedImport
403+
? factory.updateNamedImports(firstNamedImport.importClause.namedBindings, sortedImportSpecifiers)
404+
: factory.createNamedImports(sortedImportSpecifiers);
405+
406+
if (
407+
sourceFile &&
408+
newNamedImports &&
409+
firstNamedImport?.importClause.namedBindings &&
410+
!rangeIsOnSingleLine(firstNamedImport.importClause.namedBindings, sourceFile)
411+
) {
412+
setEmitFlags(newNamedImports, EmitFlags.MultiLine);
413+
}
414+
415+
// Type-only imports are not allowed to mix default, namespace, and named imports in any combination.
416+
// We could rewrite a default import as a named import (`import { default as name }`), but we currently
417+
// choose not to as a stylistic preference.
418+
if (isTypeOnly && newDefaultImport && newNamedImports) {
419+
coalescedImports.push(
420+
updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined),
421+
);
422+
coalescedImports.push(
423+
updateImportDeclarationAndClause(firstNamedImport ?? importDecl, /*name*/ undefined, newNamedImports),
424+
);
425+
}
426+
else {
427+
coalescedImports.push(
428+
updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports),
429+
);
430+
}
415431
}
416432
}
417433

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// import { A } from "./file";
4+
//// import { type B } from "./file";
5+
//// import { C } from "./file" assert { type: "a" };
6+
//// import { A as D } from "./file" assert { type: "b" };
7+
//// import { E } from "./file" with { type: "a" };
8+
//// import { A as F } from "./file" with { type: "b" };
9+
////
10+
//// type G = A | B | C | D | E | F;
11+
12+
verify.organizeImports(
13+
`import { A, type B } from "./file";
14+
import { C } from "./file" assert { type: "a" };
15+
import { A as D } from "./file" assert { type: "b" };
16+
import { E } from "./file" with { type: "a" };
17+
import { A as F } from "./file" with { type: "b" };
18+
19+
type G = A | B | C | D | E | F;`);
20+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////import { A } from "./a";
4+
////import { C } from "./a" assert { type: "a" };
5+
////import { Z } from "./z";
6+
////import { A as D } from "./a" assert { type: "b" };
7+
////import { E } from "./a" with { type: "a" };
8+
////import { F } from "./a" assert { type: "a" };
9+
////import { B } from "./a";
10+
////
11+
////export type G = A | B | C | D | E | F | Z;
12+
13+
verify.organizeImports(
14+
`import { A, B } from "./a";
15+
import { C, F } from "./a" assert { type: "a" };
16+
import { A as D } from "./a" assert { type: "b" };
17+
import { E } from "./a" with { type: "a" };
18+
import { Z } from "./z";
19+
20+
export type G = A | B | C | D | E | F | Z;`);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////import { A } from "./a";
4+
////import { C } from "./a" assert { type: "a" };
5+
////import { Z } from "./z";
6+
////import { A as D } from "./a" assert { type: "b" };
7+
////import { E } from "./a" assert { type: /* comment*/ "a" };
8+
////import { F } from "./a" assert {type: "a" };
9+
////import { Y } from "./a" assert{ type: "b" /* comment*/};
10+
////import { B } from "./a";
11+
////
12+
////export type G = A | B | C | D | E | F | Y | Z;
13+
14+
verify.organizeImports(
15+
`import { A, B } from "./a";
16+
import { C, E, F } from "./a" assert { type: "a" };
17+
import { A as D, Y } from "./a" assert { type: "b" };
18+
import { Z } from "./z";
19+
20+
export type G = A | B | C | D | E | F | Y | Z;`);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////import { A } from "./a" assert { foo: "foo", bar: "bar" };
4+
////import { B } from "./a" assert { bar: "bar", foo: "foo" };
5+
////import { D } from "./a" assert { bar: "foo", foo: "bar" };
6+
////import { E } from "./a" assert { foo: 'bar', bar: "foo" };
7+
////import { C } from "./a" assert { foo: "bar", bar: "foo" };
8+
////import { F } from "./a" assert { foo: "42" };
9+
////import { Y } from "./a" assert { foo: 42 };
10+
////import { Z } from "./a" assert { foo: "42" };
11+
////
12+
////export type G = A | B | C | D | E | F | Y | Z;
13+
14+
15+
verify.organizeImports(
16+
`import { A, B } from "./a" assert { foo: "foo", bar: "bar" };
17+
import { C, D, E } from "./a" assert { bar: "foo", foo: "bar" };
18+
import { F, Z } from "./a" assert { foo: "42" };
19+
import { Y } from "./a" assert { foo: 42 };
20+
21+
export type G = A | B | C | D | E | F | Y | Z;`);

0 commit comments

Comments
 (0)