diff --git a/crates/oxc_formatter/src/write/call_arguments.rs b/crates/oxc_formatter/src/write/call_like_expression/arguments.rs similarity index 99% rename from crates/oxc_formatter/src/write/call_arguments.rs rename to crates/oxc_formatter/src/write/call_like_expression/arguments.rs index c5fc7ccbdd62e..7515ff39961c5 100644 --- a/crates/oxc_formatter/src/write/call_arguments.rs +++ b/crates/oxc_formatter/src/write/call_like_expression/arguments.rs @@ -22,19 +22,16 @@ use crate::{ }, write, write::{ - FormatFunctionOptions, - arrow_function_expression::is_multiline_template_starting_on_same_line, - }, -}; - -use super::{ - FormatJsArrowFunctionExpression, - array_element_list::can_concisely_print_array_list, - arrow_function_expression::{ - FormatJsArrowFunctionExpressionOptions, FunctionCacheMode, GroupedCallArgumentLayout, + FormatFunctionOptions, FormatJsArrowFunctionExpression, + FormatJsArrowFunctionExpressionOptions, + array_element_list::can_concisely_print_array_list, + arrow_function_expression::{ + FunctionCacheMode, GroupedCallArgumentLayout, + is_multiline_template_starting_on_same_line, + }, + function::FormatFunction, + parameters::has_only_simple_parameters, }, - function::FormatFunction, - parameters::has_only_simple_parameters, }; impl<'a> Format<'a> for AstNode<'a, ArenaVec<'a, Argument<'a>>> { diff --git a/crates/oxc_formatter/src/write/call_like_expression/mod.rs b/crates/oxc_formatter/src/write/call_like_expression/mod.rs new file mode 100644 index 0000000000000..ae1dfdabb0878 --- /dev/null +++ b/crates/oxc_formatter/src/write/call_like_expression/mod.rs @@ -0,0 +1,135 @@ +mod arguments; + +use oxc_ast::ast::*; +use oxc_span::GetSpan; + +use crate::{ + ast_nodes::AstNode, + formatter::{Formatter, TailwindContextEntry, prelude::*, trivia::FormatTrailingComments}, + utils::{ + call_expression::is_test_call_expression, + format_node_without_trailing_comments::FormatNodeWithoutTrailingComments, + member_chain::MemberChain, tailwindcss::is_tailwind_function_call, + }, + write, + write::arrow_function_expression::is_multiline_template_starting_on_same_line, +}; +use arguments::is_simple_module_import; + +use super::FormatWrite; + +impl<'a> FormatWrite<'a> for AstNode<'a, CallExpression<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) { + let callee = self.callee(); + let type_arguments = self.type_arguments(); + let arguments = self.arguments(); + let optional = self.optional(); + + // Check if this is a Tailwind function call (e.g., clsx, cn, tw) + let is_tailwind_call = f + .options() + .experimental_tailwindcss + .as_ref() + .is_some_and(|opts| is_tailwind_function_call(&self.callee, opts)); + + // For nested non-Tailwind calls inside a Tailwind context, disable sorting + // to prevent sorting strings inside the nested call's arguments. + // (e.g., `classNames("a", x.includes("\n") ? "b" : "c")` - don't sort "\n") + let was_disabled = + if !is_tailwind_call && let Some(ctx) = f.context_mut().tailwind_context_mut() { + let was = ctx.disabled; + ctx.disabled = true; + Some(was) + } else { + None + }; + + let is_template_literal_single_arg = arguments.len() == 1 + && arguments.first().unwrap().as_expression().is_some_and(|expr| { + is_multiline_template_starting_on_same_line(expr, f.source_text()) + }); + + if !is_template_literal_single_arg + && matches!( + callee.as_ref(), + Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) + ) + && !is_simple_module_import(self.arguments(), f.comments()) + && !is_test_call_expression(self) + { + MemberChain::from_call_expression(self, f).fmt(f); + } else { + let format_inner = format_with(|f| { + // Preserve trailing comments of the callee in the following cases: + // `call /**/()` + // `call /**/()` + if self.type_arguments.is_some() { + write!(f, [callee]); + } else { + write!(f, [FormatNodeWithoutTrailingComments(callee)]); + + if self.arguments.is_empty() { + let callee_trailing_comments = f + .context() + .comments() + .comments_before_character(self.callee.span().end, b'('); + write!(f, FormatTrailingComments::Comments(callee_trailing_comments)); + } + } + write!(f, [optional.then_some("?."), type_arguments]); + + // If this IS a Tailwind function call, push the Tailwind context + let tailwind_ctx_to_push = if is_tailwind_call { + f.options() + .experimental_tailwindcss + .as_ref() + .map(|opts| TailwindContextEntry::new(opts.preserve_whitespace)) + } else { + None + }; + + // Push Tailwind context before formatting arguments + if let Some(ctx) = tailwind_ctx_to_push { + f.context_mut().push_tailwind_context(ctx); + } + + write!(f, arguments); + + // Pop Tailwind context after formatting + if tailwind_ctx_to_push.is_some() { + f.context_mut().pop_tailwind_context(); + } + }); + if matches!(callee.as_ref(), Expression::CallExpression(_)) { + write!(f, [group(&format_inner)]); + } else { + write!(f, [format_inner]); + } + } + + // Restore the previous disabled state + if let Some(was) = was_disabled + && let Some(ctx) = f.context_mut().tailwind_context_mut() + { + ctx.disabled = was; + } + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, NewExpression<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) { + write!(f, ["new", space(), self.callee(), self.type_arguments(), self.arguments()]); + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, ImportExpression<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) { + write!(f, ["import"]); + if let Some(phase) = &self.phase() { + write!(f, [".", phase.as_str()]); + } + + // Use the same logic as CallExpression arguments formatting + write!(f, self.to_arguments()); + } +} diff --git a/crates/oxc_formatter/src/write/function.rs b/crates/oxc_formatter/src/write/function.rs index ef7f2ea57c9fe..48232fa7321ac 100644 --- a/crates/oxc_formatter/src/write/function.rs +++ b/crates/oxc_formatter/src/write/function.rs @@ -197,7 +197,7 @@ pub fn should_group_function_parameters<'a>( /// A wrapper that formats content and caches the result based on the given cache mode. /// -/// It is useful in cases like in [`super::call_arguments`] because it allows printing a node +/// It is useful in cases like in arguments of [`super::call_like_expression`] because it allows printing a node /// a few times to find a proper layout. /// However, the current architecture of the formatter isn't able to do things like this, /// because it will cause the comments printed after the first printing to be lost in the diff --git a/crates/oxc_formatter/src/write/import_expression.rs b/crates/oxc_formatter/src/write/import_expression.rs deleted file mode 100644 index 1f74507c31a94..0000000000000 --- a/crates/oxc_formatter/src/write/import_expression.rs +++ /dev/null @@ -1,21 +0,0 @@ -use oxc_ast::ast::*; - -use crate::{ - ast_nodes::AstNode, - formatter::{Formatter, prelude::*}, - write, -}; - -use super::FormatWrite; - -impl<'a> FormatWrite<'a> for AstNode<'a, ImportExpression<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) { - write!(f, ["import"]); - if let Some(phase) = &self.phase() { - write!(f, [".", phase.as_str()]); - } - - // Use the same logic as CallExpression arguments formatting - write!(f, self.to_arguments()); - } -} diff --git a/crates/oxc_formatter/src/write/mod.rs b/crates/oxc_formatter/src/write/mod.rs index a879089b099e2..f6a0767922e85 100644 --- a/crates/oxc_formatter/src/write/mod.rs +++ b/crates/oxc_formatter/src/write/mod.rs @@ -7,14 +7,13 @@ mod assignment_pattern_property_list; mod binary_like_expression; mod binding_property_list; mod block_statement; -mod call_arguments; +mod call_like_expression; mod class; mod decorators; mod export_declarations; mod function; mod function_type; mod import_declaration; -mod import_expression; mod intersection_type; mod jsx; mod mapped_type; @@ -50,7 +49,7 @@ use crate::{ ast_nodes::{AstNode, AstNodes}, best_fitting, format_args, formatter::{ - Buffer, Format, Formatter, TailwindContextEntry, + Format, Formatter, prelude::*, separated::FormatSeparatedIter, token::number::{NumberFormatOptions, format_number_token}, @@ -64,15 +63,13 @@ use crate::{ utils::{ array::write_array_node, assignment_like::AssignmentLike, - call_expression::is_test_call_expression, conditional::ConditionalLike, expression::ExpressionLeftSide, format_node_without_trailing_comments::FormatNodeWithoutTrailingComments, - member_chain::MemberChain, object::{format_property_key, should_preserve_quote}, statement_body::FormatStatementBody, string::{FormatLiteralStringToken, StringLiteralParentKind}, - tailwindcss::{is_tailwind_function_call, write_tailwind_string_literal}, + tailwindcss::write_tailwind_string_literal, }, write, write::parameters::can_avoid_parentheses, @@ -80,9 +77,7 @@ use crate::{ use self::{ array_expression::FormatArrayExpression, - arrow_function_expression::is_multiline_template_starting_on_same_line, block_statement::is_empty_block, - call_arguments::is_simple_module_import, class::format_grouped_parameters_with_return_type_for_method, object_like::ObjectLike, object_pattern_like::ObjectPatternLike, @@ -232,110 +227,6 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ObjectProperty<'a>> { } } -impl<'a> FormatWrite<'a> for AstNode<'a, CallExpression<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) { - let callee = self.callee(); - let type_arguments = self.type_arguments(); - let arguments = self.arguments(); - let optional = self.optional(); - - // Check if this is a Tailwind function call (e.g., clsx, cn, tw) - let is_tailwind_call = f - .options() - .experimental_tailwindcss - .as_ref() - .is_some_and(|opts| is_tailwind_function_call(&self.callee, opts)); - - // For nested non-Tailwind calls inside a Tailwind context, disable sorting - // to prevent sorting strings inside the nested call's arguments. - // (e.g., `classNames("a", x.includes("\n") ? "b" : "c")` - don't sort "\n") - let was_disabled = - if !is_tailwind_call && let Some(ctx) = f.context_mut().tailwind_context_mut() { - let was = ctx.disabled; - ctx.disabled = true; - Some(was) - } else { - None - }; - - let is_template_literal_single_arg = arguments.len() == 1 - && arguments.first().unwrap().as_expression().is_some_and(|expr| { - is_multiline_template_starting_on_same_line(expr, f.source_text()) - }); - - if !is_template_literal_single_arg - && matches!( - callee.as_ref(), - Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) - ) - && !is_simple_module_import(self.arguments(), f.comments()) - && !is_test_call_expression(self) - { - MemberChain::from_call_expression(self, f).fmt(f); - } else { - let format_inner = format_with(|f| { - // Preserve trailing comments of the callee in the following cases: - // `call /**/()` - // `call /**/()` - if self.type_arguments.is_some() { - write!(f, [callee]); - } else { - write!(f, [FormatNodeWithoutTrailingComments(callee)]); - - if self.arguments.is_empty() { - let callee_trailing_comments = f - .context() - .comments() - .comments_before_character(self.callee.span().end, b'('); - write!(f, FormatTrailingComments::Comments(callee_trailing_comments)); - } - } - write!(f, [optional.then_some("?."), type_arguments]); - - // If this IS a Tailwind function call, push the Tailwind context - let tailwind_ctx_to_push = if is_tailwind_call { - f.options() - .experimental_tailwindcss - .as_ref() - .map(|opts| TailwindContextEntry::new(opts.preserve_whitespace)) - } else { - None - }; - - // Push Tailwind context before formatting arguments - if let Some(ctx) = tailwind_ctx_to_push { - f.context_mut().push_tailwind_context(ctx); - } - - write!(f, arguments); - - // Pop Tailwind context after formatting - if tailwind_ctx_to_push.is_some() { - f.context_mut().pop_tailwind_context(); - } - }); - if matches!(callee.as_ref(), Expression::CallExpression(_)) { - write!(f, [group(&format_inner)]); - } else { - write!(f, [format_inner]); - } - } - - // Restore the previous disabled state - if let Some(was) = was_disabled - && let Some(ctx) = f.context_mut().tailwind_context_mut() - { - ctx.disabled = was; - } - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, NewExpression<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) { - write!(f, ["new", space(), self.callee(), self.type_arguments(), self.arguments()]); - } -} - impl<'a> FormatWrite<'a> for AstNode<'a, MetaProperty<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) { write!(f, [self.meta(), ".", self.property()]);