Skip to content

Commit 7c9c8f3

Browse files
committed
feat(formatter/sort_imports): Sort imports by Array<Array<string>> data
1 parent 04e4bb9 commit 7c9c8f3

File tree

6 files changed

+285
-65
lines changed

6 files changed

+285
-65
lines changed

crates/oxc_formatter/examples/sort_imports.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ fn main() -> Result<(), String> {
2828
sort_side_effects,
2929
ignore_case,
3030
newlines_between,
31+
groups: SortImports::default_groups(),
3132
};
3233

3334
// Read source file
@@ -50,7 +51,7 @@ fn main() -> Result<(), String> {
5051

5152
// Format the parsed code
5253
let options = FormatOptions {
53-
experimental_sort_imports: Some(sort_imports_options),
54+
experimental_sort_imports: Some(sort_imports_options.clone()),
5455
..Default::default()
5556
};
5657

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

Lines changed: 249 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl IntoIterator for ImportUnits {
2020
}
2121

2222
impl 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)]
200172
pub struct ImportMetadata<'a> {
@@ -209,28 +181,248 @@ pub struct ImportMetadata<'a> {
209181
}
210182

211183
impl 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)]
269465
pub enum ImportPathKind {
@@ -285,10 +481,7 @@ pub enum ImportPathKind {
285481

286482
impl 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

Comments
 (0)