diff --git a/crates/oxc_formatter/examples/sort_imports.rs b/crates/oxc_formatter/examples/sort_imports.rs index 9c6c98dfaf4f0..8d9f8d436976c 100644 --- a/crates/oxc_formatter/examples/sort_imports.rs +++ b/crates/oxc_formatter/examples/sort_imports.rs @@ -3,7 +3,7 @@ use std::{fs, path::Path}; use oxc_allocator::Allocator; -use oxc_formatter::{FormatOptions, Formatter, SortImports, SortOrder, get_parse_options}; +use oxc_formatter::{FormatOptions, Formatter, SortImportsOptions, SortOrder, get_parse_options}; use oxc_parser::Parser; use oxc_span::SourceType; use pico_args::Arguments; @@ -21,7 +21,7 @@ fn main() -> Result<(), String> { let ignore_case = !args.contains("--no_ignore_case"); let newlines_between = !args.contains("--no_newlines_between"); - let sort_imports_options = SortImports { + let sort_imports_options = SortImportsOptions { order, partition_by_newline, partition_by_comment, diff --git a/crates/oxc_formatter/src/ir_transform/mod.rs b/crates/oxc_formatter/src/ir_transform/mod.rs index c40eea09db724..39f27ea70c563 100644 --- a/crates/oxc_formatter/src/ir_transform/mod.rs +++ b/crates/oxc_formatter/src/ir_transform/mod.rs @@ -21,4 +21,4 @@ mod sort_imports; -pub use sort_imports::SortImportsTransform; +pub use sort_imports::*; diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs index 7ee92272024f9..a85d1b292512d 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs @@ -3,12 +3,10 @@ use std::{borrow::Cow, path::Path}; use cow_utils::CowUtils; use phf::phf_set; -use crate::{ - ir_transform::sort_imports::{ - group_config::{GroupName, ImportModifier, ImportSelector}, - source_line::ImportLineMetadata, - }, - options, +use crate::ir_transform::sort_imports::{ + group_config::{GroupName, ImportModifier, ImportSelector}, + options::SortImportsOptions, + source_line::ImportLineMetadata, }; /// Compute all metadata derived from import line metadata. @@ -17,7 +15,7 @@ use crate::{ pub fn compute_import_metadata<'a>( metadata: &ImportLineMetadata<'a>, groups: &[Vec], - options: &options::SortImports, + options: &SortImportsOptions, ) -> (usize, Cow<'a, str>, bool) { let ImportLineMetadata { source, @@ -347,7 +345,7 @@ enum ImportPathKind { } /// Determine the path kind for an import source. -fn to_path_kind(source: &str, options: &options::SortImports) -> ImportPathKind { +fn to_path_kind(source: &str, options: &SortImportsOptions) -> ImportPathKind { if is_builtin(source) { return ImportPathKind::Builtin; } diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs index 1341a49f64402..d4d68c3adc578 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs @@ -1,5 +1,6 @@ mod compute_metadata; mod group_config; +pub mod options; mod partitioned_chunk; mod sortable_imports; mod source_line; @@ -13,19 +14,18 @@ use crate::{ partitioned_chunk::PartitionedChunk, source_line::SourceLine, }, - options, }; /// An IR transform that sorts import statements according to specified options. /// Heavily inspired by ESLint's `@perfectionist/sort-imports` rule. /// pub struct SortImportsTransform { - options: options::SortImports, + options: options::SortImportsOptions, groups: Vec>, } impl SortImportsTransform { - pub fn new(options: options::SortImports) -> Self { + pub fn new(options: options::SortImportsOptions) -> Self { // Parse string based groups into our internal representation for performance let groups = if let Some(groups) = &options.groups { parse_groups_from_strings(groups) diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs new file mode 100644 index 0000000000000..803edddb0db6c --- /dev/null +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs @@ -0,0 +1,90 @@ +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SortImportsOptions { + /// 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, + /// Whether to insert blank lines between different import groups. + /// - `true`: Insert one blank line between groups (default) + /// - `false`: No blank lines between groups + /// + /// NOTE: Cannot be used together with `partition_by_newline: true`. + pub newlines_between: bool, + /// Prefixes for internal imports. + /// If `None`, uses the default internal patterns. + pub internal_pattern: Option>, + /// Groups configuration for organizing imports. + /// Each inner `Vec` represents a group, and multiple group names in the same `Vec` are treated as one. + /// If `None`, uses the default groups. + pub groups: Option>>, +} + +impl Default for SortImportsOptions { + fn default() -> Self { + Self { + partition_by_newline: false, + partition_by_comment: false, + sort_side_effects: false, + order: SortOrder::default(), + ignore_case: true, + newlines_between: true, + internal_pattern: None, + groups: None, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum SortOrder { + /// Sort in ascending order (A-Z). + #[default] + Asc, + /// Sort in descending order (Z-A). + Desc, +} + +impl SortOrder { + pub const fn is_asc(self) -> bool { + matches!(self, Self::Asc) + } + + pub const fn is_desc(self) -> bool { + matches!(self, Self::Desc) + } +} + +impl FromStr for SortOrder { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "asc" => Ok(Self::Asc), + "desc" => Ok(Self::Desc), + _ => Err("Value not supported for SortOrder. Supported values are 'asc' and 'desc'."), + } + } +} + +impl fmt::Display for SortOrder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + SortOrder::Asc => "ASC", + SortOrder::Desc => "DESC", + }; + f.write_str(s) + } +} diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/partitioned_chunk.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/partitioned_chunk.rs index 654d791813469..b5acd4fd5c5d5 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/partitioned_chunk.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/partitioned_chunk.rs @@ -1,11 +1,9 @@ -use crate::{ - ir_transform::sort_imports::{ - compute_metadata::compute_import_metadata, - group_config::GroupName, - sortable_imports::{SortSortableImports, SortableImport}, - source_line::SourceLine, - }, - options, +use crate::ir_transform::sort_imports::{ + compute_metadata::compute_import_metadata, + group_config::GroupName, + options::SortImportsOptions, + sortable_imports::{SortSortableImports, SortableImport}, + source_line::SourceLine, }; #[derive(Debug)] @@ -50,7 +48,7 @@ impl<'a> PartitionedChunk<'a> { pub fn into_sorted_import_units( self, groups: &[Vec], - options: &options::SortImports, + options: &SortImportsOptions, ) -> (Vec>, Vec>) { let Self::Imports(lines) = self else { unreachable!( diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/sortable_imports.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/sortable_imports.rs index 5e621a036aafa..5070a9a46388d 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/sortable_imports.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/sortable_imports.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{ir_transform::sort_imports::source_line::SourceLine, options}; +use crate::ir_transform::sort_imports::{options::SortImportsOptions, source_line::SourceLine}; #[derive(Debug)] pub struct SortableImport<'a> { @@ -18,11 +18,11 @@ pub struct SortableImport<'a> { // --- pub trait SortSortableImports { - fn sort(&mut self, options: &options::SortImports); + fn sort(&mut self, options: &SortImportsOptions); } impl SortSortableImports for Vec> { - fn sort(&mut self, options: &options::SortImports) { + fn sort(&mut self, options: &SortImportsOptions) { let imports_len = self.len(); // Perform sorting only if needed @@ -90,7 +90,7 @@ impl SortSortableImports for Vec> { fn sort_within_group( indices: &mut [usize], imports: &[SortableImport], - options: &options::SortImports, + options: &SortImportsOptions, ) { if indices.len() < 2 { return; @@ -148,7 +148,7 @@ fn sort_within_group( fn sort_indices_by_source( indices: &mut [usize], imports: &[SortableImport], - options: &options::SortImports, + options: &SortImportsOptions, ) { indices.sort_by(|&a, &b| { natord::compare(&imports[a].normalized_source, &imports[b].normalized_source) diff --git a/crates/oxc_formatter/src/lib.rs b/crates/oxc_formatter/src/lib.rs index 56793c53136a9..0cc809a3901a5 100644 --- a/crates/oxc_formatter/src/lib.rs +++ b/crates/oxc_formatter/src/lib.rs @@ -16,6 +16,7 @@ use oxc_allocator::Allocator; use oxc_ast::ast::*; pub use crate::embedded_formatter::{EmbeddedFormatter, EmbeddedFormatterCallback}; +pub use crate::ir_transform::options::*; pub use crate::options::*; pub use crate::service::{oxfmtrc::Oxfmtrc, parse_utils::*}; use crate::{ diff --git a/crates/oxc_formatter/src/options.rs b/crates/oxc_formatter/src/options.rs index 408c4391e1014..6b1fca42aa142 100644 --- a/crates/oxc_formatter/src/options.rs +++ b/crates/oxc_formatter/src/options.rs @@ -7,6 +7,7 @@ use crate::{ prelude::{if_group_breaks, token}, printer::PrinterOptions, }, + ir_transform::options::SortImportsOptions, write, }; @@ -71,9 +72,8 @@ pub struct FormatOptions { /// Enable formatting for embedded languages (e.g., CSS, SQL, GraphQL) within template literals. Defaults to "auto". pub embedded_language_formatting: EmbeddedLanguageFormatting, - // TODO: `FormatOptions`? Split out as `TransformOptions`? /// Sort import statements. By default disabled. - pub experimental_sort_imports: Option, + pub experimental_sort_imports: Option, } impl FormatOptions { @@ -978,93 +978,3 @@ impl fmt::Display for EmbeddedLanguageFormatting { f.write_str(s) } } - -// --- - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SortImports { - /// 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, - /// Whether to insert blank lines between different import groups. - /// - `true`: Insert one blank line between groups (default) - /// - `false`: No blank lines between groups - /// - /// NOTE: Cannot be used together with `partition_by_newline: true`. - pub newlines_between: bool, - /// Prefixes for internal imports. - /// If `None`, uses the default internal patterns. - pub internal_pattern: Option>, - /// Groups configuration for organizing imports. - /// Each inner `Vec` represents a group, and multiple group names in the same `Vec` are treated as one. - /// If `None`, uses the default groups. - pub groups: Option>>, -} - -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, - newlines_between: true, - internal_pattern: None, - groups: None, - } - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] -pub enum SortOrder { - /// Sort in ascending order (A-Z). - #[default] - Asc, - /// Sort in descending order (Z-A). - Desc, -} - -impl SortOrder { - pub const fn is_asc(self) -> bool { - matches!(self, Self::Asc) - } - - pub const fn is_desc(self) -> bool { - matches!(self, Self::Desc) - } -} - -impl FromStr for SortOrder { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "asc" => Ok(Self::Asc), - "desc" => Ok(Self::Desc), - _ => Err("Value not supported for SortOrder. Supported values are 'asc' and 'desc'."), - } - } -} - -impl fmt::Display for SortOrder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = match self { - SortOrder::Asc => "ASC", - SortOrder::Desc => "DESC", - }; - f.write_str(s) - } -} diff --git a/crates/oxc_formatter/src/service/oxfmtrc.rs b/crates/oxc_formatter/src/service/oxfmtrc.rs index 0550cfff238bc..6b49d235d26e0 100644 --- a/crates/oxc_formatter/src/service/oxfmtrc.rs +++ b/crates/oxc_formatter/src/service/oxfmtrc.rs @@ -6,7 +6,8 @@ use serde::{Deserialize, Deserializer, Serialize}; use crate::{ ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, EmbeddedLanguageFormatting, Expand, FormatOptions, IndentStyle, IndentWidth, LineEnding, - LineWidth, QuoteProperties, QuoteStyle, Semicolons, SortImports, SortOrder, TrailingCommas, + LineWidth, QuoteProperties, QuoteStyle, Semicolons, SortImportsOptions, SortOrder, + TrailingCommas, }; /// Configuration options for the formatter. @@ -370,7 +371,7 @@ impl Oxfmtrc { return Err("Invalid `sortImports` configuration: `partitionByNewline: true` and `newlinesBetween: true` cannot be used together".to_string()); } - options.experimental_sort_imports = Some(SortImports { + options.experimental_sort_imports = Some(SortImportsOptions { partition_by_newline: sort_imports_config.partition_by_newline, partition_by_comment: sort_imports_config.partition_by_comment, sort_side_effects: sort_imports_config.sort_side_effects, diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index a01c1ef179587..c25ed32a06651 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -31,7 +31,7 @@ use oxc::{ use oxc_formatter::{ ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, Expand, FormatOptions, Formatter, IndentStyle, IndentWidth, LineEnding, LineWidth, QuoteProperties, QuoteStyle, - Semicolons, SortImports, SortOrder, TrailingCommas, get_parse_options, + Semicolons, SortImportsOptions, SortOrder, TrailingCommas, get_parse_options, }; use oxc_linter::{ ConfigStore, ConfigStoreBuilder, ContextSubHost, ExternalPluginStore, LintOptions, Linter, @@ -503,7 +503,7 @@ impl Oxc { .and_then(|o| o.parse::().ok()) .unwrap_or_default(); - format_options.experimental_sort_imports = Some(SortImports { + format_options.experimental_sort_imports = Some(SortImportsOptions { partition_by_newline: sort_imports_config.partition_by_newline.unwrap_or(false), partition_by_comment: sort_imports_config.partition_by_comment.unwrap_or(false), sort_side_effects: sort_imports_config.sort_side_effects.unwrap_or(false), diff --git a/tasks/benchmark/benches/formatter.rs b/tasks/benchmark/benches/formatter.rs index e7275acf62bcb..073ce657163ef 100644 --- a/tasks/benchmark/benches/formatter.rs +++ b/tasks/benchmark/benches/formatter.rs @@ -1,6 +1,6 @@ use oxc_allocator::Allocator; use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use oxc_formatter::{FormatOptions, Formatter, SortImports, get_parse_options}; +use oxc_formatter::{FormatOptions, Formatter, SortImportsOptions, get_parse_options}; use oxc_parser::Parser; use oxc_tasks_common::TestFiles; @@ -20,7 +20,7 @@ fn bench_formatter(criterion: &mut Criterion) { .parse() .program; let format_options = FormatOptions { - experimental_sort_imports: Some(SortImports::default()), + experimental_sort_imports: Some(SortImportsOptions::default()), ..Default::default() }; runner.run(|| {