From e06c93e8d893204f4eedc2b30d45dfac994f440a Mon Sep 17 00:00:00 2001 From: Dunqing Date: Fri, 26 Sep 2025 19:55:20 +0800 Subject: [PATCH 1/4] fix(formatter): correct comments checking for some places --- .../oxc_formatter/src/formatter/comments.rs | 8 ++-- .../src/formatter/source_text.rs | 27 ++++++++---- .../oxc_formatter/src/generated/ast_nodes.rs | 2 +- .../src/utils/assignment_like.rs | 44 +++++++++---------- crates/oxc_formatter/src/utils/conditional.rs | 6 +-- crates/oxc_formatter/src/utils/typecast.rs | 40 +++++++++++------ .../oxc_formatter/src/write/call_arguments.rs | 6 ++- .../src/write/member_expression.rs | 37 +++++++++++++++- crates/oxc_formatter/src/write/mod.rs | 23 +++++----- crates/oxc_formatter/src/write/parameters.rs | 17 ++++--- crates/oxc_formatter/src/write/program.rs | 17 ++----- .../src/write/return_or_throw_statement.rs | 6 +-- .../oxc_formatter/src/write/try_statement.rs | 35 ++++++++++----- .../src/generators/formatter/ast_nodes.rs | 9 +++- tasks/coverage/snapshots/formatter_babel.snap | 6 +-- .../snapshots/formatter_typescript.snap | 6 +-- 16 files changed, 174 insertions(+), 115 deletions(-) diff --git a/crates/oxc_formatter/src/formatter/comments.rs b/crates/oxc_formatter/src/formatter/comments.rs index f8c2f7f318f3e..248d24db68be9 100644 --- a/crates/oxc_formatter/src/formatter/comments.rs +++ b/crates/oxc_formatter/src/formatter/comments.rs @@ -236,7 +236,7 @@ impl<'a> Comments<'a> { /// Checks if there are any comments between the given positions. pub fn has_comment_in_range(&self, start: u32, end: u32) -> bool { - self.comments_before_iter(end).any(|comment| comment.span.end >= start) + self.comments_before_iter(end).any(|comment| comment.span.end > start) } /// Checks if there are any comments within the given span. @@ -253,10 +253,8 @@ impl<'a> Comments<'a> { /// Checks if there are any leading own-line comments before the given position. pub fn has_leading_own_line_comment(&self, start: u32) -> bool { - self.comments_before_iter(start).any(|comment| { - self.source_text.is_own_line_comment(comment) - || self.source_text.lines_after(comment.span.end) > 0 - }) + self.comments_before_iter(start) + .any(|comment| self.source_text.lines_after(comment.span.end) > 0) } /// Checks if there are leading or trailing comments around `current_span`. diff --git a/crates/oxc_formatter/src/formatter/source_text.rs b/crates/oxc_formatter/src/formatter/source_text.rs index 93f44820ca26b..8cf8ffdb9ecf6 100644 --- a/crates/oxc_formatter/src/formatter/source_text.rs +++ b/crates/oxc_formatter/src/formatter/source_text.rs @@ -129,13 +129,24 @@ impl<'a> SourceText<'a> { self.text_for(&span).chars().count() } - /// Count consecutive line breaks after position + /// Count consecutive line breaks after position, returning `0` if only whitespace follows pub fn lines_after(&self, end: u32) -> usize { - self.slice_from(end) - .chars() - .filter(|&c| !is_white_space_single_line(c)) - .take_while(|&c| is_line_terminator(c)) - .count() + let mut count = 0; + for char in self.slice_from(end).chars() { + if is_white_space_single_line(char) { + continue; + } + + if is_line_terminator(char) { + count += 1; + continue; + } + + return count; + } + + // No non-whitespace characters found after position, so return `0` to avoid adding extra new lines + 0 } /// Count line breaks between syntax nodes, considering comments and parentheses @@ -146,7 +157,7 @@ impl<'a> SourceText<'a> { // Should skip the leading comments of the node. if let Some(comment) = comments.first() - && comment.span.end < start + && comment.span.end <= start { start = comment.span.start; } @@ -188,7 +199,7 @@ impl<'a> SourceText<'a> { count += 1; } - count + 0 } pub fn is_own_line_comment(&self, comment: &Comment) -> bool { diff --git a/crates/oxc_formatter/src/generated/ast_nodes.rs b/crates/oxc_formatter/src/generated/ast_nodes.rs index d1e4e214e3d63..59bf02e5e3f05 100644 --- a/crates/oxc_formatter/src/generated/ast_nodes.rs +++ b/crates/oxc_formatter/src/generated/ast_nodes.rs @@ -4189,7 +4189,7 @@ impl<'a> AstNode<'a, ComputedMemberExpression<'a>> { #[inline] pub fn expression(&self) -> &AstNode<'a, Expression<'a>> { - let following_node = self.following_node; + let following_node = None; self.allocator.alloc(AstNode { inner: &self.inner.expression, allocator: self.allocator, diff --git a/crates/oxc_formatter/src/utils/assignment_like.rs b/crates/oxc_formatter/src/utils/assignment_like.rs index 0bb1ae3a82312..47bb9346d0a3e 100644 --- a/crates/oxc_formatter/src/utils/assignment_like.rs +++ b/crates/oxc_formatter/src/utils/assignment_like.rs @@ -314,19 +314,19 @@ impl<'a> AssignmentLike<'a, '_> { } let right_expression = self.get_right_expression(); + if let Some(expr) = right_expression { + if let Some(layout) = self.chain_formatting_layout(expr) { + return layout; + } - if let Some(layout) = right_expression.and_then(|expr| self.chain_formatting_layout(expr)) { - return layout; - } - - if let Some(Expression::CallExpression(call_expression)) = - &right_expression.map(AsRef::as_ref) - && call_expression - .callee - .get_identifier_reference() - .is_some_and(|ident| ident.name == "require") - { - return AssignmentLikeLayout::NeverBreakAfterOperator; + if let Expression::CallExpression(call_expression) = expr.as_ref() + && call_expression + .callee + .get_identifier_reference() + .is_some_and(|ident| ident.name == "require") + { + return AssignmentLikeLayout::NeverBreakAfterOperator; + } } if self.should_break_after_operator(right_expression, f) { @@ -359,10 +359,8 @@ impl<'a> AssignmentLike<'a, '_> { return AssignmentLikeLayout::BreakAfterOperator; } - let is_poorly_breakable = match &right_expression { - Some(expression) => is_poorly_breakable_member_or_call_chain(expression, f), - None => false, - }; + let is_poorly_breakable = + right_expression.is_some_and(|expr| is_poorly_breakable_member_or_call_chain(expr, f)); if is_poorly_breakable { return AssignmentLikeLayout::BreakAfterOperator; @@ -581,17 +579,19 @@ fn should_break_after_operator<'a>( ) -> bool { let is_jsx = matches!(right.as_ref(), Expression::JSXElement(_) | Expression::JSXFragment(_)); + if is_jsx { + return false; + } + + let comments = f.comments(); let source_text = f.source_text(); - for comment in f.comments().comments_before(right.span().start) { - if !is_jsx - && (source_text.lines_after(comment.span.end) > 0 - || source_text.is_own_line_comment(comment)) - { + for comment in comments.comments_before(right.span().start) { + if source_text.lines_after(comment.span.end) > 0 { return true; } // Needs to wrap a parenthesis for the node, so it won't break. - if f.comments().is_type_cast_comment(comment) { + if comments.is_type_cast_comment(comment) { return false; } } diff --git a/crates/oxc_formatter/src/utils/conditional.rs b/crates/oxc_formatter/src/utils/conditional.rs index 898ee6b092327..4f60ca9b992a2 100644 --- a/crates/oxc_formatter/src/utils/conditional.rs +++ b/crates/oxc_formatter/src/utils/conditional.rs @@ -144,13 +144,13 @@ fn format_trailing_comments<'a>( if source_text.contains_newline_between(start, comment.span.start) { return &comments[..index]; } - // If this comment is a line comment, then it is a end of line comment, so we stop here and return the comments with this comment - else if comment.is_line() { + // If this comment is a line comment or an end of line comment, so we stop here and return the comments with this comment + else if comment.is_line() || source_text.is_end_of_line_comment(comment) { return &comments[..=index]; } // Store the index of the comment before the operator, if no line comment or no new line is found, then return all comments before operator else if source_text.bytes_contain(start, comment.span.start, operator) { - index_before_operator = Some(index + 1); + index_before_operator = Some(index); } // Update the start position for the next iteration diff --git a/crates/oxc_formatter/src/utils/typecast.rs b/crates/oxc_formatter/src/utils/typecast.rs index f146070de2d08..c360662568907 100644 --- a/crates/oxc_formatter/src/utils/typecast.rs +++ b/crates/oxc_formatter/src/utils/typecast.rs @@ -60,24 +60,36 @@ pub fn format_type_cast_comment_node<'a, T>( write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; f.context_mut().comments_mut().mark_as_handled_type_cast_comment(); - } else { - let elements = f.elements().iter().rev(); - // If the printed cast comment is already handled, return early to avoid infinite recursion. - if !comments.is_already_handled_type_cast_comment() - && comments.printed_comments().last().is_some_and(|c| { - c.span.end <= span.start - && source.all_bytes_match(c.span.end, span.start, |c| { - c.is_ascii_whitespace() || c == b'(' - }) - && f.comments().is_type_cast_comment(c) - }) + } else if !comments.is_already_handled_type_cast_comment() + && let Some(last_printed_comment) = comments.printed_comments().last() + { + if !last_printed_comment.span.end <= span.start + || !f.comments().is_type_cast_comment(last_printed_comment) { - f.context_mut().comments_mut().mark_as_handled_type_cast_comment(); - } else { - // No typecast comment return Ok(false); } + + let mut has_left_paren = false; + // Check whether there is only whitespace or left parenthesis between the comment and the node + for &byte in source.bytes_range(last_printed_comment.span.end, span.start) { + if !byte.is_ascii_whitespace() { + if byte == b'(' { + has_left_paren = true; + } else { + return Ok(false); + } + } + } + + if !has_left_paren { + return Ok(false); + } + + f.context_mut().comments_mut().mark_as_handled_type_cast_comment(); + } else { + // No typecast comment + return Ok(false); } // https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/print/estree.js#L117-L120 diff --git a/crates/oxc_formatter/src/write/call_arguments.rs b/crates/oxc_formatter/src/write/call_arguments.rs index a7ff6a03a7a98..46ffb0b97e2ba 100644 --- a/crates/oxc_formatter/src/write/call_arguments.rs +++ b/crates/oxc_formatter/src/write/call_arguments.rs @@ -96,8 +96,10 @@ impl<'a> Format<'a> for AstNode<'a, ArenaVec<'a, Argument<'a>>> { ); } - let has_empty_line = - self.iter().any(|arg| f.source_text().get_lines_before(arg.span(), f.comments()) > 1); + let has_empty_line = self + .iter() + .skip(1) + .any(|arg| f.source_text().get_lines_before(arg.span(), f.comments()) > 1); if has_empty_line || (!matches!(self.parent.parent(), AstNodes::Decorator(_)) && is_function_composition_args(self)) diff --git a/crates/oxc_formatter/src/write/member_expression.rs b/crates/oxc_formatter/src/write/member_expression.rs index 1bbbc45638b5d..26b0f4160037b 100644 --- a/crates/oxc_formatter/src/write/member_expression.rs +++ b/crates/oxc_formatter/src/write/member_expression.rs @@ -1,11 +1,18 @@ use std::ops::Deref; use oxc_ast::ast::*; +use oxc_span::GetSpan; use crate::{ JsLabels, format_args, formatter::{ - Buffer, Format, FormatResult, Formatter, prelude::*, trivia::format_dangling_comments, + Buffer, Format, FormatResult, Formatter, + buffer::RemoveSoftLinesBuffer, + prelude::*, + trivia::{ + DanglingIndentMode, FormatDanglingComments, FormatLeadingComments, + FormatTrailingComments, format_dangling_comments, + }, }, generated::ast_nodes::{AstNode, AstNodes}, options::Expand, @@ -30,7 +37,14 @@ impl<'a> FormatWrite<'a> for AstNode<'a, StaticMemberExpression<'a>> { recording.stop().has_label(LabelId::of(JsLabels::MemberChain)) }; - match layout(self, is_member_chain) { + let comments = f.context().comments().block_comments_before(self.property.span.start); + if !comments.is_empty() { + write!(f, [space()])?; + f.context_mut().comments_mut().increase_printed_count_by(comments.len()); + f.join_with(space()).entries(comments.iter()).finish()?; + } + + match layout(self, is_member_chain, f) { StaticMemberLayout::NoBreak => { let format_no_break = format_with(|f| write!(f, [operator_token(self.optional()), self.property()])); @@ -46,6 +60,17 @@ impl<'a> FormatWrite<'a> for AstNode<'a, StaticMemberExpression<'a>> { f, [group(&indent(&format_args!( soft_line_break(), + &format_once(|f| { + let comments = + f.context().comments().comments_before(self.property.span.start); + if !comments.is_empty() { + write!( + f, + [FormatLeadingComments::Comments(comments), soft_line_break()] + )?; + } + Ok(()) + }), operator_token(self.optional()), self.property(), )))] @@ -71,7 +96,15 @@ fn operator_token(optional: bool) -> &'static str { fn layout<'a>( node: &AstNode<'a, StaticMemberExpression<'a>>, is_member_chain: bool, + f: &Formatter<'_, 'a>, ) -> StaticMemberLayout { + if f.comments() + .comments_before_iter(node.property.span.start) + .any(|c| f.source_text().is_own_line_comment(c)) + { + return StaticMemberLayout::BreakAfterObject; + } + // `a.b.c!` and `a.b?.c` // `TSNonNullExpression` is a wrapper node for `!`, and `ChainExpression` is a wrapper node for `?.`, // so we need to skip them to find the real parent node. diff --git a/crates/oxc_formatter/src/write/mod.rs b/crates/oxc_formatter/src/write/mod.rs index 0ff21c6e2fad6..67fa0d266fce1 100644 --- a/crates/oxc_formatter/src/write/mod.rs +++ b/crates/oxc_formatter/src/write/mod.rs @@ -83,6 +83,7 @@ use self::{ object_like::ObjectLike, object_pattern_like::ObjectPatternLike, parameters::{ParameterLayout, ParameterList}, + return_or_throw_statement::FormatAdjacentArgument, semicolon::OptionalSemicolon, type_parameters::{FormatTSTypeParameters, FormatTSTypeParametersOptions}, utils::{ @@ -659,7 +660,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ForStatement<'a>> { impl<'a> FormatWrite<'a> for AstNode<'a, ForInStatement<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - let comments = f.context().comments().own_line_comments_before(self.body.span().start); + let comments = f.context().comments().own_line_comments_before(self.right.span().start); write!( f, [ @@ -683,7 +684,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ForInStatement<'a>> { impl<'a> FormatWrite<'a> for AstNode<'a, ForOfStatement<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - let comments = f.context().comments().own_line_comments_before(self.body.span().start); + let comments = f.context().comments().own_line_comments_before(self.right.span().start); let r#await = self.r#await(); let left = self.left(); @@ -857,12 +858,17 @@ impl<'a> FormatWrite<'a> for AstNode<'a, BindingPattern<'a>> { impl<'a> FormatWrite<'a> for AstNode<'a, AssignmentPattern<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - let comments = f.context().comments().own_line_comments_before(self.right.span().start); + let mut left = self.left().memoized(); + left.inspect(f)?; write!( f, [ - FormatLeadingComments::Comments(comments), - self.left(), + format_once(|f| { + let comments = + f.context().comments().own_line_comments_before(self.right.span().start); + FormatLeadingComments::Comments(comments).fmt(f) + }), + left, space(), "=", space(), @@ -938,12 +944,9 @@ impl<'a> FormatWrite<'a, FormatJsArrowFunctionExpressionOptions> impl<'a> FormatWrite<'a> for AstNode<'a, YieldExpression<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, "yield")?; - if self.delegate() { - write!(f, "*")?; - } + write!(f, ["yield", self.delegate().then_some("*")])?; if let Some(argument) = &self.argument() { - write!(f, [space(), argument])?; + write!(f, [space(), FormatAdjacentArgument(argument)])?; } Ok(()) } diff --git a/crates/oxc_formatter/src/write/parameters.rs b/crates/oxc_formatter/src/write/parameters.rs index 5a274db55a563..1150165ebf8a7 100644 --- a/crates/oxc_formatter/src/write/parameters.rs +++ b/crates/oxc_formatter/src/write/parameters.rs @@ -43,8 +43,8 @@ impl<'a> FormatWrite<'a> for AstNode<'a, FormalParameters<'a>> { let has_any_decorated_parameter = self.items.iter().any(|param| !param.decorators.is_empty()); - let can_hug = should_hug_function_parameters(self, this_param, parentheses_not_needed, f) - && !has_any_decorated_parameter; + let can_hug = !has_any_decorated_parameter + && should_hug_function_parameters(self, this_param, parentheses_not_needed, f); let layout = if !self.has_parameter() && this_param.is_none() { ParameterLayout::NoParameters @@ -77,13 +77,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, FormalParameters<'a>> { ParameterLayout::Default => { write!( f, - soft_block_indent(&format_args!( - &ParameterList::with_layout(self, this_param, layout), - format_once(|f| { - let comments = f.context().comments().comments_before(self.span.end); - write!(f, [FormatTrailingComments::Comments(comments)]) - }) - )) + soft_block_indent(&&ParameterList::with_layout(self, this_param, layout)) ); } } @@ -341,6 +335,11 @@ pub fn should_hug_function_parameters<'a>( Expression::Identifier(_) => true, _ => false, } + && !f + .comments() + .comments_in_range(assignment.left.span().end, assignment.right.span().start) + .iter() + .any(|c| f.source_text().is_own_line_comment(c)) } BindingPatternKind::ArrayPattern(_) | BindingPatternKind::ObjectPattern(_) => true, BindingPatternKind::BindingIdentifier(_) => { diff --git a/crates/oxc_formatter/src/write/program.rs b/crates/oxc_formatter/src/write/program.rs index 447812c5dccfe..bb82f3435c4e5 100644 --- a/crates/oxc_formatter/src/write/program.rs +++ b/crates/oxc_formatter/src/write/program.rs @@ -7,7 +7,7 @@ use oxc_syntax::identifier::{ZWNBSP, is_line_terminator}; use crate::{ Buffer, Format, FormatResult, FormatTrailingCommas, TrailingSeparator, format_args, - formatter::{prelude::*, separated::FormatSeparatedIter, trivia::FormatTrailingComments}, + formatter::{prelude::*, trivia::FormatTrailingComments}, generated::ast_nodes::{AstNode, AstNodes}, utils::{ call_expression::is_test_call_expression, @@ -25,7 +25,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, Program<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { let format_trailing_comments = format_once(|f| { let comments = f.context().comments().comments_before(self.span.end); - FormatTrailingComments::Comments(comments).fmt(f) + write!(f, FormatTrailingComments::Comments(comments)) }); write!( @@ -112,19 +112,8 @@ impl<'a> Format<'a> for AstNode<'a, Vec<'a, Directive<'a>>> { // } //``` // so we should keep an extra empty line after JsDirectiveList - let source_text = f.context().source_text(); - let mut count = 0; - let mut source_text_chars = source_text.slice_from(last_directive.span.end).chars(); - for char in source_text_chars.by_ref() { - if is_line_terminator(char) { - count += 1; - } else if !char.is_whitespace() { - break; - } - } - // Need an extra empty line if it has the following line and still has non-characters after whitespace. - let need_extra_empty_line = source_text_chars.next().is_some() && count > 1; + let need_extra_empty_line = f.source_text().lines_after(last_directive.span.end) > 1; write!(f, if need_extra_empty_line { empty_line() } else { hard_line_break() }) } } diff --git a/crates/oxc_formatter/src/write/return_or_throw_statement.rs b/crates/oxc_formatter/src/write/return_or_throw_statement.rs index 96cfe8c68599c..61a5c55054206 100644 --- a/crates/oxc_formatter/src/write/return_or_throw_statement.rs +++ b/crates/oxc_formatter/src/write/return_or_throw_statement.rs @@ -60,7 +60,7 @@ impl<'a> Format<'a> for ReturnAndThrowStatement<'a, '_> { write!(f, self.keyword())?; if let Some(argument) = self.argument() { - write!(f, [space(), FormatReturnOrThrowArgument(argument)])?; + write!(f, [space(), FormatAdjacentArgument(argument)])?; } let dangling_comments = f.context().comments().comments_before(self.span().end); @@ -84,9 +84,9 @@ impl<'a> Format<'a> for ReturnAndThrowStatement<'a, '_> { } } -pub struct FormatReturnOrThrowArgument<'a, 'b>(&'b AstNode<'a, Expression<'a>>); +pub struct FormatAdjacentArgument<'a, 'b>(pub &'b AstNode<'a, Expression<'a>>); -impl<'a> Format<'a> for FormatReturnOrThrowArgument<'a, '_> { +impl<'a> Format<'a> for FormatAdjacentArgument<'a, '_> { fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { let argument = self.0; diff --git a/crates/oxc_formatter/src/write/try_statement.rs b/crates/oxc_formatter/src/write/try_statement.rs index 61c18ec1f4e28..d693ce646fbf9 100644 --- a/crates/oxc_formatter/src/write/try_statement.rs +++ b/crates/oxc_formatter/src/write/try_statement.rs @@ -9,7 +9,10 @@ use crate::{ Formatter, prelude::*, separated::FormatSeparatedIter, - trivia::{FormatLeadingComments, FormatTrailingComments}, + trivia::{ + DanglingIndentMode, FormatDanglingComments, FormatLeadingComments, + FormatTrailingComments, + }, }, generated::ast_nodes::{AstNode, AstNodes}, write, @@ -40,15 +43,27 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TryStatement<'a>> { impl<'a> FormatWrite<'a> for AstNode<'a, CatchClause<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - // `try {} /* comment */ catch (e) {}` - // should be formatted like: - // `try {} catch (e) { /* comment */ }` - // - // Comments before the catch clause should be printed in the block statement. - // We cache them here to avoid the `params` printing them accidentally. - let printed_comments = f.intern(&format_leading_comments(self.span)); - if let Ok(Some(comments)) = printed_comments { - f.context_mut().cache_element(&self.span, comments); + let comments = f.context().comments().comments_before(self.span.start); + let has_line_comment = comments.iter().any(|comment| { + comment.is_line() + || f.source_text().is_own_line_comment(comment) + || f.source_text().is_end_of_line_comment(comment) + }); + + if has_line_comment { + // `try {} /* comment */\n catch (e) {}` + // should be formatted like: + // `try {} catch (e) { /* comment */ }` + // + // Comments before the catch clause should be printed in the block statement. + // We cache them here to avoid the `params` printing them accidentally. + let printed_comments = f.intern(&FormatLeadingComments::Comments(comments)); + if let Ok(Some(comments)) = printed_comments { + f.context_mut().cache_element(&self.span, comments); + } + } else if !comments.is_empty() { + // otherwise, print them before `catch` + write!(f, [FormatTrailingComments::Comments(comments), space()]); } write!(f, ["catch", space(), self.param(), space()])?; diff --git a/tasks/ast_tools/src/generators/formatter/ast_nodes.rs b/tasks/ast_tools/src/generators/formatter/ast_nodes.rs index 3a4e7695998a3..a2480c68eea15 100644 --- a/tasks/ast_tools/src/generators/formatter/ast_nodes.rs +++ b/tasks/ast_tools/src/generators/formatter/ast_nodes.rs @@ -20,8 +20,13 @@ const FORMATTER_CRATE_PATH: &str = "crates/oxc_formatter"; /// Based on the printing comments algorithm, the last child of these AST nodes don't need to print comments. /// Without following nodes could lead to only print comments that before the end of the node, which is what we want. -const AST_NODE_WITHOUT_FOLLOWING_NODE_LIST: &[&str] = - &["AssignmentExpression", "FormalParameters", "StaticMemberExpression", "ObjectProperty"]; +const AST_NODE_WITHOUT_FOLLOWING_NODE_LIST: &[&str] = &[ + "AssignmentExpression", + "FormalParameters", + "StaticMemberExpression", + "ObjectProperty", + "ComputedMemberExpression", +]; const AST_NODE_WITH_FOLLOWING_NODE_LIST: &[&str] = &["Function", "Class"]; diff --git a/tasks/coverage/snapshots/formatter_babel.snap b/tasks/coverage/snapshots/formatter_babel.snap index 817cd6f11b51e..1b4780425c738 100644 --- a/tasks/coverage/snapshots/formatter_babel.snap +++ b/tasks/coverage/snapshots/formatter_babel.snap @@ -2,10 +2,6 @@ commit: 41d96516 formatter_babel Summary: AST Parsed : 2423/2423 (100.00%) -Positive Passed: 2420/2423 (99.88%) -Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/comments/basic/try-statement/input.js - -Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/class/division/input.js - +Positive Passed: 2422/2423 (99.96%) Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2022/top-level-await-unambiguous/module/input.js `await` is only allowed within async functions and at the top levels of modules diff --git a/tasks/coverage/snapshots/formatter_typescript.snap b/tasks/coverage/snapshots/formatter_typescript.snap index 520037a01f66d..0b6e274677546 100644 --- a/tasks/coverage/snapshots/formatter_typescript.snap +++ b/tasks/coverage/snapshots/formatter_typescript.snap @@ -2,7 +2,7 @@ commit: 261630d6 formatter_typescript Summary: AST Parsed : 8816/8816 (100.00%) -Positive Passed: 8806/8816 (99.89%) +Positive Passed: 8808/8816 (99.91%) Mismatch: tasks/coverage/typescript/tests/cases/compiler/amdLikeInputDeclarationEmit.ts Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/arrayFromAsync.ts @@ -11,10 +11,6 @@ Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitCastReus Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/genericTypeAssertions3.ts Unexpected token -Mismatch: tasks/coverage/typescript/tests/cases/compiler/propertyAccessExpressionInnerComments.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/tryStatementInternalComments.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/classes/propertyMemberDeclarations/staticPropertyNameConflicts.ts Classes may not have a static property named prototypeClasses may not have a static property named prototypeClasses may not have a static property named prototypeClasses may not have a static property named prototypeClasses may not have a static property named prototypeClasses may not have a static property named prototype Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/expressions/elementAccess/letIdentifierInElementAccess01.ts From 9967372558dda3cdea870a2f5c2cd72dc8d416d9 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Tue, 30 Sep 2025 07:54:33 +0800 Subject: [PATCH 2/4] third --- .../oxc_formatter/src/formatter/comments.rs | 22 +++-- .../src/parentheses/expression.rs | 98 +++++++++++++++++++ crates/oxc_formatter/src/utils/conditional.rs | 4 +- crates/oxc_formatter/src/utils/typecast.rs | 16 ++- 4 files changed, 127 insertions(+), 13 deletions(-) diff --git a/crates/oxc_formatter/src/formatter/comments.rs b/crates/oxc_formatter/src/formatter/comments.rs index 248d24db68be9..ca0da0c656fce 100644 --- a/crates/oxc_formatter/src/formatter/comments.rs +++ b/crates/oxc_formatter/src/formatter/comments.rs @@ -127,7 +127,8 @@ pub struct Comments<'a> { printed_count: usize, /// The index of the type cast comment that has been printed already. /// Used to prevent duplicate processing of special TypeScript type cast comments. - handled_type_cast_comment: usize, + last_handled_type_cast_comment: usize, + type_cast_node_span: Span, /// Optional limit for the unprinted_comments view. /// /// When set, [`Self::unprinted_comments()`] will only return comments up to this index, @@ -141,7 +142,8 @@ impl<'a> Comments<'a> { source_text, comments, printed_count: 0, - handled_type_cast_comment: 0, + last_handled_type_cast_comment: 0, + type_cast_node_span: Span::default(), view_limit: None, } } @@ -505,14 +507,20 @@ impl<'a> Comments<'a> { ) } - /// Marks the most recently printed type cast comment as handled. - pub fn mark_as_handled_type_cast_comment(&mut self) { - self.handled_type_cast_comment = self.printed_count; + /// Marks the given span as a type cast node. + pub fn mark_as_type_cast_node(&mut self, node: &impl GetSpan) { + self.type_cast_node_span = node.span(); + self.last_handled_type_cast_comment = self.printed_count; } /// Checks if the most recently printed type cast comment has been handled. - pub fn is_already_handled_type_cast_comment(&self) -> bool { - self.printed_count == self.handled_type_cast_comment + pub fn is_handled_type_cast_comment(&self) -> bool { + self.printed_count == self.last_handled_type_cast_comment + } + + #[inline] + pub fn is_type_cast_node(&self, node: &impl GetSpan) -> bool { + self.type_cast_node_span == node.span() } /// Temporarily limits the unprinted comments view to only those before the given position. diff --git a/crates/oxc_formatter/src/parentheses/expression.rs b/crates/oxc_formatter/src/parentheses/expression.rs index 682df959a121d..4d3c9af9f6713 100644 --- a/crates/oxc_formatter/src/parentheses/expression.rs +++ b/crates/oxc_formatter/src/parentheses/expression.rs @@ -73,6 +73,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, Expression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, IdentifierReference<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + match self.name.as_str() { "async" => { matches!(self.parent, AstNodes::ForOfStatement(stmt) if !stmt.r#await && stmt.left.span().contains_inclusive(self.span)) @@ -165,6 +169,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, Super> { impl<'a> NeedsParentheses<'a> for AstNode<'a, NumericLiteral<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + if let AstNodes::StaticMemberExpression(member) = self.parent { return member.object.without_parentheses().span() == self.span(); } @@ -174,6 +182,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, NumericLiteral<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, StringLiteral<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + if let AstNodes::ExpressionStatement(stmt) = self.parent { // `() => "foo"` if let AstNodes::FunctionBody(arrow) = stmt.parent { @@ -207,6 +219,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, ArrayExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, ObjectExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; is_class_extends(self.span, parent) || is_first_in_statement( @@ -239,6 +255,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, ComputedMemberExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, StaticMemberExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + matches!(self.parent, AstNodes::NewExpression(_)) && { ExpressionLeftSide::Expression(self.object()).iter().any(|expr| { matches!(expr, ExpressionLeftSide::Expression(e) if @@ -258,6 +278,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, PrivateFieldExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, CallExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + match self.parent { AstNodes::NewExpression(_) => true, AstNodes::ExportDefaultDeclaration(_) => { @@ -280,12 +304,20 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, CallExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, NewExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + is_class_extends(self.span, self.parent) } } impl<'a> NeedsParentheses<'a> for AstNode<'a, UpdateExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; if self.prefix() && let AstNodes::UnaryExpression(unary) = parent @@ -303,6 +335,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, UpdateExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, UnaryExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; match parent { AstNodes::UnaryExpression(parent_unary) => { @@ -323,6 +359,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, UnaryExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, BinaryExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + (self.operator.is_in() && is_in_for_initializer(self)) || binary_like_needs_parens(BinaryLikeExpression::BinaryExpression(self)) } @@ -371,12 +411,20 @@ fn is_in_for_initializer(expr: &AstNode<'_, BinaryExpression<'_>>) -> bool { impl<'a> NeedsParentheses<'a> for AstNode<'a, PrivateInExpression<'a>> { #[inline] fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + is_class_extends(self.span, self.parent) } } impl<'a> NeedsParentheses<'a> for AstNode<'a, LogicalExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; if let AstNodes::LogicalExpression(parent) = parent { parent.operator() != self.operator() @@ -392,6 +440,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, LogicalExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, ConditionalExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; if matches!( parent, @@ -420,6 +472,11 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, Function<'a>> { if self.r#type() != FunctionType::FunctionExpression { return false; } + + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; matches!( parent, @@ -436,6 +493,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, Function<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, AssignmentExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + match self.parent { // Expression statements, only object destructuring needs parens: // - `a = b` = no parens @@ -519,6 +580,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, AssignmentExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, SequenceExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + !matches!( self.parent, AstNodes::ReturnStatement(_) @@ -534,12 +599,20 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, SequenceExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, AwaitExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + await_or_yield_needs_parens(self.span(), self.parent) } } impl<'a> NeedsParentheses<'a> for AstNode<'a, ChainExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + match self.parent { AstNodes::NewExpression(_) => true, AstNodes::CallExpression(call) => !call.optional, @@ -557,6 +630,11 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, Class<'a>> { if self.r#type() != ClassType::ClassExpression { return false; } + + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; match parent { AstNodes::CallExpression(_) @@ -583,6 +661,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, ParenthesizedExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, ArrowFunctionExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; if matches!( parent, @@ -606,6 +688,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, ArrowFunctionExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, YieldExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + let parent = self.parent; matches!(parent, AstNodes::AwaitExpression(_) | AstNodes::TSTypeAssertion(_)) || await_or_yield_needs_parens(self.span(), parent) @@ -614,6 +700,10 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, YieldExpression<'a>> { impl<'a> NeedsParentheses<'a> for AstNode<'a, ImportExpression<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + matches!(self.parent, AstNodes::NewExpression(_)) } } @@ -1022,12 +1112,20 @@ fn jsx_element_or_fragment_needs_paren(span: Span, parent: &AstNodes<'_>) -> boo impl NeedsParentheses<'_> for AstNode<'_, JSXElement<'_>> { fn needs_parentheses(&self, f: &Formatter<'_, '_>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + jsx_element_or_fragment_needs_paren(self.span, self.parent) } } impl NeedsParentheses<'_> for AstNode<'_, JSXFragment<'_>> { fn needs_parentheses(&self, f: &Formatter<'_, '_>) -> bool { + if f.comments().is_type_cast_node(self) { + return false; + } + jsx_element_or_fragment_needs_paren(self.span, self.parent) } } diff --git a/crates/oxc_formatter/src/utils/conditional.rs b/crates/oxc_formatter/src/utils/conditional.rs index 4f60ca9b992a2..babaa1814bf77 100644 --- a/crates/oxc_formatter/src/utils/conditional.rs +++ b/crates/oxc_formatter/src/utils/conditional.rs @@ -161,9 +161,7 @@ fn format_trailing_comments<'a>( }; let comments = get_comments(f); - FormatTrailingComments::Comments(comments).fmt(f)?; - - Ok(()) + FormatTrailingComments::Comments(comments).fmt(f) } impl<'a> FormatConditionalLike<'a, '_> { diff --git a/crates/oxc_formatter/src/utils/typecast.rs b/crates/oxc_formatter/src/utils/typecast.rs index c360662568907..dc1fe1d6dea4b 100644 --- a/crates/oxc_formatter/src/utils/typecast.rs +++ b/crates/oxc_formatter/src/utils/typecast.rs @@ -59,9 +59,9 @@ pub fn format_type_cast_comment_node<'a, T>( let type_cast_comments = &comments[..=type_cast_comment_index]; write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; - f.context_mut().comments_mut().mark_as_handled_type_cast_comment(); + f.context_mut().comments_mut().mark_as_type_cast_node(node); // If the printed cast comment is already handled, return early to avoid infinite recursion. - } else if !comments.is_already_handled_type_cast_comment() + } else if !comments.is_handled_type_cast_comment() && let Some(last_printed_comment) = comments.printed_comments().last() { if !last_printed_comment.span.end <= span.start @@ -70,6 +70,16 @@ pub fn format_type_cast_comment_node<'a, T>( return Ok(false); } + // Get the source text from the end of type cast comment to the node span + let node_source_text = source.bytes_range(last_printed_comment.span.end, span.end); + + // `(/** @type {Number} */ (bar).zoo)` + // ^^^^ + // Should wrap for `baz` rather than `baz.zoo` + if has_closed_parentheses(node_source_text) { + return Ok(false); + } + let mut has_left_paren = false; // Check whether there is only whitespace or left parenthesis between the comment and the node for &byte in source.bytes_range(last_printed_comment.span.end, span.start) { @@ -86,7 +96,7 @@ pub fn format_type_cast_comment_node<'a, T>( return Ok(false); } - f.context_mut().comments_mut().mark_as_handled_type_cast_comment(); + f.context_mut().comments_mut().mark_as_type_cast_node(node); } else { // No typecast comment return Ok(false); From 9f9d22f7804ad8696aa70c8e8d15cd7d5684680d Mon Sep 17 00:00:00 2001 From: Dunqing Date: Tue, 30 Sep 2025 07:55:16 +0800 Subject: [PATCH 3/4] correct parenthesis check for `TSUnionType` and `TSIntersectionType` --- .../oxc_formatter/src/parentheses/ts_type.rs | 19 +++++++++++-------- .../snapshots/prettier.ts.snap.md | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/oxc_formatter/src/parentheses/ts_type.rs b/crates/oxc_formatter/src/parentheses/ts_type.rs index 0061424a1c494..82fad2b01f9ce 100644 --- a/crates/oxc_formatter/src/parentheses/ts_type.rs +++ b/crates/oxc_formatter/src/parentheses/ts_type.rs @@ -104,7 +104,8 @@ fn function_like_type_needs_parentheses<'a>( } false } - AstNodes::TSUnionType(_) | AstNodes::TSIntersectionType(_) => true, + AstNodes::TSUnionType(union) => union.types.len() > 1, + AstNodes::TSIntersectionType(intersection) => intersection.types.len() > 1, _ => operator_type_or_higher_needs_parens(span, parent), } } @@ -126,12 +127,13 @@ fn operator_type_or_higher_needs_parens(span: Span, parent: &AstNodes) -> bool { impl<'a> NeedsParentheses<'a> for AstNode<'a, TSIntersectionType<'a>> { fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool { - matches!( - self.parent, - AstNodes::TSArrayType(_) - | AstNodes::TSTypeOperator(_) - | AstNodes::TSIndexedAccessType(_) - ) + match self.parent { + AstNodes::TSUnionType(union) => self.types.len() > 1 && union.types.len() > 1, + AstNodes::TSIntersectionType(intersection) => { + self.types.len() > 1 && intersection.types.len() > 1 + } + parent => operator_type_or_higher_needs_parens(self.span(), parent), + } } } @@ -141,7 +143,8 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, TSConditionalType<'a>> { AstNodes::TSConditionalType(ty) => { ty.extends_type().span() == self.span() || ty.check_type().span() == self.span() } - AstNodes::TSUnionType(_) | AstNodes::TSIntersectionType(_) => true, + AstNodes::TSUnionType(union) => union.types.len() > 1, + AstNodes::TSIntersectionType(intersection) => intersection.types.len() > 1, _ => operator_type_or_higher_needs_parens(self.span, self.parent), } } diff --git a/tasks/prettier_conformance/snapshots/prettier.ts.snap.md b/tasks/prettier_conformance/snapshots/prettier.ts.snap.md index 23f2c2c2eaa4b..c6052e45a12e4 100644 --- a/tasks/prettier_conformance/snapshots/prettier.ts.snap.md +++ b/tasks/prettier_conformance/snapshots/prettier.ts.snap.md @@ -31,16 +31,16 @@ ts compatibility: 533/573 (93.02%) | typescript/definite/without-annotation.ts | 💥 | 83.33% | | typescript/enum/computed-members.ts | 💥 | 0.00% | | typescript/interface/ignore.ts | 💥✨ | 40.09% | -| typescript/intersection/intersection-parens.ts | 💥💥 | 80.85% | +| typescript/intersection/intersection-parens.ts | 💥💥 | 86.17% | | typescript/intersection/consistent-with-flow/intersection-parens.ts | 💥 | 69.77% | | typescript/last-argument-expansion/decorated-function.tsx | 💥 | 29.06% | | typescript/multiparser-css/issue-6259.ts | 💥 | 57.14% | | typescript/non-null/optional-chain.ts | 💥 | 72.22% | | typescript/object-multiline/multiline.ts | 💥✨ | 23.21% | | typescript/prettier-ignore/mapped-types.ts | 💥 | 63.16% | -| typescript/prettier-ignore/prettier-ignore-nested-unions.ts | 💥 | 44.00% | +| typescript/prettier-ignore/prettier-ignore-nested-unions.ts | 💥 | 68.00% | | typescript/type-arguments-bit-shift-left-like/3.ts | 💥 | 0.00% | | typescript/type-arguments-bit-shift-left-like/5.tsx | 💥 | 0.00% | | typescript/union/union-parens.ts | 💥 | 92.59% | -| typescript/union/consistent-with-flow/prettier-ignore.ts | 💥 | 60.00% | +| typescript/union/consistent-with-flow/prettier-ignore.ts | 💥 | 88.00% | | typescript/union/single-type/single-type.ts | 💥 | 0.00% | From cda9e16711531828441b50d728f1c1d7aab0cb74 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Sun, 5 Oct 2025 22:37:37 +0800 Subject: [PATCH 4/4] up --- .../src/utils/member_chain/mod.rs | 7 +++ crates/oxc_formatter/src/utils/typecast.rs | 54 +++++++------------ 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/crates/oxc_formatter/src/utils/member_chain/mod.rs b/crates/oxc_formatter/src/utils/member_chain/mod.rs index c6133a0d784d1..792eb8ed1606a 100644 --- a/crates/oxc_formatter/src/utils/member_chain/mod.rs +++ b/crates/oxc_formatter/src/utils/member_chain/mod.rs @@ -235,6 +235,13 @@ impl<'a> Format<'a> for MemberChain<'a, '_> { if has_comment || has_new_line_or_comment_between || self.groups_should_break(f) { write!(f, [group(&format_expanded)]) } else { + let has_empty_line_before_tail = + self.tail.first().is_some_and(MemberChainGroup::needs_empty_line); + + if has_empty_line_before_tail || self.last_group().will_break(f) { + write!(f, [expand_parent()])?; + } + write!(f, [best_fitting!(format_one_line, format_expanded)]) } }); diff --git a/crates/oxc_formatter/src/utils/typecast.rs b/crates/oxc_formatter/src/utils/typecast.rs index dc1fe1d6dea4b..19ca4352d0cc1 100644 --- a/crates/oxc_formatter/src/utils/typecast.rs +++ b/crates/oxc_formatter/src/utils/typecast.rs @@ -42,12 +42,14 @@ pub fn format_type_cast_comment_node<'a, T>( return Ok(false); } - if let Some(type_cast_comment_index) = comments.get_type_cast_comment_index(span) { - let comments = f.context().comments().unprinted_comments(); - let type_cast_comment = &comments[type_cast_comment_index]; - + if !comments.is_handled_type_cast_comment() + && let Some(last_printed_comment) = comments.printed_comments().last() + && last_printed_comment.span.end <= span.start + && f.source_text().next_non_whitespace_byte_is(last_printed_comment.span.end, b'(') + && f.comments().is_type_cast_comment(last_printed_comment) + { // Get the source text from the end of type cast comment to the node span - let node_source_text = source.bytes_range(type_cast_comment.span.end, span.end); + let node_source_text = source.bytes_range(last_printed_comment.span.end, span.end); // `(/** @type {Number} */ (bar).zoo)` // ^^^^ @@ -56,22 +58,13 @@ pub fn format_type_cast_comment_node<'a, T>( return Ok(false); } - let type_cast_comments = &comments[..=type_cast_comment_index]; - - write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; f.context_mut().comments_mut().mark_as_type_cast_node(node); - // If the printed cast comment is already handled, return early to avoid infinite recursion. - } else if !comments.is_handled_type_cast_comment() - && let Some(last_printed_comment) = comments.printed_comments().last() - { - if !last_printed_comment.span.end <= span.start - || !f.comments().is_type_cast_comment(last_printed_comment) - { - return Ok(false); - } + } else if let Some(type_cast_comment_index) = comments.get_type_cast_comment_index(span) { + let comments = f.context().comments().unprinted_comments(); + let type_cast_comment = &comments[type_cast_comment_index]; // Get the source text from the end of type cast comment to the node span - let node_source_text = source.bytes_range(last_printed_comment.span.end, span.end); + let node_source_text = source.bytes_range(type_cast_comment.span.end, span.end); // `(/** @type {Number} */ (bar).zoo)` // ^^^^ @@ -80,23 +73,11 @@ pub fn format_type_cast_comment_node<'a, T>( return Ok(false); } - let mut has_left_paren = false; - // Check whether there is only whitespace or left parenthesis between the comment and the node - for &byte in source.bytes_range(last_printed_comment.span.end, span.start) { - if !byte.is_ascii_whitespace() { - if byte == b'(' { - has_left_paren = true; - } else { - return Ok(false); - } - } - } - - if !has_left_paren { - return Ok(false); - } + let type_cast_comments = &comments[..=type_cast_comment_index]; + write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; f.context_mut().comments_mut().mark_as_type_cast_node(node); + // If the printed cast comment is already handled, return early to avoid infinite recursion. } else { // No typecast comment return Ok(false); @@ -124,7 +105,12 @@ fn has_closed_parentheses(source: &[u8]) -> bool { while i < source.len() { match source[i] { b'(' => paren_count += 1, - b')' => paren_count -= 1, + b')' => { + paren_count -= 1; + if paren_count == 0 { + return true; + } + } b'/' if i + 1 < source.len() => { match source[i + 1] { b'/' => {