@@ -20,7 +20,7 @@ impl IntoIterator for ImportUnits {
2020}
2121
2222impl ImportUnits {
23- pub fn sort_imports ( & mut self , elements : & [ FormatElement ] , options : options:: SortImports ) {
23+ pub fn sort_imports ( & mut self , elements : & [ FormatElement ] , options : & options:: SortImports ) {
2424 let imports_len = self . 0 . len ( ) ;
2525
2626 // Perform sorting only if needed
@@ -47,8 +47,11 @@ impl ImportUnits {
4747 let metadata_a = self . 0 [ a] . get_metadata ( elements) ;
4848 let metadata_b = self . 0 [ b] . get_metadata ( elements) ;
4949
50- // First, compare by group
51- let group_ord = metadata_a. group ( ) . cmp ( & metadata_b. group ( ) ) ;
50+ // First, compare by group index
51+ let group_idx_a = metadata_a. match_group ( & options. groups ) ;
52+ let group_idx_b = metadata_b. match_group ( & options. groups ) ;
53+
54+ let group_ord = group_idx_a. cmp ( & group_idx_b) ;
5255 if group_ord != std:: cmp:: Ordering :: Equal {
5356 return if options. order . is_desc ( ) { group_ord. reverse ( ) } else { group_ord } ;
5457 }
@@ -153,7 +156,7 @@ impl SortableImport {
153156 }
154157
155158 /// Check if this import should be ignored (not sorted).
156- pub fn is_ignored ( & self , options : options:: SortImports ) -> bool {
159+ pub fn is_ignored ( & self , options : & options:: SortImports ) -> bool {
157160 match self . import_line {
158161 SourceLine :: Import ( ImportLine { is_side_effect, .. } ) => {
159162 // TODO: Check ignore comments?
@@ -164,37 +167,6 @@ impl SortableImport {
164167 }
165168}
166169
167- /// Import group classification for sorting.
168- ///
169- /// NOTE: The order of variants in this enum determines the sort order when comparing groups.
170- /// Groups are sorted in the order they appear here (TypeImport first, Unknown last).
171- #[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord ) ]
172- pub enum ImportGroup {
173- /// Type-only imports from builtin or external packages
174- /// e.g., `import type { Foo } from 'react'`
175- TypeImport ,
176- /// Value imports from Node.js builtin modules or external packages
177- /// Corresponds to `['value-builtin', 'value-external']` in perfectionist
178- /// e.g., `import fs from 'node:fs'`, `import React from 'react'`
179- ValueBuiltinOrExternal ,
180- /// Type-only imports from internal modules
181- /// e.g., `import type { Config } from '~/types'`, `import type { User } from '@/models'`
182- TypeInternal ,
183- /// Value imports from internal modules
184- /// e.g., `import { config } from '~/config'`, `import { utils } from '@/utils'`
185- ValueInternal ,
186- /// Type-only imports from relative paths (parent, sibling, or index)
187- /// Corresponds to `['type-parent', 'type-sibling', 'type-index']` in perfectionist
188- /// e.g., `import type { Props } from '../types'`, `import type { State } from './types'`
189- TypeRelative ,
190- /// Value imports from relative paths (parent, sibling, or index)
191- /// Corresponds to `['value-parent', 'value-sibling', 'value-index']` in perfectionist
192- /// e.g., `import { helper } from '../parent'`, `import { Component } from './sibling'`
193- ValueRelative ,
194- /// Unclassified imports (fallback)
195- Unknown ,
196- }
197-
198170/// Metadata about an import for sorting purposes.
199171#[ derive( Debug , Clone ) ]
200172pub struct ImportMetadata < ' a > {
@@ -209,28 +181,248 @@ pub struct ImportMetadata<'a> {
209181}
210182
211183impl ImportMetadata < ' _ > {
212- /// Determine the import group based on metadata.
213- pub fn group ( & self ) -> ImportGroup {
214- if self . is_type_import {
215- return match self . path_kind {
216- ImportPathKind :: Builtin | ImportPathKind :: External => ImportGroup :: TypeImport ,
217- ImportPathKind :: Internal => ImportGroup :: TypeInternal ,
218- ImportPathKind :: Parent | ImportPathKind :: Sibling | ImportPathKind :: Index => {
219- ImportGroup :: TypeRelative
184+ /// Match this import against the configured groups and return the group index.
185+ /// Returns the index of the first matching group, or the index of "unknown" group if present,
186+ /// or the last index + 1 if no match found.
187+ ///
188+ /// Matching prioritizes more specific group names (e.g., "type-external" over "type-import").
189+ pub fn match_group ( & self , groups : & [ Vec < String > ] ) -> usize {
190+ let possible_names = self . generate_group_names ( ) ;
191+ let mut unknown_index = None ;
192+
193+ // Try each possible name in order (most specific first)
194+ for possible_name in & possible_names {
195+ for ( group_idx, group) in groups. iter ( ) . enumerate ( ) {
196+ for group_name in group {
197+ // Check if this is the "unknown" group
198+ if group_name == "unknown" {
199+ unknown_index = Some ( group_idx) ;
200+ }
201+
202+ // Check if this possible name matches this group
203+ if possible_name == group_name {
204+ return group_idx;
205+ }
220206 }
221- ImportPathKind :: Unknown => ImportGroup :: Unknown ,
222- } ;
207+ }
223208 }
224209
225- match self . path_kind {
226- ImportPathKind :: Builtin | ImportPathKind :: External => {
227- ImportGroup :: ValueBuiltinOrExternal
210+ // No match found - use "unknown" group if present, otherwise return last + 1
211+ unknown_index. unwrap_or ( groups. len ( ) )
212+ }
213+
214+ /// Generate all possible group names for this import, ordered by specificity.
215+ /// Returns group names in the format used by perfectionist.
216+ ///
217+ /// Perfectionist format examples:
218+ /// - `type-external` - type modifier + path selector
219+ /// - `value-internal` - value modifier + path selector
220+ /// - `type-import` - type modifier + import selector
221+ /// - `external` - path selector only
222+ fn generate_group_names ( & self ) -> Vec < String > {
223+ let selectors = self . selectors ( ) ;
224+ let modifiers = self . modifiers ( ) ;
225+
226+ let mut group_names = Vec :: new ( ) ;
227+
228+ // Most specific: type/value modifier combined with path selectors
229+ // e.g., "type-external", "value-internal", "type-parent"
230+ let type_or_value_modifier = if self . is_type_import { "type" } else { "value" } ;
231+
232+ for selector in & selectors {
233+ // Skip the generic "type" selector since it's already in the modifier
234+ if matches ! ( selector, ImportSelector :: Type ) {
235+ continue ;
236+ }
237+
238+ // For path-based selectors, combine with type/value modifier
239+ if matches ! (
240+ selector,
241+ ImportSelector :: Builtin
242+ | ImportSelector :: External
243+ | ImportSelector :: Internal
244+ | ImportSelector :: Parent
245+ | ImportSelector :: Sibling
246+ | ImportSelector :: Index
247+ ) {
248+ let name = format ! ( "{}-{}" , type_or_value_modifier, selector. as_str( ) ) ;
249+ group_names. push ( name) ;
250+ }
251+ }
252+
253+ // Add other modifier combinations for special selectors
254+ for selector in & selectors {
255+ // Skip path-based selectors (already handled above) and "import" selector
256+ if matches ! (
257+ selector,
258+ ImportSelector :: Builtin
259+ | ImportSelector :: External
260+ | ImportSelector :: Internal
261+ | ImportSelector :: Parent
262+ | ImportSelector :: Sibling
263+ | ImportSelector :: Index
264+ | ImportSelector :: Import
265+ | ImportSelector :: Type
266+ ) {
267+ continue ;
228268 }
229- ImportPathKind :: Internal => ImportGroup :: ValueInternal ,
230- ImportPathKind :: Parent | ImportPathKind :: Sibling | ImportPathKind :: Index => {
231- ImportGroup :: ValueRelative
269+
270+ // For special selectors like side-effect, side-effect-style, style
271+ // combine with relevant modifiers
272+ for modifier in & modifiers {
273+ let name = format ! ( "{}-{}" , modifier. as_str( ) , selector. as_str( ) ) ;
274+ group_names. push ( name) ;
232275 }
233- ImportPathKind :: Unknown => ImportGroup :: Unknown ,
276+
277+ // Selector-only name
278+ group_names. push ( selector. as_str ( ) . to_string ( ) ) ;
279+ }
280+
281+ // Add "type-import" or "value-import" or just "import"
282+ if self . is_type_import {
283+ group_names. push ( "type-import" . to_string ( ) ) ;
284+ }
285+
286+ group_names. push ( "import" . to_string ( ) ) ;
287+
288+ group_names
289+ }
290+
291+ /// Compute all selectors for this import, ordered from most to least specific.
292+ fn selectors ( & self ) -> Vec < ImportSelector > {
293+ let mut selectors = Vec :: new ( ) ;
294+
295+ // Most specific selectors first
296+ if self . is_side_effect && self . is_style_import {
297+ selectors. push ( ImportSelector :: SideEffectStyle ) ;
298+ }
299+ if self . is_side_effect {
300+ selectors. push ( ImportSelector :: SideEffect ) ;
301+ }
302+ if self . is_style_import {
303+ selectors. push ( ImportSelector :: Style ) ;
304+ }
305+ // Type selector
306+ if self . is_type_import {
307+ selectors. push ( ImportSelector :: Type ) ;
308+ }
309+ // Path-based selectors
310+ match self . path_kind {
311+ ImportPathKind :: Index => selectors. push ( ImportSelector :: Index ) ,
312+ ImportPathKind :: Sibling => selectors. push ( ImportSelector :: Sibling ) ,
313+ ImportPathKind :: Parent => selectors. push ( ImportSelector :: Parent ) ,
314+ ImportPathKind :: Internal => selectors. push ( ImportSelector :: Internal ) ,
315+ ImportPathKind :: Builtin => selectors. push ( ImportSelector :: Builtin ) ,
316+ ImportPathKind :: External => selectors. push ( ImportSelector :: External ) ,
317+ ImportPathKind :: Unknown => { }
318+ }
319+ // Catch-all selector
320+ selectors. push ( ImportSelector :: Import ) ;
321+
322+ selectors
323+ }
324+
325+ /// Compute all modifiers for this import.
326+ fn modifiers ( & self ) -> Vec < ImportModifier > {
327+ let mut modifiers = Vec :: new ( ) ;
328+
329+ if self . is_side_effect {
330+ modifiers. push ( ImportModifier :: SideEffect ) ;
331+ }
332+ if self . is_type_import {
333+ modifiers. push ( ImportModifier :: Type ) ;
334+ } else {
335+ modifiers. push ( ImportModifier :: Value ) ;
336+ }
337+ if self . has_default_specifier {
338+ modifiers. push ( ImportModifier :: Default ) ;
339+ }
340+ if self . has_namespace_specifier {
341+ modifiers. push ( ImportModifier :: Wildcard ) ;
342+ }
343+ if self . has_named_specifier {
344+ modifiers. push ( ImportModifier :: Named ) ;
345+ }
346+
347+ modifiers
348+ }
349+ }
350+
351+ /// Selector types for import categorization.
352+ /// Selectors identify the type or location of an import.
353+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
354+ enum ImportSelector {
355+ /// Type-only imports (`import type { ... }`)
356+ Type ,
357+ /// Side-effect style imports (CSS, SCSS, etc. without bindings)
358+ SideEffectStyle ,
359+ /// Side-effect imports (imports without bindings)
360+ SideEffect ,
361+ /// Style file imports (CSS, SCSS, etc.)
362+ Style ,
363+ /// Index file imports (`./`, `../`)
364+ Index ,
365+ /// Sibling module imports (`./foo`)
366+ Sibling ,
367+ /// Parent module imports (`../foo`)
368+ Parent ,
369+ /// Internal module imports (matching internal patterns like `~/`, `@/`)
370+ Internal ,
371+ /// Built-in module imports (`node:fs`, `fs`)
372+ Builtin ,
373+ /// External module imports (from node_modules)
374+ External ,
375+ /// Catch-all selector
376+ Import ,
377+ }
378+
379+ impl ImportSelector {
380+ /// Returns the string representation used in group names.
381+ const fn as_str ( self ) -> & ' static str {
382+ match self {
383+ Self :: Type => "type" ,
384+ Self :: SideEffectStyle => "side-effect-style" ,
385+ Self :: SideEffect => "side-effect" ,
386+ Self :: Style => "style" ,
387+ Self :: Index => "index" ,
388+ Self :: Sibling => "sibling" ,
389+ Self :: Parent => "parent" ,
390+ Self :: Internal => "internal" ,
391+ Self :: Builtin => "builtin" ,
392+ Self :: External => "external" ,
393+ Self :: Import => "import" ,
394+ }
395+ }
396+ }
397+
398+ /// Modifier types for import categorization.
399+ /// Modifiers describe characteristics of how an import is declared.
400+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
401+ enum ImportModifier {
402+ /// Side-effect imports
403+ SideEffect ,
404+ /// Type-only imports
405+ Type ,
406+ /// Value imports (non-type)
407+ Value ,
408+ /// Default specifier present
409+ Default ,
410+ /// Namespace/wildcard specifier present (`* as`)
411+ Wildcard ,
412+ /// Named specifiers present
413+ Named ,
414+ }
415+
416+ impl ImportModifier {
417+ /// Returns the string representation used in group names.
418+ const fn as_str ( self ) -> & ' static str {
419+ match self {
420+ Self :: SideEffect => "side-effect" ,
421+ Self :: Type => "type" ,
422+ Self :: Value => "value" ,
423+ Self :: Default => "default" ,
424+ Self :: Wildcard => "wildcard" ,
425+ Self :: Named => "named" ,
234426 }
235427 }
236428}
@@ -264,6 +456,10 @@ static NODE_BUILTINS: phf::Set<&'static str> = phf_set! {
264456 "zlib" ,
265457} ;
266458
459+ fn is_builtin ( source : & str ) -> bool {
460+ source. starts_with ( "node:" ) || source. starts_with ( "bun:" ) || NODE_BUILTINS . contains ( source)
461+ }
462+
267463/// Classification of import path types for grouping.
268464#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
269465pub enum ImportPathKind {
@@ -285,10 +481,7 @@ pub enum ImportPathKind {
285481
286482impl ImportPathKind {
287483 fn new ( source : & str ) -> Self {
288- if source. starts_with ( "node:" )
289- || source. starts_with ( "bun:" )
290- || NODE_BUILTINS . contains ( source)
291- {
484+ if is_builtin ( source) {
292485 return Self :: Builtin ;
293486 }
294487
0 commit comments