Skip to content

Commit e6d7b52

Browse files
authored
Fix multiline import specifier sorting (#51634)
* Fix multiline import specifier sorting * Update baselines * Switch to EmitFlag, set hasTrailingComma on original node array * Update API baseline * Update baselines
1 parent 0c60da9 commit e6d7b52

File tree

9 files changed

+179
-95
lines changed

9 files changed

+179
-95
lines changed

src/compiler/emitter.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4862,7 +4862,14 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
48624862
}
48634863

48644864
function emitList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector<Node>, start?: number, count?: number) {
4865-
emitNodeList(emit, parentNode, children, format, parenthesizerRule, start, count);
4865+
emitNodeList(
4866+
emit,
4867+
parentNode,
4868+
children,
4869+
format | (parentNode && getEmitFlags(parentNode) & EmitFlags.MultiLine ? ListFormat.PreferNewLine : 0),
4870+
parenthesizerRule,
4871+
start,
4872+
count);
48664873
}
48674874

48684875
function emitExpressionList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector<Expression>, start?: number, count?: number) {

src/compiler/types.ts

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7490,7 +7490,7 @@ export interface EmitNode {
74907490
helpers?: EmitHelper[]; // Emit helpers for the node
74917491
startsOnNewLine?: boolean; // If the node should begin on a new line
74927492
snippetElement?: SnippetElement; // Snippet element of the node
7493-
typeNode?: TypeNode; // VariableDeclaration type
7493+
typeNode?: TypeNode; // VariableDeclaration type
74947494
}
74957495

74967496
/** @internal */
@@ -7520,38 +7520,39 @@ export const enum SnippetKind {
75207520
export const enum EmitFlags {
75217521
None = 0,
75227522
SingleLine = 1 << 0, // The contents of this node should be emitted on a single line.
7523-
AdviseOnEmitNode = 1 << 1, // The printer should invoke the onEmitNode callback when printing this node.
7524-
NoSubstitution = 1 << 2, // Disables further substitution of an expression.
7525-
CapturesThis = 1 << 3, // The function captures a lexical `this`
7526-
NoLeadingSourceMap = 1 << 4, // Do not emit a leading source map location for this node.
7527-
NoTrailingSourceMap = 1 << 5, // Do not emit a trailing source map location for this node.
7523+
MultiLine = 1 << 1,
7524+
AdviseOnEmitNode = 1 << 2, // The printer should invoke the onEmitNode callback when printing this node.
7525+
NoSubstitution = 1 << 3, // Disables further substitution of an expression.
7526+
CapturesThis = 1 << 4, // The function captures a lexical `this`
7527+
NoLeadingSourceMap = 1 << 5, // Do not emit a leading source map location for this node.
7528+
NoTrailingSourceMap = 1 << 6, // Do not emit a trailing source map location for this node.
75287529
NoSourceMap = NoLeadingSourceMap | NoTrailingSourceMap, // Do not emit a source map location for this node.
7529-
NoNestedSourceMaps = 1 << 6, // Do not emit source map locations for children of this node.
7530-
NoTokenLeadingSourceMaps = 1 << 7, // Do not emit leading source map location for token nodes.
7531-
NoTokenTrailingSourceMaps = 1 << 8, // Do not emit trailing source map location for token nodes.
7530+
NoNestedSourceMaps = 1 << 7, // Do not emit source map locations for children of this node.
7531+
NoTokenLeadingSourceMaps = 1 << 8, // Do not emit leading source map location for token nodes.
7532+
NoTokenTrailingSourceMaps = 1 << 9, // Do not emit trailing source map location for token nodes.
75327533
NoTokenSourceMaps = NoTokenLeadingSourceMaps | NoTokenTrailingSourceMaps, // Do not emit source map locations for tokens of this node.
7533-
NoLeadingComments = 1 << 9, // Do not emit leading comments for this node.
7534-
NoTrailingComments = 1 << 10, // Do not emit trailing comments for this node.
7534+
NoLeadingComments = 1 << 10, // Do not emit leading comments for this node.
7535+
NoTrailingComments = 1 << 11, // Do not emit trailing comments for this node.
75357536
NoComments = NoLeadingComments | NoTrailingComments, // Do not emit comments for this node.
7536-
NoNestedComments = 1 << 11,
7537-
HelperName = 1 << 12, // The Identifier refers to an *unscoped* emit helper (one that is emitted at the top of the file)
7538-
ExportName = 1 << 13, // Ensure an export prefix is added for an identifier that points to an exported declaration with a local name (see SymbolFlags.ExportHasLocal).
7539-
LocalName = 1 << 14, // Ensure an export prefix is not added for an identifier that points to an exported declaration.
7540-
InternalName = 1 << 15, // The name is internal to an ES5 class body function.
7541-
Indented = 1 << 16, // Adds an explicit extra indentation level for class and function bodies when printing (used to match old emitter).
7542-
NoIndentation = 1 << 17, // Do not indent the node.
7543-
AsyncFunctionBody = 1 << 18,
7544-
ReuseTempVariableScope = 1 << 19, // Reuse the existing temp variable scope during emit.
7545-
CustomPrologue = 1 << 20, // Treat the statement as if it were a prologue directive (NOTE: Prologue directives are *not* transformed).
7546-
NoHoisting = 1 << 21, // Do not hoist this declaration in --module system
7547-
HasEndOfDeclarationMarker = 1 << 22, // Declaration has an associated NotEmittedStatement to mark the end of the declaration
7548-
Iterator = 1 << 23, // The expression to a `yield*` should be treated as an Iterator when down-leveling, not an Iterable.
7549-
NoAsciiEscaping = 1 << 24, // When synthesizing nodes that lack an original node or textSourceNode, we want to write the text on the node with ASCII escaping substitutions.
7550-
/** @internal */ TypeScriptClassWrapper = 1 << 25, // The node is an IIFE class wrapper created by the ts transform.
7551-
/** @internal */ NeverApplyImportHelper = 1 << 26, // Indicates the node should never be wrapped with an import star helper (because, for example, it imports tslib itself)
7552-
/** @internal */ IgnoreSourceNewlines = 1 << 27, // Overrides `printerOptions.preserveSourceNewlines` to print this node (and all descendants) with default whitespace.
7553-
/** @internal */ Immutable = 1 << 28, // Indicates a node is a singleton intended to be reused in multiple locations. Any attempt to make further changes to the node will result in an error.
7554-
/** @internal */ IndirectCall = 1 << 29, // Emit CallExpression as an indirect call: `(0, f)()`
7537+
NoNestedComments = 1 << 12,
7538+
HelperName = 1 << 13, // The Identifier refers to an *unscoped* emit helper (one that is emitted at the top of the file)
7539+
ExportName = 1 << 14, // Ensure an export prefix is added for an identifier that points to an exported declaration with a local name (see SymbolFlags.ExportHasLocal).
7540+
LocalName = 1 << 15, // Ensure an export prefix is not added for an identifier that points to an exported declaration.
7541+
InternalName = 1 << 16, // The name is internal to an ES5 class body function.
7542+
Indented = 1 << 17, // Adds an explicit extra indentation level for class and function bodies when printing (used to match old emitter).
7543+
NoIndentation = 1 << 18, // Do not indent the node.
7544+
AsyncFunctionBody = 1 << 19,
7545+
ReuseTempVariableScope = 1 << 20, // Reuse the existing temp variable scope during emit.
7546+
CustomPrologue = 1 << 21, // Treat the statement as if it were a prologue directive (NOTE: Prologue directives are *not* transformed).
7547+
NoHoisting = 1 << 22, // Do not hoist this declaration in --module system
7548+
HasEndOfDeclarationMarker = 1 << 23, // Declaration has an associated NotEmittedStatement to mark the end of the declaration
7549+
Iterator = 1 << 24, // The expression to a `yield*` should be treated as an Iterator when down-leveling, not an Iterable.
7550+
NoAsciiEscaping = 1 << 25, // When synthesizing nodes that lack an original node or textSourceNode, we want to write the text on the node with ASCII escaping substitutions.
7551+
/** @internal */ TypeScriptClassWrapper = 1 << 26, // The node is an IIFE class wrapper created by the ts transform.
7552+
/** @internal */ NeverApplyImportHelper = 1 << 27, // Indicates the node should never be wrapped with an import star helper (because, for example, it imports tslib itself)
7553+
/** @internal */ IgnoreSourceNewlines = 1 << 28, // Overrides `printerOptions.preserveSourceNewlines` to print this node (and all descendants) with default whitespace.
7554+
/** @internal */ Immutable = 1 << 29, // Indicates a node is a singleton intended to be reused in multiple locations. Any attempt to make further changes to the node will result in an error.
7555+
/** @internal */ IndirectCall = 1 << 30, // Emit CallExpression as an indirect call: `(0, f)()`
75557556
}
75567557

75577558
export interface EmitHelperBase {

src/services/organizeImports.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
compareValues,
88
Comparison,
99
createScanner,
10+
EmitFlags,
1011
emptyArray,
1112
ExportDeclaration,
1213
ExportSpecifier,
@@ -43,7 +44,9 @@ import {
4344
NamespaceImport,
4445
OrganizeImportsMode,
4546
Program,
47+
rangeIsOnSingleLine,
4648
Scanner,
49+
setEmitFlags,
4750
some,
4851
SortedReadonlyArray,
4952
SourceFile,
@@ -79,7 +82,7 @@ export function organizeImports(
7982
const maybeRemove = shouldRemove ? removeUnusedImports : identity;
8083
const maybeCoalesce = shouldCombine ? coalesceImports : identity;
8184
const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => {
82-
const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program));
85+
const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), sourceFile);
8386
return shouldSort
8487
? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2))
8588
: processedDeclarations;
@@ -296,7 +299,7 @@ function getExternalModuleName(specifier: Expression) {
296299
*
297300
* @internal
298301
*/
299-
export function coalesceImports(importGroup: readonly ImportDeclaration[]) {
302+
export function coalesceImports(importGroup: readonly ImportDeclaration[], sourceFile?: SourceFile) {
300303
if (importGroup.length === 0) {
301304
return importGroup;
302305
}
@@ -350,7 +353,11 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[]) {
350353

351354
newImportSpecifiers.push(...getNewImportSpecifiers(namedImports));
352355

353-
const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers);
356+
const sortedImportSpecifiers = factory.createNodeArray(
357+
sortSpecifiers(newImportSpecifiers),
358+
(namedImports[0]?.importClause!.namedBindings as NamedImports)?.elements.hasTrailingComma
359+
);
360+
354361
const importDecl = defaultImports.length > 0
355362
? defaultImports[0]
356363
: namedImports[0];
@@ -363,6 +370,14 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[]) {
363370
? factory.createNamedImports(sortedImportSpecifiers)
364371
: factory.updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217
365372

373+
if (sourceFile &&
374+
newNamedImports &&
375+
namedImports[0]?.importClause!.namedBindings &&
376+
!rangeIsOnSingleLine(namedImports[0].importClause.namedBindings, sourceFile)
377+
) {
378+
setEmitFlags(newNamedImports, EmitFlags.MultiLine);
379+
}
380+
366381
// Type-only imports are not allowed to mix default, namespace, and named imports in any combination.
367382
// We could rewrite a default import as a named import (`import { default as name }`), but we currently
368383
// choose not to as a stylistic preference.

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7367,33 +7367,34 @@ declare namespace ts {
73677367
enum EmitFlags {
73687368
None = 0,
73697369
SingleLine = 1,
7370-
AdviseOnEmitNode = 2,
7371-
NoSubstitution = 4,
7372-
CapturesThis = 8,
7373-
NoLeadingSourceMap = 16,
7374-
NoTrailingSourceMap = 32,
7375-
NoSourceMap = 48,
7376-
NoNestedSourceMaps = 64,
7377-
NoTokenLeadingSourceMaps = 128,
7378-
NoTokenTrailingSourceMaps = 256,
7379-
NoTokenSourceMaps = 384,
7380-
NoLeadingComments = 512,
7381-
NoTrailingComments = 1024,
7382-
NoComments = 1536,
7383-
NoNestedComments = 2048,
7384-
HelperName = 4096,
7385-
ExportName = 8192,
7386-
LocalName = 16384,
7387-
InternalName = 32768,
7388-
Indented = 65536,
7389-
NoIndentation = 131072,
7390-
AsyncFunctionBody = 262144,
7391-
ReuseTempVariableScope = 524288,
7392-
CustomPrologue = 1048576,
7393-
NoHoisting = 2097152,
7394-
HasEndOfDeclarationMarker = 4194304,
7395-
Iterator = 8388608,
7396-
NoAsciiEscaping = 16777216
7370+
MultiLine = 2,
7371+
AdviseOnEmitNode = 4,
7372+
NoSubstitution = 8,
7373+
CapturesThis = 16,
7374+
NoLeadingSourceMap = 32,
7375+
NoTrailingSourceMap = 64,
7376+
NoSourceMap = 96,
7377+
NoNestedSourceMaps = 128,
7378+
NoTokenLeadingSourceMaps = 256,
7379+
NoTokenTrailingSourceMaps = 512,
7380+
NoTokenSourceMaps = 768,
7381+
NoLeadingComments = 1024,
7382+
NoTrailingComments = 2048,
7383+
NoComments = 3072,
7384+
NoNestedComments = 4096,
7385+
HelperName = 8192,
7386+
ExportName = 16384,
7387+
LocalName = 32768,
7388+
InternalName = 65536,
7389+
Indented = 131072,
7390+
NoIndentation = 262144,
7391+
AsyncFunctionBody = 524288,
7392+
ReuseTempVariableScope = 1048576,
7393+
CustomPrologue = 2097152,
7394+
NoHoisting = 4194304,
7395+
HasEndOfDeclarationMarker = 8388608,
7396+
Iterator = 16777216,
7397+
NoAsciiEscaping = 33554432
73977398
}
73987399
interface EmitHelperBase {
73997400
readonly name: string;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3431,33 +3431,34 @@ declare namespace ts {
34313431
enum EmitFlags {
34323432
None = 0,
34333433
SingleLine = 1,
3434-
AdviseOnEmitNode = 2,
3435-
NoSubstitution = 4,
3436-
CapturesThis = 8,
3437-
NoLeadingSourceMap = 16,
3438-
NoTrailingSourceMap = 32,
3439-
NoSourceMap = 48,
3440-
NoNestedSourceMaps = 64,
3441-
NoTokenLeadingSourceMaps = 128,
3442-
NoTokenTrailingSourceMaps = 256,
3443-
NoTokenSourceMaps = 384,
3444-
NoLeadingComments = 512,
3445-
NoTrailingComments = 1024,
3446-
NoComments = 1536,
3447-
NoNestedComments = 2048,
3448-
HelperName = 4096,
3449-
ExportName = 8192,
3450-
LocalName = 16384,
3451-
InternalName = 32768,
3452-
Indented = 65536,
3453-
NoIndentation = 131072,
3454-
AsyncFunctionBody = 262144,
3455-
ReuseTempVariableScope = 524288,
3456-
CustomPrologue = 1048576,
3457-
NoHoisting = 2097152,
3458-
HasEndOfDeclarationMarker = 4194304,
3459-
Iterator = 8388608,
3460-
NoAsciiEscaping = 16777216
3434+
MultiLine = 2,
3435+
AdviseOnEmitNode = 4,
3436+
NoSubstitution = 8,
3437+
CapturesThis = 16,
3438+
NoLeadingSourceMap = 32,
3439+
NoTrailingSourceMap = 64,
3440+
NoSourceMap = 96,
3441+
NoNestedSourceMaps = 128,
3442+
NoTokenLeadingSourceMaps = 256,
3443+
NoTokenTrailingSourceMaps = 512,
3444+
NoTokenSourceMaps = 768,
3445+
NoLeadingComments = 1024,
3446+
NoTrailingComments = 2048,
3447+
NoComments = 3072,
3448+
NoNestedComments = 4096,
3449+
HelperName = 8192,
3450+
ExportName = 16384,
3451+
LocalName = 32768,
3452+
InternalName = 65536,
3453+
Indented = 131072,
3454+
NoIndentation = 262144,
3455+
AsyncFunctionBody = 524288,
3456+
ReuseTempVariableScope = 1048576,
3457+
CustomPrologue = 2097152,
3458+
NoHoisting = 4194304,
3459+
HasEndOfDeclarationMarker = 8388608,
3460+
Iterator = 16777216,
3461+
NoAsciiEscaping = 33554432
34613462
}
34623463
interface EmitHelperBase {
34633464
readonly name: string;

tests/cases/fourslash/organizeImports1.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@
2020

2121
verify.organizeImports(
2222
`import {
23-
a, b,
24-
b as B, c,
25-
c as C, d, d as D, e, f,
26-
f as F, g,
27-
g as G, h, h as H
23+
a,
24+
b,
25+
b as B,
26+
c,
27+
c as C,
28+
d, d as D,
29+
e,
30+
f,
31+
f as F,
32+
g,
33+
g as G,
34+
h, h as H
2835
} from './foo';
2936
3037
console.log(a, B, b, c, C, d, D);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////import {
4+
//// Type1,
5+
//// Type2,
6+
//// func4,
7+
//// Type3,
8+
//// Type4,
9+
//// Type5,
10+
//// Type7,
11+
//// Type8,
12+
//// Type9,
13+
//// func1,
14+
//// func2,
15+
//// Type6,
16+
//// func3,
17+
//// func5,
18+
//// func6,
19+
//// func7,
20+
//// func8,
21+
//// func9,
22+
////} from "foo";
23+
////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {}
24+
////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);
25+
26+
verify.organizeImports(
27+
`import {
28+
func1,
29+
func2,
30+
func3,
31+
func4,
32+
func5,
33+
func6,
34+
func7,
35+
func8,
36+
func9,
37+
Type1,
38+
Type2,
39+
Type3,
40+
Type4,
41+
Type5,
42+
Type6,
43+
Type7,
44+
Type8,
45+
Type9,
46+
} from "foo";
47+
interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {}
48+
console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);`
49+
);

tests/cases/fourslash/organizeImports2.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
//// console.log(Foo, Bar);
1111

1212
verify.organizeImports(
13-
`import { Bar, Foo } from "foo";
13+
`import {
14+
Bar,
15+
Foo
16+
} from "foo";
1417
1518
console.log(Foo, Bar);`
1619
);

tests/cases/fourslash/organizeImportsNoFormatOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ format.setFormatOptions({});
1616
// Default newline is carriage return.
1717
// See `getNewLineOrDefaultFromHost()` in services/utilities.ts.
1818
verify.organizeImports(
19-
`import {\r\nstat,\r\nstatSync\r\n} from "fs";\r\nexport function fakeFn() {
19+
`import {\r\nstat,\r\nstatSync,\r\n} from "fs";\r\nexport function fakeFn() {
2020
stat;
2121
statSync;
2222
}`);

0 commit comments

Comments
 (0)