Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ mod source_line;
use oxc_allocator::{Allocator, Vec as ArenaVec};

use crate::{
SortImportsOptions,
formatter::format_element::{FormatElement, LineMode, document::Document},
JsLabels, SortImportsOptions,
formatter::format_element::{
FormatElement, LineMode,
document::Document,
tag::{LabelId, Tag},
},
ir_transform::sort_imports::{
group_config::parse_groups_from_strings, partitioned_chunk::PartitionedChunk,
source_line::SourceLine,
Expand Down Expand Up @@ -65,19 +69,54 @@ impl SortImportsTransform {
// - If this is the case, we should check `Tag::StartLabelled(JsLabels::ImportDeclaration)`
let mut lines = vec![];
let mut current_line_start = 0;
// Track if we're inside a multiline block comment (started with `/*` but not yet closed with `*/`)
let mut in_multiline_comment = false;
// Track if current line is a standalone multiline comment (no import on same line)
let mut is_standalone_multiline_comment = false;

for (idx, el) in prev_elements.iter().enumerate() {
// Check for multiline comment boundaries
if let FormatElement::Text { text, .. } = el {
if !in_multiline_comment && text.starts_with("/*") && !text.ends_with("*/") {
in_multiline_comment = true;
is_standalone_multiline_comment = true;
} else if in_multiline_comment && text.ends_with("*/") {
in_multiline_comment = false;
}
}

// An import on the same line means the comment is attached to it, not standalone
if let FormatElement::Tag(Tag::StartLabelled(id)) = el
&& *id == LabelId::of(JsLabels::ImportDeclaration)
{
is_standalone_multiline_comment = false;
}

if let FormatElement::Line(mode) = el
&& matches!(mode, LineMode::Empty | LineMode::Hard)
{
// If we're inside a multiline comment, don't flush the line yet.
// Wait until the comment is closed so the entire comment is treated as one line.
if in_multiline_comment {
continue;
}

// Flush current line
if current_line_start < idx {
lines.push(SourceLine::from_element_range(
prev_elements,
current_line_start..idx,
*mode,
));
let line = if is_standalone_multiline_comment {
// Standalone multiline comment: directly create CommentOnly
SourceLine::CommentOnly(current_line_start..idx, *mode)
} else {
SourceLine::from_element_range(
prev_elements,
current_line_start..idx,
*mode,
)
};
lines.push(line);
}
current_line_start = idx + 1;
is_standalone_multiline_comment = false;

// We need this explicitly to detect boundaries later.
if matches!(mode, LineMode::Empty) {
Expand Down
129 changes: 129 additions & 0 deletions crates/oxc_formatter/tests/ir_transform/sort_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,135 @@ import { t } from "t";
);
}

#[test]
fn should_sort_with_multiline_comments_attached_to_each_import() {
assert_format(
r#"
/*
* hi
*/
import cn from "classnames"
import type { Hello } from "pkg"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
import type { Hello } from "pkg";

/*
* hi
*/
import cn from "classnames";
"#,
);
assert_format(
r#"
/*
* hi
*/
import cn from "classnames"
import { Hello } from "pkg"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
/*
* hi
*/
import cn from "classnames";
import { Hello } from "pkg";
"#,
);
// Each multiline comment attached to its own import
assert_format(
r#"
/*
* for b
*/
import b from "b"
/*
* for a
*/
import a from "a"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
/*
* for a
*/
import a from "a";
/*
* for b
*/
import b from "b";
"#,
);

// Consecutive multiline comments before imports
// Comments are attached to the immediately following import (import b)
assert_format(
r#"
/*
* comment1
*/
/*
* comment2
*/
import b from "b"
import a from "a"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
import a from "a";
/*
* comment1
*/
/*
* comment2
*/
import b from "b";
"#,
);

// Multiline comment separated by empty line (partitioned)
assert_format(
r#"
/*
* comment
*/

import b from "b"
import a from "a"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
/*
* comment
*/

import a from "a";
import b from "b";
"#,
);

// Mix of single-line and multiline block comments
assert_format(
r#"
/* single */ import b from "b"
/*
* multiline
*/
import a from "a"
"#,
r#"{ "experimentalSortImports": {} }"#,
r#"
/*
* multiline
*/
import a from "a";
/* single */ import b from "b";
"#,
);
}

#[test]
fn should_support_newlines_between_option() {
// Test newlines_between: false (no blank lines between groups)
Expand Down
Loading