11use std:: path:: Path ;
22
3+ use rustc_hash:: FxHashSet ;
34use schemars:: JsonSchema ;
45use serde:: { Deserialize , Serialize } ;
56use serde_json:: Value ;
67
78use oxc_formatter:: {
89 ArrowParentheses , AttributePosition , BracketSameLine , BracketSpacing , CustomGroupDefinition ,
9- EmbeddedLanguageFormatting , Expand , FormatOptions , ImportModifier , ImportSelector , IndentStyle ,
10- IndentWidth , LineEnding , LineWidth , QuoteProperties , QuoteStyle , Semicolons ,
11- SortImportsOptions , SortOrder , TailwindcssOptions , TrailingCommas ,
10+ EmbeddedLanguageFormatting , Expand , FormatOptions , GroupEntry , GroupName , ImportModifier ,
11+ ImportSelector , IndentStyle , IndentWidth , LineEnding , LineWidth , QuoteProperties , QuoteStyle ,
12+ Semicolons , SortImportsOptions , SortOrder , TailwindcssOptions , TrailingCommas ,
1213} ;
1314use oxc_toml:: Options as TomlFormatterOptions ;
1415
@@ -419,7 +420,46 @@ impl FormatConfig {
419420 if let Some ( v) = config. internal_pattern {
420421 sort_imports. internal_pattern = v;
421422 }
423+ // Validate and parse `customGroups` first, since `groups` may refer to custom group names.
424+ if let Some ( v) = config. custom_groups {
425+ let mut custom_groups = Vec :: with_capacity ( v. len ( ) ) ;
426+ for cg in v {
427+ let CustomGroupItemConfig { group_name, element_name_pattern, .. } = cg;
428+ let selector = match cg. selector . as_deref ( ) {
429+ Some ( s) => match ImportSelector :: parse ( s) {
430+ Some ( parsed) => Some ( parsed) ,
431+ None => {
432+ return Err ( format ! (
433+ "Invalid `sortImports` configuration: unknown selector: `{s}` in customGroups: `{group_name}`"
434+ ) ) ;
435+ }
436+ } ,
437+ None => None ,
438+ } ;
439+ let raw_modifiers = cg. modifiers . unwrap_or_default ( ) ;
440+ let mut modifiers = Vec :: with_capacity ( raw_modifiers. len ( ) ) ;
441+ for m in & raw_modifiers {
442+ match ImportModifier :: parse ( m) {
443+ Some ( parsed) => modifiers. push ( parsed) ,
444+ None => {
445+ return Err ( format ! (
446+ "Invalid `sortImports` configuration: unknown modifier: `{m}` in customGroups: `{group_name}`"
447+ ) ) ;
448+ }
449+ }
450+ }
451+ custom_groups. push ( CustomGroupDefinition {
452+ group_name,
453+ element_name_pattern,
454+ selector,
455+ modifiers,
456+ } ) ;
457+ }
458+ sort_imports. custom_groups = custom_groups;
459+ }
422460 if let Some ( v) = config. groups {
461+ let custom_group_names: FxHashSet < & str > =
462+ sort_imports. custom_groups . iter ( ) . map ( |g| g. group_name . as_str ( ) ) . collect ( ) ;
423463 let mut groups = Vec :: new ( ) ;
424464 let mut newline_boundary_overrides: Vec < Option < bool > > = Vec :: new ( ) ;
425465 let mut pending_override: Option < bool > = None ;
@@ -437,15 +477,24 @@ impl FormatConfig {
437477 }
438478 other => {
439479 if !groups. is_empty ( ) {
440- // Record the boundary between the previous group and this one.
441- // `pending_override` is
442- // - `Some(bool)` if a marker preceded this group
443- // - or `None` (= use global `newlines_between`) otherwise
444- // For the very first group (`groups.is_empty()`),
445- // there is no preceding boundary, so we skip this entirely.
446480 newline_boundary_overrides. push ( pending_override. take ( ) ) ;
447481 }
448- groups. push ( other. into_vec ( ) ) ;
482+ let mut entries = Vec :: new ( ) ;
483+ for name in other. into_vec ( ) {
484+ let entry = if name == "unknown" {
485+ GroupEntry :: Unknown
486+ } else if custom_group_names. contains ( name. as_str ( ) ) {
487+ GroupEntry :: Custom ( name)
488+ } else if let Some ( group_name) = GroupName :: parse ( & name) {
489+ GroupEntry :: Predefined ( group_name)
490+ } else {
491+ return Err ( format ! (
492+ "Invalid `sortImports` configuration: unknown group name `{name}` in `groups`"
493+ ) ) ;
494+ } ;
495+ entries. push ( entry) ;
496+ }
497+ groups. push ( entries) ;
449498 }
450499 }
451500 }
@@ -463,22 +512,6 @@ impl FormatConfig {
463512 {
464513 return Err ( "Invalid `sortImports` configuration: `partitionByNewline` and per-group `{ \" newlinesBetween\" }` markers cannot be used together" . to_string ( ) ) ;
465514 }
466- if let Some ( v) = config. custom_groups {
467- sort_imports. custom_groups = v
468- . into_iter ( )
469- . map ( |c| CustomGroupDefinition {
470- group_name : c. group_name ,
471- element_name_pattern : c. element_name_pattern ,
472- selector : c. selector . as_deref ( ) . and_then ( ImportSelector :: parse) ,
473- modifiers : c
474- . modifiers
475- . unwrap_or_default ( )
476- . iter ( )
477- . filter_map ( |s| ImportModifier :: parse ( s) )
478- . collect ( ) ,
479- } )
480- . collect ( ) ;
481- }
482515
483516 // `partition_by_newline: true` and `newlines_between: true` cannot be used together
484517 if sort_imports. partition_by_newline && sort_imports. newlines_between {
@@ -679,8 +712,8 @@ pub struct SortImportsConfig {
679712 ///
680713 /// The list of selectors is sorted from most to least important:
681714 /// - `type` — TypeScript type imports.
682- /// - `side-effect-style ` — Side effect style imports.
683- /// - `side-effect ` — Side effect imports.
715+ /// - `side_effect_style ` — Side effect style imports.
716+ /// - `side_effect ` — Side effect imports.
684717 /// - `style` — Style imports.
685718 /// - `index` — Main file from the current directory.
686719 /// - `sibling` — Modules from the same directory.
@@ -692,7 +725,7 @@ pub struct SortImportsConfig {
692725 /// - `import` — Any import.
693726 ///
694727 /// The list of modifiers is sorted from most to least important:
695- /// - `side-effect ` — Side effect imports.
728+ /// - `side_effect ` — Side effect imports.
696729 /// - `type` — TypeScript type imports.
697730 /// - `value` — Value imports.
698731 /// - `default` — Imports containing the default specifier.
@@ -776,14 +809,14 @@ pub struct CustomGroupItemConfig {
776809 pub element_name_pattern : Vec < String > ,
777810 /// Selector to match the import kind.
778811 ///
779- /// Possible values: `"type"`, `"side-effect-style "`, `"side-effect "`, `"style"`, `"index"`,
812+ /// Possible values: `"type"`, `"side_effect_style "`, `"side_effect "`, `"style"`, `"index"`,
780813 /// `"sibling"`, `"parent"`, `"subpath"`, `"internal"`, `"builtin"`, `"external"`, `"import"`
781814 #[ serde( skip_serializing_if = "Option::is_none" ) ]
782815 pub selector : Option < String > ,
783816 /// Modifiers to match the import characteristics.
784817 /// All specified modifiers must be present (AND logic).
785818 ///
786- /// Possible values: `"side-effect "`, `"type"`, `"value"`, `"default"`, `"wildcard"`, `"named"`
819+ /// Possible values: `"side_effect "`, `"type"`, `"value"`, `"default"`, `"wildcard"`, `"named"`
787820 #[ serde( skip_serializing_if = "Option::is_none" ) ]
788821 pub modifiers : Option < Vec < String > > ,
789822}
@@ -1259,9 +1292,21 @@ mod tests {
12591292 let oxfmt_options = config. into_oxfmt_options ( ) . unwrap ( ) ;
12601293 let sort_imports = oxfmt_options. format_options . experimental_sort_imports . unwrap ( ) ;
12611294 assert_eq ! ( sort_imports. groups. len( ) , 5 ) ;
1262- assert_eq ! ( sort_imports. groups[ 0 ] , vec![ "builtin" . to_string( ) ] ) ;
1263- assert_eq ! ( sort_imports. groups[ 1 ] , vec![ "external" . to_string( ) , "internal" . to_string( ) ] ) ;
1264- assert_eq ! ( sort_imports. groups[ 4 ] , vec![ "index" . to_string( ) ] ) ;
1295+ assert_eq ! (
1296+ sort_imports. groups[ 0 ] ,
1297+ vec![ GroupEntry :: Predefined ( GroupName :: parse( "builtin" ) . unwrap( ) ) ]
1298+ ) ;
1299+ assert_eq ! (
1300+ sort_imports. groups[ 1 ] ,
1301+ vec![
1302+ GroupEntry :: Predefined ( GroupName :: parse( "external" ) . unwrap( ) ) ,
1303+ GroupEntry :: Predefined ( GroupName :: parse( "internal" ) . unwrap( ) )
1304+ ]
1305+ ) ;
1306+ assert_eq ! (
1307+ sort_imports. groups[ 4 ] ,
1308+ vec![ GroupEntry :: Predefined ( GroupName :: parse( "index" ) . unwrap( ) ) ]
1309+ ) ;
12651310
12661311 // Test groups with newlinesBetween overrides
12671312 let config: FormatConfig = serde_json:: from_str (
@@ -1280,9 +1325,18 @@ mod tests {
12801325 let oxfmt_options = config. into_oxfmt_options ( ) . unwrap ( ) ;
12811326 let sort_imports = oxfmt_options. format_options . experimental_sort_imports . unwrap ( ) ;
12821327 assert_eq ! ( sort_imports. groups. len( ) , 3 ) ;
1283- assert_eq ! ( sort_imports. groups[ 0 ] , vec![ "builtin" . to_string( ) ] ) ;
1284- assert_eq ! ( sort_imports. groups[ 1 ] , vec![ "external" . to_string( ) ] ) ;
1285- assert_eq ! ( sort_imports. groups[ 2 ] , vec![ "parent" . to_string( ) ] ) ;
1328+ assert_eq ! (
1329+ sort_imports. groups[ 0 ] ,
1330+ vec![ GroupEntry :: Predefined ( GroupName :: parse( "builtin" ) . unwrap( ) ) ]
1331+ ) ;
1332+ assert_eq ! (
1333+ sort_imports. groups[ 1 ] ,
1334+ vec![ GroupEntry :: Predefined ( GroupName :: parse( "external" ) . unwrap( ) ) ]
1335+ ) ;
1336+ assert_eq ! (
1337+ sort_imports. groups[ 2 ] ,
1338+ vec![ GroupEntry :: Predefined ( GroupName :: parse( "parent" ) . unwrap( ) ) ]
1339+ ) ;
12861340 assert_eq ! ( sort_imports. newline_boundary_overrides. len( ) , 2 ) ;
12871341 assert_eq ! ( sort_imports. newline_boundary_overrides[ 0 ] , Some ( false ) ) ;
12881342 assert_eq ! ( sort_imports. newline_boundary_overrides[ 1 ] , None ) ;
0 commit comments