Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 94 additions & 18 deletions crates/oxc_formatter/src/ir_transform/sort_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,11 @@ impl SortImportsTransform {
// const YET_ANOTHER_BOUNDARY = true;
// ```
let (mut import_units, trailing_lines) = chunk.into_import_units(prev_elements);

// Perform sorting if needed
if 1 < import_units.len() {
// TODO: Sort based on `options.groups`, `options.type`, etc...
// TODO: Consider `options.ignore_case`, `special_characters`, removing `?raw`, etc...
import_units.sort_by(|a, b| {
let ord = a.get_source(prev_elements).cmp(b.get_source(prev_elements));
if self.options.order.is_desc() { ord.reverse() } else { ord }
});
}

let preserve_empty_line = self.options.partition_by_newline;
import_units.sort_imports(prev_elements, self.options);

// Output sorted import units
for SortableImport { leading_lines, import_line } in &import_units {
let preserve_empty_line = self.options.partition_by_newline;
for SortableImport { leading_lines, import_line } in import_units {
for line in leading_lines {
line.write(prev_elements, &mut next_elements, preserve_empty_line);
}
Expand Down Expand Up @@ -394,10 +384,7 @@ impl PartitionedChunk {
}

#[must_use]
fn into_import_units(
self,
elements: &[FormatElement],
) -> (Vec<SortableImport>, Vec<SourceLine>) {
fn into_import_units(self, elements: &[FormatElement]) -> (ImportUnits, Vec<SourceLine>) {
let Self::Imports(lines) = self else {
unreachable!(
"`into_import_units()` must be called on `PartitionedChunk::Imports` only."
Expand Down Expand Up @@ -429,7 +416,88 @@ impl PartitionedChunk {
// Any remaining comments/lines are trailing
let trailing_lines = current_leading_lines;

(units, trailing_lines)
(ImportUnits(units), trailing_lines)
}
}

#[derive(Debug)]
struct ImportUnits(Vec<SortableImport>);

impl IntoIterator for ImportUnits {
type Item = SortableImport;
type IntoIter = std::vec::IntoIter<SortableImport>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl ImportUnits {
// TODO: Sort based on `options.groups`, `options.type`, etc...
// TODO: Consider `options.ignore_case`, `special_characters`, removing `?raw`, etc...
fn sort_imports(&mut self, elements: &[FormatElement], options: options::SortImports) {
let imports_len = self.0.len();

// Perform sorting only if needed
if imports_len < 2 {
return;
}

// Separate imports into:
// - sortable: indices of imports that should be sorted
// - fixed: indices of side-effect imports when `sort_side_effects: false`
let mut sortable_indices = vec![];
let mut fixed_indices = vec![];
for (idx, si) in self.0.iter().enumerate() {
if options.sort_side_effects || !si.is_side_effect_import() {
sortable_indices.push(idx);
} else {
fixed_indices.push(idx);
}
}

// Sort indices by comparing their corresponding import sources
sortable_indices.sort_by(|&a, &b| {
let ord = self.0[a].get_source(elements).cmp(self.0[b].get_source(elements));
if options.order.is_desc() { ord.reverse() } else { ord }
});

// Create a permutation map
let mut permutation = vec![0; imports_len];
let mut sortable_iter = sortable_indices.into_iter();
for (idx, perm) in permutation.iter_mut().enumerate() {
// NOTE: This is O(n), but side-effect imports are usually few
if fixed_indices.contains(&idx) {
*perm = idx;
} else if let Some(sorted_idx) = sortable_iter.next() {
*perm = sorted_idx;
}
}
debug_assert!(
permutation.iter().copied().collect::<rustc_hash::FxHashSet<_>>().len() == imports_len,
"`permutation` must be a valid permutation, all indices must be unique."
);

// Apply permutation in-place using cycle decomposition
let mut visited = vec![false; imports_len];
for idx in 0..imports_len {
// Already visited or already in the correct position
if visited[idx] || permutation[idx] == idx {
continue;
}
// Follow the cycle
let mut current = idx;
loop {
let next = permutation[current];
visited[current] = true;
if next == idx {
break;
}
self.0.swap(current, next);
current = next;
}
}
debug_assert!(self.0.len() == imports_len, "Length must remain the same after sorting.");
}
}

Expand Down Expand Up @@ -460,4 +528,12 @@ impl SortableImport {
),
}
}

/// Check if this import is a side-effect-only import.
fn is_side_effect_import(&self) -> bool {
match self.import_line {
SourceLine::Import(_, _, is_side_effect) => is_side_effect,
_ => unreachable!("`import_line` must be of type `SourceLine::Import`."),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -581,87 +581,6 @@ describe("alphabetical", () => {
});
});

it("preserves side-effect import order when sorting disabled", async () => {
await valid({
options: [
{
...options,
groups: ["external", "side-effect", "unknown"],
sortSideEffects: false,
},
],
code: dedent`
import a from 'aaaa'

import 'bbb'
import './cc'
import '../d'
`,
});

await valid({
options: [
{
...options,
groups: ["external", "side-effect", "unknown"],
sortSideEffects: false,
},
],
code: dedent`
import 'c'
import 'bb'
import 'aaa'
`,
});

await invalid({
options: [
{
...options,
groups: ["external", "side-effect", "unknown"],
sortSideEffects: false,
},
],
output: dedent`
import a from 'aaaa'
import e from 'e'

import './cc'
import 'bbb'
import '../d'
`,
code: dedent`
import './cc'
import 'bbb'
import e from 'e'
import a from 'aaaa'
import '../d'
`,
});
});

it("sorts side-effect imports when sorting enabled", async () => {
await invalid({
options: [
{
...options,
groups: ["external", "side-effect", "unknown"],
sortSideEffects: true,
},
],
output: dedent`
import 'aaa'
import 'bb'
import 'c'
`,
code: dedent`
import 'c'
import 'bb'
import 'aaa'
`,
});
});

it("preserves original order when side-effect imports are not grouped", async () => {
await invalid({
output: dedent`
Expand Down
111 changes: 111 additions & 0 deletions crates/oxc_formatter/tests/ir_transform/sort_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,114 @@ import { log2 } from "./log2";
"#,
);
}

// ---

#[test]
fn should_sort_side_effects() {
// Side effect imports stay in their original positions by default
assert_format(
r#"
import c from "c";
import b from "b";
import "s";
import a from "a";
import z from "z";
"#,
&FormatOptions {
experimental_sort_imports: Some(SortImports::default()),
..Default::default()
},
r#"
import a from "a";
import b from "b";
import "s";
import c from "c";
import z from "z";
"#,
);
// Side effect imports stay in their original positions if `sort_side_effects: false`
assert_format(
r#"
import c from "c";
import b from "b";
import "s";
import a from "a";
import z from "z";
"#,
&FormatOptions {
experimental_sort_imports: Some(SortImports {
sort_side_effects: false,
..Default::default()
}),
..Default::default()
},
r#"
import a from "a";
import b from "b";
import "s";
import c from "c";
import z from "z";
"#,
);
assert_format(
r#"
import "c";
import "bb";
import "aaa";
"#,
&FormatOptions {
experimental_sort_imports: Some(SortImports {
sort_side_effects: false,
..Default::default()
}),
..Default::default()
},
r#"
import "c";
import "bb";
import "aaa";
"#,
);
// When `sort_side_effects: true`, all imports are sorted
assert_format(
r#"
import y from "y";
import a from "a";
import "z";
import "x";
"#,
&FormatOptions {
experimental_sort_imports: Some(SortImports {
sort_side_effects: true,
..Default::default()
}),
..Default::default()
},
r#"
import a from "a";
import "x";
import y from "y";
import "z";
"#,
);
assert_format(
r#"
import "c";
import "bb";
import "aaa";
"#,
&FormatOptions {
experimental_sort_imports: Some(SortImports {
sort_side_effects: true,
..Default::default()
}),
..Default::default()
},
r#"
import "aaa";
import "bb";
import "c";
"#,
);
}
Loading