Skip to content

Commit 97c4516

Browse files
committed
fix: Fix node sorting edge case related to whitespace.
1 parent 10d560c commit 97c4516

File tree

6 files changed

+144
-54
lines changed

6 files changed

+144
-54
lines changed

src/parsing/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ mod comments;
22
mod node_helpers;
33
mod parser_types;
44
mod parser;
5+
mod sorting;
56
mod tokens;
67
mod swc;
78

89
use comments::*;
910
use parser_types::*;
11+
use sorting::*;
1012
use tokens::*;
1113

1214
pub use parser::parse;

src/parsing/parser.rs

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ fn parse_named_import_or_export_specifiers<'a>(parent: &Node<'a>, specifiers: Ve
11931193
fn get_node_sorter<'a>(
11941194
parent_decl: &Node,
11951195
context: &Context<'a>,
1196-
) -> Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering>> {
1196+
) -> Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> std::cmp::Ordering>> {
11971197
match parent_decl {
11981198
Node::NamedExport(_) => get_node_sorter_from_order(context.config.export_declaration_sort_named_exports),
11991199
Node::ImportDecl(_) => get_node_sorter_from_order(context.config.import_declaration_sort_named_imports),
@@ -5095,7 +5095,7 @@ struct ParseSeparatedValuesOptions<'a> {
50955095
custom_single_line_separator: Option<PrintItems>,
50965096
multi_line_options: parser_helpers::MultiLineOptions,
50975097
force_possible_newline_at_start: bool,
5098-
node_sorter: Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering>>,
5098+
node_sorter: Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> std::cmp::Ordering>>,
50995099
}
51005100

51015101
#[inline]
@@ -5435,7 +5435,7 @@ struct ParseObjectLikeNodeOptions<'a> {
54355435
prefer_single_line: bool,
54365436
surround_single_line_with_spaces: bool,
54375437
allow_blank_lines: bool,
5438-
node_sorter: Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering>>,
5438+
node_sorter: Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> std::cmp::Ordering>>,
54395439
}
54405440

54415441
fn parse_object_like_node<'a>(opts: ParseObjectLikeNodeOptions<'a>, context: &mut Context<'a>) -> PrintItems {
@@ -6789,54 +6789,3 @@ fn get_parsed_semi_colon(option: SemiColons, is_trailing: bool, is_multi_line: &
67896789
fn create_span_data(lo: BytePos, hi: BytePos) -> Span {
67906790
Span { lo, hi, ctxt: Default::default() }
67916791
}
6792-
6793-
/* sort functions */
6794-
6795-
fn get_node_sorter_from_order<'a>(order: SortOrder) -> Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering>> {
6796-
match order {
6797-
SortOrder::Maintain => None,
6798-
SortOrder::CaseInsensitive => Some(sort_by_text_case_insensitive()),
6799-
SortOrder::CaseSensitive => Some(sort_by_text_case_sensitive()),
6800-
}
6801-
}
6802-
6803-
fn sort_by_text_case_insensitive<'a>() -> Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering> {
6804-
Box::new(|a, b, context| {
6805-
if let Some(a) = a.as_ref() {
6806-
if let Some(b) = b.as_ref() {
6807-
let case_insensitive_result = a.text(context).to_lowercase().cmp(&b.text(context).to_lowercase());
6808-
if case_insensitive_result == std::cmp::Ordering::Equal {
6809-
a.text(context).cmp(&b.text(context)) // do a case sensitive comparison at this point
6810-
} else {
6811-
case_insensitive_result
6812-
}
6813-
} else {
6814-
std::cmp::Ordering::Greater
6815-
}
6816-
} else {
6817-
if b.is_none() {
6818-
std::cmp::Ordering::Equal
6819-
} else {
6820-
std::cmp::Ordering::Less
6821-
}
6822-
}
6823-
})
6824-
}
6825-
6826-
fn sort_by_text_case_sensitive<'a>() -> Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &mut Context<'a>) -> std::cmp::Ordering> {
6827-
Box::new(|a, b, context| {
6828-
if let Some(a) = a.as_ref() {
6829-
if let Some(b) = b.as_ref() {
6830-
a.text(context).cmp(&b.text(context)) // do a case sensitive comparison at this point
6831-
} else {
6832-
std::cmp::Ordering::Greater
6833-
}
6834-
} else {
6835-
if b.is_none() {
6836-
std::cmp::Ordering::Equal
6837-
} else {
6838-
std::cmp::Ordering::Less
6839-
}
6840-
}
6841-
})
6842-
}

src/parsing/parser_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use super::*;
88
use super::super::configuration::*;
99
use super::super::utils::Stack;
1010

11+
// todo: the span_data() should be changed to span() -- This was previously based on how swc used to work
12+
1113
pub struct Context<'a> {
1214
pub config: &'a Configuration,
1315
pub comments: CommentCollection<'a>,

