Skip to content

Commit 6b402b3

Browse files
authored
feat: ability to sort type only named imports and exports first or last (#664)
1 parent 66e8247 commit 6b402b3

14 files changed

+286
-28
lines changed

deployment/schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,21 @@
297297
"description": "Force multiple lines only if importing more than one thing."
298298
}]
299299
},
300+
"typeOnlyImportsExportsSortOrder": {
301+
"description": "The kind of sort ordering to use for typed imports and exports.",
302+
"type": "string",
303+
"default": "last",
304+
"oneOf": [{
305+
"const": "first",
306+
"description": "Puts type-only named imports and exports first."
307+
}, {
308+
"const": "last",
309+
"description": "Puts type-only named imports and exports last."
310+
}, {
311+
"const": "none",
312+
"description": "Does not sort based on if a type-only named import or export."
313+
}]
314+
},
300315
"sortOrder": {
301316
"description": "The kind of sort ordering to use.",
302317
"type": "string",
@@ -1000,9 +1015,15 @@
10001015
"exportDeclaration.sortNamedExports": {
10011016
"$ref": "#/definitions/sortOrder"
10021017
},
1018+
"exportDeclaration.sortTypeOnlyExports": {
1019+
"$ref": "#/definitions/typeOnlyImportsExportsSortOrder"
1020+
},
10031021
"importDeclaration.sortNamedImports": {
10041022
"$ref": "#/definitions/sortOrder"
10051023
},
1024+
"importDeclaration.sortTypeOnlyImports": {
1025+
"$ref": "#/definitions/typeOnlyImportsExportsSortOrder"
1026+
},
10061027
"ignoreNodeCommentText": {
10071028
"description": "The text to use for an ignore comment (ex. `// dprint-ignore`).",
10081029
"default": "dprint-ignore",

src/configuration/builder.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,13 +536,27 @@ impl ConfigurationBuilder {
536536
self.insert("importDeclaration.sortNamedImports", value.to_string().into())
537537
}
538538

539+
/// Sorts type-only named imports first, last, or none (no sorting).
540+
///
541+
/// Default: Last
542+
pub fn import_declaration_sort_type_only_imports(&mut self, value: NamedTypeImportsExportsOrder) -> &mut Self {
543+
self.insert("importDeclaration.sortTypeOnlyImports", value.to_string().into())
544+
}
545+
539546
/// Alphabetically sorts the export declaration's named exports.
540547
///
541548
/// Default: Case insensitive
542549
pub fn export_declaration_sort_named_exports(&mut self, value: SortOrder) -> &mut Self {
543550
self.insert("exportDeclaration.sortNamedExports", value.to_string().into())
544551
}
545552

553+
/// Sorts type-only named exports first, last, or none (no sorting).
554+
///
555+
/// Default: Last
556+
pub fn export_declaration_sort_type_only_exports(&mut self, value: NamedTypeImportsExportsOrder) -> &mut Self {
557+
self.insert("exportDeclaration.sortTypeOnlyExports", value.to_string().into())
558+
}
559+
546560
/* ignore comments */
547561

548562
/// The text to use for an ignore comment (ex. `// dprint-ignore`).
@@ -1105,6 +1119,8 @@ mod tests {
11051119
.module_sort_export_declarations(SortOrder::Maintain)
11061120
.import_declaration_sort_named_imports(SortOrder::Maintain)
11071121
.export_declaration_sort_named_exports(SortOrder::Maintain)
1122+
.import_declaration_sort_type_only_imports(NamedTypeImportsExportsOrder::First)
1123+
.export_declaration_sort_type_only_exports(NamedTypeImportsExportsOrder::None)
11081124
/* ignore comments */
11091125
.ignore_node_comment_text("ignore")
11101126
.ignore_file_comment_text("ignore-file")
@@ -1269,7 +1285,7 @@ mod tests {
12691285
.while_statement_space_around(true);
12701286

12711287
let inner_config = config.get_inner_config();
1272-
assert_eq!(inner_config.len(), 179);
1288+
assert_eq!(inner_config.len(), 181);
12731289
let diagnostics = resolve_config(inner_config, &Default::default()).diagnostics;
12741290
assert_eq!(diagnostics.len(), 0);
12751291
}

src/configuration/resolve_config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
119119
module_sort_export_declarations: get_value(&mut config, "module.sortExportDeclarations", SortOrder::CaseInsensitive, &mut diagnostics),
120120
import_declaration_sort_named_imports: get_value(&mut config, "importDeclaration.sortNamedImports", SortOrder::CaseInsensitive, &mut diagnostics),
121121
export_declaration_sort_named_exports: get_value(&mut config, "exportDeclaration.sortNamedExports", SortOrder::CaseInsensitive, &mut diagnostics),
122+
import_declaration_sort_type_only_imports: get_value(
123+
&mut config,
124+
"importDeclaration.sortTypeOnlyImports",
125+
NamedTypeImportsExportsOrder::Last,
126+
&mut diagnostics,
127+
),
128+
export_declaration_sort_type_only_exports: get_value(
129+
&mut config,
130+
"exportDeclaration.sortTypeOnlyExports",
131+
NamedTypeImportsExportsOrder::Last,
132+
&mut diagnostics,
133+
),
122134
/* ignore comments */
123135
ignore_node_comment_text: get_value(&mut config, "ignoreNodeCommentText", String::from("dprint-ignore"), &mut diagnostics),
124136
ignore_file_comment_text: get_value(&mut config, "ignoreFileCommentText", String::from("dprint-ignore-file"), &mut diagnostics),

src/configuration/types.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@ generate_str_to_from![
296296
[CaseInsensitive, "caseInsensitive"]
297297
];
298298

299+
#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Default)]
300+
#[serde(rename_all = "camelCase")]
301+
pub enum NamedTypeImportsExportsOrder {
302+
First,
303+
#[default]
304+
Last,
305+
None,
306+
}
307+
308+
generate_str_to_from![NamedTypeImportsExportsOrder, [First, "first"], [Last, "last"], [None, "none"]];
309+
299310
#[derive(Clone, Serialize, Deserialize)]
300311
#[serde(rename_all = "camelCase")]
301312
pub struct Configuration {
@@ -336,8 +347,12 @@ pub struct Configuration {
336347
pub module_sort_export_declarations: SortOrder,
337348
#[serde(rename = "importDeclaration.sortNamedImports")]
338349
pub import_declaration_sort_named_imports: SortOrder,
350+
#[serde(rename = "importDeclaration.sortTypeOnlyImports")]
351+
pub import_declaration_sort_type_only_imports: NamedTypeImportsExportsOrder,
339352
#[serde(rename = "exportDeclaration.sortNamedExports")]
340353
pub export_declaration_sort_named_exports: SortOrder,
354+
#[serde(rename = "exportDeclaration.sortTypeOnlyExports")]
355+
pub export_declaration_sort_type_only_exports: NamedTypeImportsExportsOrder,
341356
/* ignore comments */
342357
pub ignore_node_comment_text: String,
343358
pub ignore_file_comment_text: String,

src/generation/generate.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,8 +1544,14 @@ fn gen_named_import_or_export_specifiers<'a>(opts: GenNamedImportOrExportSpecifi
15441544
context: &Context<'a>,
15451545
) -> Option<Box<dyn Fn((usize, Option<Node<'a>>), (usize, Option<Node<'a>>), Program<'a>) -> std::cmp::Ordering>> {
15461546
match parent_decl {
1547-
Node::NamedExport(_) => get_node_sorter_from_order(context.config.export_declaration_sort_named_exports),
1548-
Node::ImportDecl(_) => get_node_sorter_from_order(context.config.import_declaration_sort_named_imports),
1547+
Node::NamedExport(_) => get_node_sorter_from_order(
1548+
context.config.export_declaration_sort_named_exports,
1549+
context.config.export_declaration_sort_type_only_exports,
1550+
),
1551+
Node::ImportDecl(_) => get_node_sorter_from_order(
1552+
context.config.import_declaration_sort_named_imports,
1553+
context.config.import_declaration_sort_type_only_imports,
1554+
),
15491555
_ => unreachable!(),
15501556
}
15511557
}
@@ -7104,8 +7110,8 @@ fn gen_statements<'a>(inner_range: SourceRange, stmts: Vec<Node<'a>>, context: &
71047110
context: &Context<'a>,
71057111
) -> Option<Box<dyn Fn((usize, Option<Node<'a>>), (usize, Option<Node<'a>>), Program<'a>) -> std::cmp::Ordering>> {
71067112
match group_kind {
7107-
StmtGroupKind::Imports => get_node_sorter_from_order(context.config.module_sort_import_declarations),
7108-
StmtGroupKind::Exports => get_node_sorter_from_order(context.config.module_sort_export_declarations),
7113+
StmtGroupKind::Imports => get_node_sorter_from_order(context.config.module_sort_import_declarations, NamedTypeImportsExportsOrder::None),
7114+
StmtGroupKind::Exports => get_node_sorter_from_order(context.config.module_sort_export_declarations, NamedTypeImportsExportsOrder::None),
71097115
StmtGroupKind::Other => None,
71107116
}
71117117
}

src/generation/sorting/mod.rs

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ use std::cmp::Ordering;
88

99
use crate::configuration::*;
1010

11-
// a little rough, but good enough for our purposes
11+
// very rough... this should be improved to not allocate so much
12+
// and be cleaner
1213

13-
pub fn get_node_sorter_from_order<'a>(order: SortOrder) -> Option<Box<dyn Fn((usize, Option<Node<'a>>), (usize, Option<Node<'a>>), Program<'a>) -> Ordering>> {
14+
pub fn get_node_sorter_from_order<'a>(
15+
order: SortOrder,
16+
named_type_imports_exports_order: NamedTypeImportsExportsOrder,
17+
) -> Option<Box<dyn Fn((usize, Option<Node<'a>>), (usize, Option<Node<'a>>), Program<'a>) -> Ordering>> {
1418
// todo: how to reduce code duplication here?
1519
match order {
1620
SortOrder::Maintain => None,
17-
SortOrder::CaseInsensitive => Some(Box::new(|(a_index, a), (b_index, b), program| {
21+
SortOrder::CaseInsensitive => Some(Box::new(move |(a_index, a), (b_index, b), program| {
1822
let result = if is_import_or_export_declaration(&a) {
19-
cmp_optional_nodes(a, b, program, |a, b, module| {
23+
cmp_optional_nodes(a, b, program, named_type_imports_exports_order, |a, b, module| {
2024
cmp_module_specifiers(a.text_fast(module), b.text_fast(module), cmp_text_case_insensitive)
2125
})
2226
} else {
23-
cmp_optional_nodes(a, b, program, |a, b, module| {
27+
cmp_optional_nodes(a, b, program, named_type_imports_exports_order, |a, b, module| {
2428
cmp_text_case_insensitive(a.text_fast(module), b.text_fast(module))
2529
})
2630
};
@@ -30,13 +34,15 @@ pub fn get_node_sorter_from_order<'a>(order: SortOrder) -> Option<Box<dyn Fn((us
3034
result
3135
}
3236
})),
33-
SortOrder::CaseSensitive => Some(Box::new(|(a_index, a), (b_index, b), program| {
37+
SortOrder::CaseSensitive => Some(Box::new(move |(a_index, a), (b_index, b), program| {
3438
let result = if is_import_or_export_declaration(&a) {
35-
cmp_optional_nodes(a, b, program, |a, b, module| {
39+
cmp_optional_nodes(a, b, program, named_type_imports_exports_order, |a, b, module| {
3640
cmp_module_specifiers(a.text_fast(module), b.text_fast(module), cmp_text_case_sensitive)
3741
})
3842
} else {
39-
cmp_optional_nodes(a, b, program, |a, b, module| cmp_text_case_sensitive(a.text_fast(module), b.text_fast(module)))
43+
cmp_optional_nodes(a, b, program, named_type_imports_exports_order, |a, b, module| {
44+
cmp_text_case_sensitive(a.text_fast(module), b.text_fast(module))
45+
})
4046
};
4147
if result == Ordering::Equal {
4248
a_index.cmp(&b_index)
@@ -51,11 +57,12 @@ fn cmp_optional_nodes<'a>(
5157
a: Option<Node<'a>>,
5258
b: Option<Node<'a>>,
5359
program: Program<'a>,
60+
named_type_imports_exports_order: NamedTypeImportsExportsOrder,
5461
cmp_func: impl Fn(&SourceRange, &SourceRange, Program<'a>) -> Ordering,
5562
) -> Ordering {
5663
if let Some(a) = a {
5764
if let Some(b) = b {
58-
cmp_nodes(a, b, program, cmp_func)
65+
cmp_nodes(a, b, program, named_type_imports_exports_order, cmp_func)
5966
} else {
6067
Ordering::Greater
6168
}
@@ -66,15 +73,36 @@ fn cmp_optional_nodes<'a>(
6673
}
6774
}
6875

69-
fn cmp_nodes<'a>(a: Node<'a>, b: Node<'a>, program: Program<'a>, cmp_func: impl Fn(&SourceRange, &SourceRange, Program<'a>) -> Ordering) -> Ordering {
76+
fn cmp_nodes<'a>(
77+
a: Node<'a>,
78+
b: Node<'a>,
79+
program: Program<'a>,
80+
named_type_imports_exports_order: NamedTypeImportsExportsOrder,
81+
cmp_func: impl Fn(&SourceRange, &SourceRange, Program<'a>) -> Ordering,
82+
) -> Ordering {
7083
let a_nodes = get_comparison_nodes(a);
7184
let b_nodes = get_comparison_nodes(b);
7285

7386
for (i, a) in a_nodes.iter().enumerate() {
7487
if let Some(b) = b_nodes.get(i) {
75-
let cmp_result = cmp_func(a, b, program);
76-
if cmp_result != Ordering::Equal {
77-
return cmp_result;
88+
match (a, b) {
89+
(ComparisonNode::HasType, ComparisonNode::NoType) => match named_type_imports_exports_order {
90+
NamedTypeImportsExportsOrder::First => return Ordering::Less,
91+
NamedTypeImportsExportsOrder::Last => return Ordering::Greater,
92+
NamedTypeImportsExportsOrder::None => {}
93+
},
94+
(ComparisonNode::NoType, ComparisonNode::HasType) => match named_type_imports_exports_order {
95+
NamedTypeImportsExportsOrder::First => return Ordering::Greater,
96+
NamedTypeImportsExportsOrder::Last => return Ordering::Less,
97+
NamedTypeImportsExportsOrder::None => {}
98+
},
99+
(ComparisonNode::Node(a), ComparisonNode::Node(b)) => {
100+
let cmp_result = cmp_func(a, b, program);
101+
if cmp_result != Ordering::Equal {
102+
return cmp_result;
103+
}
104+
}
105+
_ => {}
78106
}
79107
} else {
80108
return Ordering::Greater;
@@ -88,42 +116,58 @@ fn cmp_nodes<'a>(a: Node<'a>, b: Node<'a>, program: Program<'a>, cmp_func: impl
88116
}
89117
}
90118

91-
fn get_comparison_nodes(node: Node) -> Vec<SourceRange> {
119+
enum ComparisonNode {
120+
HasType,
121+
NoType,
122+
Node(SourceRange),
123+
}
124+
125+
fn get_comparison_nodes(node: Node) -> Vec<ComparisonNode> {
92126
match node {
93127
Node::ImportNamedSpecifier(node) => {
128+
let first_node = if node.is_type_only() {
129+
ComparisonNode::HasType
130+
} else {
131+
ComparisonNode::NoType
132+
};
94133
if let Some(imported) = &node.imported {
95-
vec![imported.range(), node.local.range()]
134+
vec![first_node, ComparisonNode::Node(imported.range()), ComparisonNode::Node(node.local.range())]
96135
} else {
97-
vec![node.local.range()]
136+
vec![first_node, ComparisonNode::Node(node.local.range())]
98137
}
99138
}
100139
Node::ExportNamedSpecifier(node) => {
140+
let first_node = if node.is_type_only() {
141+
ComparisonNode::HasType
142+
} else {
143+
ComparisonNode::NoType
144+
};
101145
if let Some(exported) = &node.exported {
102-
vec![node.orig.range(), exported.range()]
146+
vec![first_node, ComparisonNode::Node(node.orig.range()), ComparisonNode::Node(exported.range())]
103147
} else {
104-
vec![node.orig.range()]
148+
vec![first_node, ComparisonNode::Node(node.orig.range())]
105149
}
106150
}
107151
Node::ImportDecl(node) => {
108-
vec![node.src.range()]
152+
vec![ComparisonNode::Node(node.src.range())]
109153
}
110154
Node::NamedExport(node) => {
111155
if let Some(src) = &node.src {
112-
vec![src.range()]
156+
vec![ComparisonNode::Node(src.range())]
113157
} else if cfg!(debug_assertions) {
114158
unimplemented!("Should not call this for named exports with src.");
115159
} else {
116-
vec![node.range()]
160+
vec![ComparisonNode::Node(node.range())]
117161
}
118162
}
119163
Node::ExportAll(node) => {
120-
vec![node.src.range()]
164+
vec![ComparisonNode::Node(node.src.range())]
121165
}
122166
_ => {
123167
if cfg!(debug_assertions) {
124168
unimplemented!("Not implemented sort node.");
125169
} else {
126-
vec![node.range()]
170+
vec![ComparisonNode::Node(node.range())]
127171
}
128172
}
129173
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
~~ lineWidth: 40, exportDeclaration.sortTypeOnlyExports: first ~~
2+
== should sort first ==
3+
export {
4+
type a,
5+
testing,
6+
other,
7+
outttttttttttttttt,
8+
type z,
9+
} from "asdf";
10+
11+
[expect]
12+
export {
13+
type a,
14+
type z,
15+
other,
16+
outttttttttttttttt,
17+
testing,
18+
} from "asdf";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
~~ lineWidth: 40, exportDeclaration.sortTypeOnlyExports: last ~~
2+
== should sort ==
3+
export {
4+
type a,
5+
testing,
6+
other,
7+
outttttttttttttttt,
8+
type z,
9+
} from "asdf";
10+
11+
[expect]
12+
export {
13+
other,
14+
outttttttttttttttt,
15+
testing,
16+
type a,
17+
type z,
18+
} from "asdf";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
~~ lineWidth: 40, exportDeclaration.sortTypeOnlyExports: none ~~
2+
== should sort ==
3+
export {
4+
type a,
5+
testing,
6+
other,
7+
outttttttttttttttt,
8+
type z,
9+
} from "asdf";
10+
11+
[expect]
12+
export {
13+
type a,
14+
other,
15+
outttttttttttttttt,
16+
testing,
17+
type z,
18+
} from "asdf";

0 commit comments

Comments
 (0)