From ef8e1946a4cd5c71919b78e767d01d7dfeeae142 Mon Sep 17 00:00:00 2001 From: liziye627-design <232619260+liziye627-design@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:18:04 +0800 Subject: [PATCH] fix(formatter): keep a block comment before its comma when wrapping lists --- .../emmylua_formatter/src/formatter/expr.rs | 104 ++++++++++++++---- .../src/test/expression_tests.rs | 44 ++++++++ 2 files changed, 124 insertions(+), 24 deletions(-) diff --git a/crates/emmylua_formatter/src/formatter/expr.rs b/crates/emmylua_formatter/src/formatter/expr.rs index 909ed35fb..223c3f073 100644 --- a/crates/emmylua_formatter/src/formatter/expr.rs +++ b/crates/emmylua_formatter/src/formatter/expr.rs @@ -1238,10 +1238,13 @@ fn collect_call_arg_entries( } if let Some(arg) = LuaExpr::cast(child) { - let trailing_comment = extract_trailing_comment(ctx, plan, arg.syntax()); - if trailing_comment.is_some() { + let extracted = extract_trailing_comment(ctx, plan, arg.syntax()); + if let Some(comment) = &extracted { collected.has_comments = true; + collected.consumed_comment_ranges.push(comment.range); } + let mut doc = format_expr(ctx, plan, &arg); + let trailing_comment = attach_comment_before_separator(&mut doc, extracted); let trailing_align_hint = trailing_comment.as_ref().is_some_and(|(_, range)| { trailing_gap_requests_alignment( arg.syntax(), @@ -1249,12 +1252,9 @@ fn collect_call_arg_entries( ctx.config.comments.line_comment_min_spaces_before.max(1), ) }); - if let Some((_, range)) = &trailing_comment { - collected.consumed_comment_ranges.push(*range); - } collected.entries.push(CallArgEntry { leading_comments: std::mem::take(&mut pending_comments), - doc: format_expr(ctx, plan, &arg), + doc, trailing_comment: trailing_comment.map(|(docs, _)| docs), trailing_align_hint, }); @@ -1784,16 +1784,16 @@ fn collect_table_entries( if let Some(field) = LuaTableField::cast(child) { let blank_lines_before = count_blank_lines_before(field.syntax()); - let trailing_comment = extract_trailing_comment(ctx, plan, field.syntax()); - if trailing_comment.is_some() { + let extracted = extract_trailing_comment(ctx, plan, field.syntax()); + if let Some(comment) = &extracted { collected.has_comments = true; + collected.consumed_comment_ranges.push(comment.range); } if blank_lines_before > 0 { collected.has_comments = true; } - if let Some((_, range)) = &trailing_comment { - collected.consumed_comment_ranges.push(*range); - } + let mut doc = format_table_field_ir(ctx, plan, &field); + let trailing_comment = attach_comment_before_separator(&mut doc, extracted); let comment_align_hint = trailing_comment.as_ref().is_some_and(|(_, range)| { trailing_gap_requests_alignment( field.syntax(), @@ -1805,7 +1805,7 @@ fn collect_table_entries( field: field.clone(), leading_comments: std::mem::take(&mut pending_comments), blank_lines_before, - doc: format_table_field_ir(ctx, plan, &field), + doc, eq_split: None, align_hint: field_requests_alignment(&field), comment_align_hint, @@ -3712,11 +3712,35 @@ where .collect() } +/// A trailing comment found after a list item, and whether it sits before the +/// item's separator (e.g. `a --[[@as T]],`). Such a comment must stay ahead of +/// the separator, otherwise it would attach to the next item. +struct ExtractedTrailingComment { + docs: Vec, + range: TextRange, + before_separator: bool, +} + +/// Keeps a comment that precedes the separator inline in `doc`, and returns +/// any other trailing comment for the caller to place after the separator. +fn attach_comment_before_separator( + doc: &mut Vec, + comment: Option, +) -> Option<(Vec, TextRange)> { + let comment = comment?; + if comment.before_separator { + doc.push(ir::space()); + doc.extend(comment.docs); + return None; + } + Some((comment.docs, comment.range)) +} + fn extract_trailing_comment( ctx: &FormatContext, plan: &FormatPlan, node: &LuaSyntaxNode, -) -> Option<(Vec, TextRange)> { +) -> Option { for child in node.children() { if child.kind() != LuaKind::Syntax(LuaSyntaxKind::Comment) { continue; @@ -3732,29 +3756,35 @@ fn extract_trailing_comment( return None; } - return Some(( - render::render_comment_with_spacing(ctx, &comment, plan), - child.text_range(), - )); + return Some(ExtractedTrailingComment { + docs: render::render_comment_with_spacing(ctx, &comment, plan), + range: child.text_range(), + before_separator: is_block_comment(&child) && separator_follows(node), + }); } + let mut seen_separator = false; let mut next = node.next_sibling_or_token(); for _ in 0..4 { let sibling = next.as_ref()?; match sibling.kind() { - LuaKind::Token(LuaTokenKind::TkWhitespace) - | LuaKind::Token(LuaTokenKind::TkSemicolon) - | LuaKind::Token(LuaTokenKind::TkComma) => {} + LuaKind::Token(LuaTokenKind::TkWhitespace) => {} + LuaKind::Token(LuaTokenKind::TkSemicolon) | LuaKind::Token(LuaTokenKind::TkComma) => { + seen_separator = true; + } LuaKind::Syntax(LuaSyntaxKind::Comment) => { let comment_node = sibling.as_node()?; if comment_node.text().contains_char('\n') { return None; } let comment = LuaComment::cast(comment_node.clone())?; - return Some(( - render::render_comment_with_spacing(ctx, &comment, plan), - comment_node.text_range(), - )); + return Some(ExtractedTrailingComment { + docs: render::render_comment_with_spacing(ctx, &comment, plan), + range: comment_node.text_range(), + before_separator: !seen_separator + && is_block_comment(comment_node) + && separator_follows(comment_node), + }); } _ => return None, } @@ -3764,6 +3794,32 @@ fn extract_trailing_comment( None } +/// `--[[ ... ]]` / `--[==[ ... ]==]`, which can sit before a separator on the +/// same line, unlike a `--` line comment. +fn is_block_comment(node: &LuaSyntaxNode) -> bool { + let text = node.text().to_string(); + let Some(rest) = text.strip_prefix("--[") else { + return false; + }; + rest.trim_start_matches('=').starts_with('[') +} + +/// Whether a `,` or `;` follows `node` on the same line before any other item. +fn separator_follows(node: &LuaSyntaxNode) -> bool { + let mut next = node.next_sibling_or_token(); + while let Some(sibling) = next { + match sibling.kind() { + LuaKind::Token(LuaTokenKind::TkWhitespace) => {} + LuaKind::Token(LuaTokenKind::TkSemicolon) | LuaKind::Token(LuaTokenKind::TkComma) => { + return true; + } + _ => return false, + } + next = sibling.next_sibling_or_token(); + } + false +} + fn extract_trailing_comment_text(node: &LuaSyntaxNode) -> Option<(Vec, TextRange)> { let mut next = node.next_sibling_or_token(); for _ in 0..4 { diff --git a/crates/emmylua_formatter/src/test/expression_tests.rs b/crates/emmylua_formatter/src/test/expression_tests.rs index 90b094430..5f85bb093 100644 --- a/crates/emmylua_formatter/src/test/expression_tests.rs +++ b/crates/emmylua_formatter/src/test/expression_tests.rs @@ -609,6 +609,50 @@ a=1, ); } + // Issue #1300: a block comment before the comma (e.g. an `--[[@as T]]` + // cast) must stay before the comma, or it attaches to the next item. + #[test] + fn test_block_comment_before_comma_stays_on_call_arg() { + assert_format!( + "g(a --[[@as T]], b)\n", + r#" +g( + a --[[@as T]], + b +) +"# + ); + } + + #[test] + fn test_block_comment_before_comma_stays_on_table_field() { + assert_format!( + "local t = { path = x --[[@as string]], other = 1 }\n", + r#" +local t = { + path = x --[[@as string]], + other = 1 +} +"# + ); + } + + #[test] + fn test_block_comment_before_comma_is_idempotent() { + use emmylua_parser::LuaLanguageLevel; + + let config = crate::config::LuaFormatConfig::default(); + for source in [ + "g(a --[[@as T]], b)\n", + "local t = { path = x --[[@as string]], other = 1 }\n", + "g(a, b --[[@as T]])\n", + ] { + let first = crate::format_text(source, LuaLanguageLevel::default(), &config).formatted; + let second = crate::format_text(&first, LuaLanguageLevel::default(), &config).formatted; + assert_eq!(second, first, "not idempotent for {source:?}"); + } + } + #[test] fn test_call_expr_formats_after_open_comment_attachment() { assert_format!(