src/parsing/sorting.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use swc_common::{Span, Spanned};
2+
use std::cmp::Ordering;
3+
4+
use super::*;
5+
use crate::configuration::*;
6+
7+
pub fn get_node_sorter_from_order<'a>(order: SortOrder) -> Option<Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> Ordering>> {
8+
match order {
9+
SortOrder::Maintain => None,
10+
SortOrder::CaseInsensitive => Some(sort_by_text_case_insensitive()),
11+
SortOrder::CaseSensitive => Some(sort_by_text_case_sensitive()),
12+
}
13+
}
14+
15+
fn sort_by_text_case_insensitive<'a>() -> Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> Ordering> {
16+
Box::new(|a, b, context| {
17+
cmp_optional_nodes(a, b, context, cmp_nodes_case_insensitive)
18+
})
19+
}
20+
21+
fn sort_by_text_case_sensitive<'a>() -> Box<dyn Fn(&Option<Node<'a>>, &Option<Node<'a>>, &Context<'a>) -> Ordering> {
22+
Box::new(|a, b, context| {
23+
cmp_optional_nodes(a, b, context, cmp_nodes_case_sensitive)
24+
})
25+
}
26+
27+
fn cmp_optional_nodes<'a>(
28+
a: &Option<Node<'a>>,
29+
b: &Option<Node<'a>>,
30+
context: &Context<'a>,
31+
cmp_func: impl Fn(&dyn Ranged, &dyn Ranged, &Context<'a>) -> Ordering,
32+
) -> Ordering {
33+
if let Some(a) = a.as_ref() {
34+
if let Some(b) = b.as_ref() {
35+
cmp_nodes(&a, &b, context, cmp_func)
36+
} else {
37+
Ordering::Greater
38+
}
39+
} else {
40+
if b.is_none() {
41+
Ordering::Equal
42+
} else {
43+
Ordering::Less
44+
}
45+
}
46+
}
47+
48+
fn cmp_nodes<'a>(
49+
a: &Node<'a>,
50+
b: &Node<'a>,
51+
context: &Context<'a>,
52+
cmp_func: impl Fn(&dyn Ranged, &dyn Ranged, &Context<'a>) -> Ordering,
53+
) -> Ordering {
54+
let a_nodes = get_comparison_nodes(a);
55+
let b_nodes = get_comparison_nodes(b);
56+
57+
for (i, a) in a_nodes.iter().enumerate() {
58+
if let Some(b) = b_nodes.get(i) {
59+
let cmp_result = cmp_func(a, b, context);
60+
if cmp_result != Ordering::Equal {
61+
return cmp_result;
62+
}
63+
} else {
64+
return Ordering::Greater;
65+
}
66+
}
67+
68+
if a_nodes.len() < b_nodes.len() {
69+
Ordering::Less
70+
} else {
71+
Ordering::Equal
72+
}
73+
}
74+
75+
fn get_comparison_nodes<'a>(node: &Node<'a>) -> Vec<Span> {
76+
match node {
77+
Node::ImportNamedSpecifier(node) => {
78+
if let Some(imported) = &node.imported {
79+
vec![imported.span(), node.local.span()]
80+
} else {
81+
vec![node.local.span()]
82+
}
83+
},
84+
Node::ExportNamedSpecifier(node) => {
85+
if let Some(exported) = &node.exported {
86+
vec![node.orig.span(), exported.span()]
87+
} else {
88+
vec![node.orig.span()]
89+
}
90+
},
91+
_ => {
92+
#[cfg(debug_assertions)]
93+
unimplemented!("Not implemented sort node.");
94+
#[cfg(not(debug_assertions))]
95+
vec![node.span_data()]
96+
}
97+
}
98+
}
99+
100+
fn cmp_nodes_case_sensitive<'a>(a: &dyn Ranged, b: &dyn Ranged, context: &Context) -> Ordering {
101+
a.text(context).cmp(&b.text(context))
102+
}
103+
104+
fn cmp_nodes_case_insensitive<'a>(a: &dyn Ranged, b: &dyn Ranged, context: &Context) -> Ordering {
105+
let case_insensitive_result = a.text(context).to_lowercase().cmp(&b.text(context).to_lowercase());
106+
if case_insensitive_result == Ordering::Equal {
107+
cmp_nodes_case_sensitive(a, b, context)
108+
} else {
109+
case_insensitive_result
110+
}
111+
}

tests/specs/declarations/export/ExportNamedDeclaration_All.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,19 @@ export { a } from "test";
7777
export {
7878
b, // test
7979
} from "test";
80+
81+
== should sort the exports in alphabetical order ==
82+
export { c, B, a, f2, f, f1} from "a";
83+
export { b as a, a as b } from "test";
84+
export { a as ab, a as aa } from "test";
85+
export { a as ab, a as aa } from "test";
86+
export { a as ab, a } from "test";
87+
export { a, a as ab } from "test";
88+
89+
[expect]
90+
export { a, B, c, f, f1, f2 } from "a";
91+
export { a as b, b as a } from "test";
92+
export { a as aa, a as ab } from "test";
93+
export { a as aa, a as ab } from "test";
94+
export { a, a as ab } from "test";
95+
export { a, a as ab } from "test";

tests/specs/declarations/import/ImportDeclaration_All.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,19 @@ import {
8989

9090
== should sort the imports in alphabetical order ==
9191
import { d, c, B, a, E, f2, f, f1} from "test";
92+
import { b as a, a as b } from "test";
93+
import { a as ab, a as aa } from "test";
94+
import { a as ab, a as aa } from "test";
95+
import { a as ab, a } from "test";
96+
import { a, a as ab } from "test";
9297

9398
[expect]
9499
import { a, B, c, d, E, f, f1, f2 } from "test";
100+
import { a as b, b as a } from "test";
101+
import { a as aa, a as ab } from "test";
102+
import { a as aa, a as ab } from "test";
103+
import { a, a as ab } from "test";
104+
import { a, a as ab } from "test";
95105

96106
== should prefer single line by default ==
97107
import {

0 commit comments

Comments
 (0)