diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports.rs b/crates/oxc_formatter/src/ir_transform/sort_imports.rs index d5e5d43755666..b3f624a294233 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports.rs @@ -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); } @@ -394,10 +384,7 @@ impl PartitionedChunk { } #[must_use] - fn into_import_units( - self, - elements: &[FormatElement], - ) -> (Vec, Vec) { + fn into_import_units(self, elements: &[FormatElement]) -> (ImportUnits, Vec) { let Self::Imports(lines) = self else { unreachable!( "`into_import_units()` must be called on `PartitionedChunk::Imports` only." @@ -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); + +impl IntoIterator for ImportUnits { + type Item = SortableImport; + type IntoIter = std::vec::IntoIter; + + 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::>().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."); } } @@ -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`."), + } + } } diff --git a/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap b/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap index 09aa3ac7c5a9e..230bc61ab2904 100644 --- a/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap +++ b/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap @@ -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` diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs index f3d8a17d175cd..390da5ef3e43b 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs @@ -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"; +"#, + ); +}