Skip to content

Commit fb4ce3f

Browse files
committed
feat(formatter/sort-imports): Support options.groups
1 parent ab00462 commit fb4ce3f

File tree

8 files changed

+872
-816
lines changed

8 files changed

+872
-816
lines changed

crates/oxc_formatter/src/ir_transform/sort_imports/import_unit.rs

Lines changed: 137 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)