From 142e7ac5e6ffde925fd7d53a5b423a832292cb4b Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:31:15 +0000 Subject: [PATCH] feat(formatter/sort-imports): Implement options.ignoreCase: bool (#14367) Part of #14253 Implement `ignoreCase` option, default `true`. --- crates/oxc_formatter/examples/sort_imports.rs | 10 +- .../src/ir_transform/sort_imports.rs | 14 +- crates/oxc_formatter/src/options.rs | 25 +++- .../tests/ir_transform/sort_imports.rs | 132 ++++++++++++++++++ 4 files changed, 174 insertions(+), 7 deletions(-) diff --git a/crates/oxc_formatter/examples/sort_imports.rs b/crates/oxc_formatter/examples/sort_imports.rs index c4c99f4ccfe27..49d49f2b0b0c9 100644 --- a/crates/oxc_formatter/examples/sort_imports.rs +++ b/crates/oxc_formatter/examples/sort_imports.rs @@ -18,9 +18,15 @@ fn main() -> Result<(), String> { let partition_by_comment = args.contains("--partition_by_comment"); let sort_side_effects = args.contains("--sort_side_effects"); let order = args.opt_value_from_str("--order").unwrap_or(None).unwrap_or(SortOrder::Asc); + let ignore_case = !args.contains("--no_ignore_case"); - let sort_imports_options = - SortImports { order, partition_by_newline, partition_by_comment, sort_side_effects }; + let sort_imports_options = SortImports { + order, + partition_by_newline, + partition_by_comment, + sort_side_effects, + ignore_case, + }; // Read source file let path = Path::new(&name); diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports.rs b/crates/oxc_formatter/src/ir_transform/sort_imports.rs index b3f624a294233..367e1dca701ba 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports.rs @@ -1,5 +1,7 @@ use std::ops::Range; +use cow_utils::CowUtils; + use crate::{ JsLabels, formatter::format_element::{ @@ -434,7 +436,7 @@ impl IntoIterator for ImportUnits { impl ImportUnits { // TODO: Sort based on `options.groups`, `options.type`, etc... - // TODO: Consider `options.ignore_case`, `special_characters`, removing `?raw`, etc... + // TODO: Consider `special_characters`, removing `?raw`, etc... fn sort_imports(&mut self, elements: &[FormatElement], options: options::SortImports) { let imports_len = self.0.len(); @@ -458,7 +460,15 @@ impl ImportUnits { // 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)); + let source_a = self.0[a].get_source(elements); + let source_b = self.0[b].get_source(elements); + + let ord = if options.ignore_case { + source_a.cow_to_lowercase().cmp(&source_b.cow_to_lowercase()) + } else { + source_a.cmp(source_b) + }; + if options.order.is_desc() { ord.reverse() } else { ord } }); diff --git a/crates/oxc_formatter/src/options.rs b/crates/oxc_formatter/src/options.rs index a45b87638a2cd..4f86158e93091 100644 --- a/crates/oxc_formatter/src/options.rs +++ b/crates/oxc_formatter/src/options.rs @@ -917,16 +917,35 @@ impl fmt::Display for OperatorPosition { // --- -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct SortImports { - /// Sort order (asc or desc). - pub order: SortOrder, /// Partition imports by newlines. + /// Default is `false`. pub partition_by_newline: bool, /// Partition imports by comments. + /// Default is `false`. pub partition_by_comment: bool, /// Sort side effects imports. + /// Default is `false`. pub sort_side_effects: bool, + /// Sort order (asc or desc). + /// Default is ascending (asc). + pub order: SortOrder, + /// Ignore case when sorting. + /// Default is `true`. + pub ignore_case: bool, +} + +impl Default for SortImports { + fn default() -> Self { + Self { + partition_by_newline: false, + partition_by_comment: false, + sort_side_effects: false, + order: SortOrder::default(), + ignore_case: true, + } + } } #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs index 390da5ef3e43b..e77e50e7a0014 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs @@ -704,6 +704,24 @@ import { log } from "./log"; import { log10 } from "./log10"; import { log1p } from "./log1p"; import { log2 } from "./log2"; +"#, + ); + assert_format( + r#" +import { log } from "./log"; +import { log10 } from "./log10"; +import { log1p } from "./log1p"; +import { log2 } from "./log2"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports::default()), + ..Default::default() + }, + r#" +import { log } from "./log"; +import { log10 } from "./log10"; +import { log1p } from "./log1p"; +import { log2 } from "./log2"; "#, ); } @@ -818,3 +836,117 @@ import "c"; "#, ); } + +// --- + +#[test] +fn should_sort_with_ignore_case_option() { + // Case-insensitive (ignore_case: true by default) + assert_format( + r#" +import { A } from "a"; +import { b } from "B"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports::default()), + ..Default::default() + }, + r#" +import { A } from "a"; +import { b } from "B"; +"#, + ); + // "a" and "A" are treated as the same, maintaining original order + assert_format( + r#" +import x from "A"; +import y from "a"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports::default()), + ..Default::default() + }, + r#" +import x from "A"; +import y from "a"; +"#, + ); + // Mixed case sorting with ignore_case: true + assert_format( + r#" +import { z } from "Z"; +import { b } from "B"; +import { a } from "a"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports { + ignore_case: true, + ..Default::default() + }), + ..Default::default() + }, + r#" +import { a } from "a"; +import { b } from "B"; +import { z } from "Z"; +"#, + ); + // Case-sensitive, lowercase comes after uppercase in ASCII + assert_format( + r#" +import { a } from "a"; +import { B } from "B"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports { + ignore_case: false, + ..Default::default() + }), + ..Default::default() + }, + r#" +import { B } from "B"; +import { a } from "a"; +"#, + ); + // Capital A vs lowercase a + assert_format( + r#" +import x from "a"; +import y from "A"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports { + ignore_case: false, + ..Default::default() + }), + ..Default::default() + }, + r#" +import y from "A"; +import x from "a"; +"#, + ); + // Multiple imports with mixed case + assert_format( + r#" +import { z } from "z"; +import { B } from "B"; +import { a } from "a"; +import { Z } from "Z"; +"#, + &FormatOptions { + experimental_sort_imports: Some(SortImports { + ignore_case: false, + ..Default::default() + }), + ..Default::default() + }, + r#" +import { B } from "B"; +import { Z } from "Z"; +import { a } from "a"; +import { z } from "z"; +"#, + ); +}