@@ -13,6 +13,7 @@ pub struct SortableImport<'a> {
1313 pub import_line : SourceLine ,
1414 pub group_idx : usize ,
1515 pub normalized_source : Cow < ' a , str > ,
16+ pub is_side_effect : bool ,
1617 pub is_ignored : bool ,
1718}
1819
@@ -24,6 +25,7 @@ impl<'a> SortableImport<'a> {
2425 // These will be computed by `collect_sort_keys()`
2526 group_idx : 0 ,
2627 normalized_source : Cow :: Borrowed ( "" ) ,
28+ is_side_effect : false ,
2729 is_ignored : false ,
2830 }
2931 }
@@ -68,15 +70,43 @@ impl<'a> SortableImport<'a> {
6870 } ;
6971 self . group_idx = matcher. match_group ( & options. groups ) ;
7072
71- // TODO: Check ignore comments?
72- self . is_ignored = !options. sort_side_effects && * is_side_effect;
73+ // Store side-effect flag for use in sorting
74+ self . is_side_effect = * is_side_effect;
75+
76+ // Determine if this import should be ignored (not moved between groups)
77+ // Based on perfectionist's logic:
78+ // - If sortSideEffects is true, never ignore
79+ // - If sortSideEffects is false and this is a side-effect:
80+ // - Check if groups contain 'side-effect' or 'side-effect-style'
81+ // - If yes, allow regrouping (not ignored)
82+ // - If no, keep in original position (ignored)
83+ let is_style_import = matcher. is_style_import ;
84+ let should_regroup_side_effect = has_side_effect_group ( & options. groups ) ;
85+ let should_regroup_side_effect_style = has_side_effect_style_group ( & options. groups ) ;
86+
87+ self . is_ignored = !options. sort_side_effects
88+ && * is_side_effect
89+ && !should_regroup_side_effect
90+ && ( !is_style_import || !should_regroup_side_effect_style) ;
7391
7492 self
7593 }
7694}
7795
7896// ---
7997
98+ /// Check if groups contain 'side-effect' group.
99+ fn has_side_effect_group ( groups : & [ Vec < String > ] ) -> bool {
100+ groups. iter ( ) . any ( |group| group. iter ( ) . any ( |name| name == "side-effect" ) )
101+ }
102+
103+ /// Check if groups contain 'side-effect-style' group.
104+ fn has_side_effect_style_group ( groups : & [ Vec < String > ] ) -> bool {
105+ groups. iter ( ) . any ( |group| group. iter ( ) . any ( |name| name == "side-effect-style" ) )
106+ }
107+
108+ // ---
109+
80110/// Helper for matching imports to configured groups.
81111///
82112/// Contains all characteristics of an import needed to determine which group it belongs to,
@@ -126,81 +156,90 @@ impl ImportGroupMatcher {
126156 /// Generate all possible group names for this import, ordered by specificity.
127157 /// Returns group names in the format used by perfectionist.
128158 ///
129- /// Perfectionist format examples:
130- /// - `type-external` - type modifier + path selector
131- /// - `value-internal` - value modifier + path selector
132- /// - `type-import` - type modifier + import selector
133- /// - `external` - path selector only
159+ /// This matches perfectionist's `generatePredefinedGroups` logic:
160+ /// For each selector (in order), generate all modifier combinations with that selector.
161+ ///
162+ /// Example with selectors=`['style', 'parent']` and modifiers=`['value', 'default']`:
163+ /// - value-default-style, value-style, default-style, style
164+ /// - value-default-parent, value-parent, default-parent, parent
134165 fn generate_group_names ( & self ) -> Vec < String > {
135166 let selectors = self . selectors ( ) ;
136167 let modifiers = self . modifiers ( ) ;
137168
138169 let mut group_names = Vec :: new ( ) ;
139170
140- // Most specific: type/value modifier combined with path selectors
141- // e.g., "type-external", "value-internal", "type-parent"
142- let type_or_value_modifier = if self . is_type_import { "type" } else { "value" } ;
143-
171+ // For each selector, generate all modifier combinations
144172 for selector in & selectors {
145- // Skip the generic "type " selector since it's already in the modifier
146- if matches ! ( selector, ImportSelector :: Type ) {
173+ // Skip "import " selector for now - it's handled specially at the end
174+ if matches ! ( selector, ImportSelector :: Import ) {
147175 continue ;
148176 }
149177
150- // For path-based selectors, combine with type/value modifier
151- if matches ! (
178+ // Path-type selectors are standalone (e.g., "parent- type", "external-type")
179+ let is_path_type_selector = matches ! (
152180 selector,
153- ImportSelector :: Builtin
154- | ImportSelector :: External
155- | ImportSelector :: Internal
156- | ImportSelector :: Parent
157- | ImportSelector :: Sibling
158- | ImportSelector :: Index
159- ) {
160- let name = format ! ( "{}-{}" , type_or_value_modifier, selector. as_str( ) ) ;
161- group_names. push ( name) ;
162- }
163- }
164-
165- // Add other modifier combinations for special selectors
166- for selector in & selectors {
167- // Skip path-based selectors (already handled above) and "import" selector
168- if matches ! (
181+ ImportSelector :: BuiltinType
182+ | ImportSelector :: ExternalType
183+ | ImportSelector :: InternalType
184+ | ImportSelector :: ParentType
185+ | ImportSelector :: SiblingType
186+ | ImportSelector :: IndexType
187+ ) ;
188+
189+ // For path-based selectors, use type/value as the primary modifier
190+ let is_path_selector = matches ! (
169191 selector,
170192 ImportSelector :: Builtin
171193 | ImportSelector :: External
172194 | ImportSelector :: Internal
173195 | ImportSelector :: Parent
174196 | ImportSelector :: Sibling
175197 | ImportSelector :: Index
176- | ImportSelector :: Import
177- | ImportSelector :: Type
178- ) {
179- continue ;
180- }
181-
182- // For special selectors like side-effect, side-effect-style, style
183- // combine with relevant modifiers
184- for modifier in & modifiers {
185- let name = format ! ( "{}-{}" , modifier. as_str( ) , selector. as_str( ) ) ;
198+ ) ;
199+
200+ if is_path_type_selector {
201+ // Path-type selectors are already in the format we want (e.g., "parent-type")
202+ group_names. push ( selector. as_str ( ) . to_string ( ) ) ;
203+ } else if matches ! ( selector, ImportSelector :: Type ) {
204+ // Type selector: just add "type"
205+ group_names. push ( "type" . to_string ( ) ) ;
206+ } else if is_path_selector {
207+ // For path selectors, combine with type/value modifier
208+ let type_or_value = if self . is_type_import { "type" } else { "value" } ;
209+ let name = format ! ( "{}-{}" , type_or_value, selector. as_str( ) ) ;
186210 group_names. push ( name) ;
211+ // Also add bare selector name
212+ group_names. push ( selector. as_str ( ) . to_string ( ) ) ;
213+ } else {
214+ // For special selectors (side-effect, style, etc.), combine with all modifiers
215+ for modifier in & modifiers {
216+ let name = format ! ( "{}-{}" , modifier. as_str( ) , selector. as_str( ) ) ;
217+ group_names. push ( name) ;
218+ }
219+ // Then add bare selector name
220+ group_names. push ( selector. as_str ( ) . to_string ( ) ) ;
187221 }
188-
189- // Selector-only name
190- group_names. push ( selector. as_str ( ) . to_string ( ) ) ;
191222 }
192223
193- // Add "type-import" or "value-import" or just "import"
194- if self . is_type_import {
195- group_names. push ( "type-import" . to_string ( ) ) ;
224+ // Add final "import" catch-all with modifiers
225+ // This generates combinations like "side-effect-import", "type-import", "value-import", etc.
226+ for modifier in & modifiers {
227+ let name = format ! ( "{}-import" , modifier. as_str( ) ) ;
228+ group_names. push ( name) ;
196229 }
197-
198230 group_names. push ( "import" . to_string ( ) ) ;
199231
200232 group_names
201233 }
202234
203235 /// Compute all selectors for this import, ordered from most to least specific.
236+ ///
237+ /// Order matches perfectionist implementation:
238+ /// 1. Special selectors (side-effect-style, side-effect, style) - most specific
239+ /// 2. Path-type selectors (parent-type, external-type, etc.) for type imports
240+ /// 3. Type selector
241+ /// 4. Path-based selectors (builtin, external, internal, parent, sibling, index)
242+ /// 5. Catch-all import selector
204243 fn selectors ( & self ) -> Vec < ImportSelector > {
205244 let mut selectors = Vec :: new ( ) ;
206245
@@ -214,10 +253,26 @@ impl ImportGroupMatcher {
214253 if self . is_style_import {
215254 selectors. push ( ImportSelector :: Style ) ;
216255 }
256+
257+ // For type imports, add path-type selectors (e.g., "parent-type", "external-type")
258+ // These come before the generic "type" selector
259+ if self . is_type_import {
260+ match self . path_kind {
261+ ImportPathKind :: Index => selectors. push ( ImportSelector :: IndexType ) ,
262+ ImportPathKind :: Sibling => selectors. push ( ImportSelector :: SiblingType ) ,
263+ ImportPathKind :: Parent => selectors. push ( ImportSelector :: ParentType ) ,
264+ ImportPathKind :: Internal => selectors. push ( ImportSelector :: InternalType ) ,
265+ ImportPathKind :: Builtin => selectors. push ( ImportSelector :: BuiltinType ) ,
266+ ImportPathKind :: External => selectors. push ( ImportSelector :: ExternalType ) ,
267+ ImportPathKind :: Unknown => { }
268+ }
269+ }
270+
217271 // Type selector
218272 if self . is_type_import {
219273 selectors. push ( ImportSelector :: Type ) ;
220274 }
275+
221276 // Path-based selectors
222277 match self . path_kind {
223278 ImportPathKind :: Index => selectors. push ( ImportSelector :: Index ) ,
@@ -272,6 +327,18 @@ enum ImportSelector {
272327 SideEffect ,
273328 /// Style file imports (CSS, SCSS, etc.)
274329 Style ,
330+ /// Type import from index file
331+ IndexType ,
332+ /// Type import from sibling module
333+ SiblingType ,
334+ /// Type import from parent module
335+ ParentType ,
336+ /// Type import from internal module
337+ InternalType ,
338+ /// Type import from built-in module
339+ BuiltinType ,
340+ /// Type import from external module
341+ ExternalType ,
275342 /// Index file imports (`./`, `../`)
276343 Index ,
277344 /// Sibling module imports (`./foo`)
@@ -296,6 +363,12 @@ impl ImportSelector {
296363 Self :: SideEffectStyle => "side-effect-style" ,
297364 Self :: SideEffect => "side-effect" ,
298365 Self :: Style => "style" ,
366+ Self :: IndexType => "index-type" ,
367+ Self :: SiblingType => "sibling-type" ,
368+ Self :: ParentType => "parent-type" ,
369+ Self :: InternalType => "internal-type" ,
370+ Self :: BuiltinType => "builtin-type" ,
371+ Self :: ExternalType => "external-type" ,
299372 Self :: Index => "index" ,
300373 Self :: Sibling => "sibling" ,
301374 Self :: Parent => "parent" ,
@@ -416,7 +489,8 @@ fn to_path_kind(source: &str) -> ImportPathKind {
416489 }
417490
418491 if source. starts_with ( '.' ) {
419- if source == "." || source == ".." || source. ends_with ( '/' ) {
492+ // Check for index file imports
493+ if is_index_import ( source) {
420494 return ImportPathKind :: Index ;
421495 }
422496 if source. starts_with ( "../" ) {
@@ -432,3 +506,17 @@ fn to_path_kind(source: &str) -> ImportPathKind {
432506
433507 ImportPathKind :: External
434508}
509+
510+ /// Check if an import is an index file import.
511+ fn is_index_import ( source : & str ) -> bool {
512+ matches ! (
513+ source,
514+ "." | "./"
515+ | ".."
516+ | "./index"
517+ | "./index.js"
518+ | "./index.ts"
519+ | "./index.d.ts"
520+ | "./index.d.js"
521+ ) || source. ends_with ( '/' )
522+ }
0 commit comments