diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs index 2df683313d971..863c46756df62 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs @@ -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, @@ -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) { diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs index 8d22a389b50a6..bf53e276ab774 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs @@ -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)