diff --git a/crates/oxc_formatter/src/write/class.rs b/crates/oxc_formatter/src/write/class.rs index 61ee56d888868..518b8bf15c354 100644 --- a/crates/oxc_formatter/src/write/class.rs +++ b/crates/oxc_formatter/src/write/class.rs @@ -112,7 +112,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, MethodDefinition<'a>> { write!(f, "?")?; } - format_grouped_parameters_with_return_type( + format_grouped_parameters_with_return_type_for_method( value.type_parameters(), value.this_param.as_deref(), value.params(), @@ -633,38 +633,75 @@ impl<'a> Format<'a> for ClassPropertySemicolon<'a, '_> { } } -pub fn format_grouped_parameters_with_return_type<'a>( +/// Based on +pub fn format_grouped_parameters_with_return_type_for_method<'a>( type_parameters: Option<&AstNode<'a, TSTypeParameterDeclaration<'a>>>, this_param: Option<&TSThisParameter<'a>>, params: &AstNode<'a, FormalParameters<'a>>, return_type: Option<&AstNode<'a, TSTypeAnnotation<'a>>>, f: &mut Formatter<'_, 'a>, ) -> FormatResult<()> { + write!(f, type_parameters)?; + group(&format_once(|f| { - let mut format_type_parameters = type_parameters.memoized(); let mut format_parameters = params.memoized(); let mut format_return_type = return_type.map(FormatNodeWithoutTrailingComments).memoized(); // Inspect early, in case the `return_type` is formatted before `parameters` // in `should_group_function_parameters`. - format_type_parameters.inspect(f)?; format_parameters.inspect(f)?; - let group_parameters = should_group_function_parameters( - type_parameters.map(AsRef::as_ref), - params.parameters_count() + usize::from(this_param.is_some()), - return_type.map(AsRef::as_ref), - &mut format_return_type, - f, - )?; + let should_break_parameters = should_break_function_parameters(params, f); + let should_group_parameters = should_break_parameters + || should_group_function_parameters( + type_parameters.map(AsRef::as_ref), + params.parameters_count() + usize::from(this_param.is_some()), + return_type.map(AsRef::as_ref), + &mut format_return_type, + f, + )?; - if group_parameters { - write!(f, [group(&format_args!(format_type_parameters, format_parameters))]) + if should_group_parameters { + write!(f, [group(&format_parameters).should_expand(should_break_parameters)])?; } else { - write!(f, [format_type_parameters, format_parameters]) - }?; + write!(f, [format_parameters])?; + } write!(f, [format_return_type]) })) .fmt(f) } + +/// Decide if a constructor parameter list should prefer a +/// multi-line layout. +/// +/// If there is more than one parameter, and any parameter has a modifier +/// (e.g. a TypeScript parameter property like `public/private/protected`, or +/// `readonly`, etc.), we break the parameters onto multiple lines. +// +/// Examples +/// -------- +/// Multiple params with a modifier → break: +/// +/// ```ts +/// // input +/// constructor(public x: number, y: number) {} +/// +/// // preferred layout +/// constructor( +/// public x: number, +/// y: number, +/// ) {} +/// ``` +/// +/// Single param with a modifier → keep inline: +/// +/// ```ts +/// constructor(private id: string) {} +/// ``` +fn should_break_function_parameters<'a>( + params: &AstNode<'a, FormalParameters<'a>>, + f: &mut Formatter<'_, 'a>, +) -> bool { + params.parameters_count() > 1 && params.items().iter().any(|param| param.has_modifier()) +} diff --git a/crates/oxc_formatter/src/write/function_type.rs b/crates/oxc_formatter/src/write/function_type.rs new file mode 100644 index 0000000000000..28acd3cb072f4 --- /dev/null +++ b/crates/oxc_formatter/src/write/function_type.rs @@ -0,0 +1,177 @@ +use oxc_ast::ast::*; + +use crate::{ + ast_nodes::AstNode, + format_args, + formatter::{Formatter, prelude::*}, + utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments, + write, + write::function::should_group_function_parameters, +}; + +use super::FormatWrite; + +impl<'a> FormatWrite<'a> for AstNode<'a, TSFunctionType<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + format_grouped_parameters_with_return_type( + self.type_parameters(), + self.this_param.as_deref(), + self.params(), + Some(self.return_type()), + /* is_function_or_constructor_type */ true, + f, + ) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TSConstructorType<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let r#abstract = self.r#abstract(); + let type_parameters = self.type_parameters(); + let params = self.params(); + let return_type = self.return_type(); + + write!( + f, + group(&format_args!( + r#abstract.then_some("abstract "), + "new", + space(), + &format_once(|f| { + format_grouped_parameters_with_return_type( + self.type_parameters(), + None, + self.params(), + Some(self.return_type()), + /* is_function_or_constructor_type */ true, + f, + ) + }) + )) + ) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TSCallSignatureDeclaration<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + format_grouped_parameters_with_return_type( + self.type_parameters(), + self.this_param.as_deref(), + self.params(), + self.return_type(), + /* is_function_or_constructor_type */ false, + f, + ) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TSMethodSignature<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let format_inner = format_once(|f| { + match self.kind() { + TSMethodSignatureKind::Method => {} + TSMethodSignatureKind::Get => { + write!(f, ["get", space()])?; + } + TSMethodSignatureKind::Set => { + write!(f, ["set", space()])?; + } + } + if self.computed() { + write!(f, "[")?; + } + write!(f, self.key())?; + if self.computed() { + write!(f, "]")?; + } + if self.optional() { + write!(f, "?")?; + } + + let mut format_type_parameters = self.type_parameters().memoized(); + let mut format_parameters = self.params().memoized(); + format_type_parameters.inspect(f)?; + format_parameters.inspect(f)?; + + let mut format_return_type = self.return_type().memoized(); + + let should_group_parameters = should_group_function_parameters( + self.type_parameters.as_deref(), + self.params.parameters_count() + usize::from(self.this_param.is_some()), + self.return_type.as_deref(), + &mut format_return_type, + f, + )?; + + if should_group_parameters { + write!(f, group(&format_args!(&format_type_parameters, &format_parameters)))?; + } else { + write!(f, [format_type_parameters, format_parameters])?; + } + + write!(f, group(&format_return_type)) + }); + + write!(f, group(&format_inner)) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TSConstructSignatureDeclaration<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + write!( + f, + group(&format_args!( + "new", + space(), + &format_once(|f| { + format_grouped_parameters_with_return_type( + self.type_parameters(), + None, + self.params(), + self.return_type(), + /* is_function_or_constructor_type */ false, + f, + ) + }) + )) + ) + } +} + +/// Based on +pub fn format_grouped_parameters_with_return_type<'a>( + type_parameters: Option<&AstNode<'a, TSTypeParameterDeclaration<'a>>>, + this_param: Option<&TSThisParameter<'a>>, + params: &AstNode<'a, FormalParameters<'a>>, + return_type: Option<&AstNode<'a, TSTypeAnnotation<'a>>>, + is_function_or_constructor_type: bool, + f: &mut Formatter<'_, 'a>, +) -> FormatResult<()> { + group(&format_once(|f| { + let mut format_type_parameters = type_parameters.memoized(); + let mut format_parameters = params.memoized(); + let mut format_return_type = return_type.map(FormatNodeWithoutTrailingComments).memoized(); + + // Inspect early, in case the `return_type` is formatted before `parameters` + // in `should_group_function_parameters`. + format_type_parameters.inspect(f)?; + format_parameters.inspect(f)?; + + let group_parameters = should_group_function_parameters( + type_parameters.map(AsRef::as_ref), + params.parameters_count() + usize::from(this_param.is_some()), + return_type.map(AsRef::as_ref), + &mut format_return_type, + f, + )?; + + if group_parameters { + write!(f, [group(&format_args!(format_type_parameters, format_parameters))]) + } else { + write!(f, [format_type_parameters, format_parameters]) + }?; + + write!(f, [is_function_or_constructor_type.then_some(space()), format_return_type]) + })) + .fmt(f) +} diff --git a/crates/oxc_formatter/src/write/mod.rs b/crates/oxc_formatter/src/write/mod.rs index 3bd23890a99d5..fc8c35524d93c 100644 --- a/crates/oxc_formatter/src/write/mod.rs +++ b/crates/oxc_formatter/src/write/mod.rs @@ -12,6 +12,7 @@ mod class; mod decorators; mod export_declarations; mod function; +mod function_type; mod import_declaration; mod import_expression; mod intersection_type; @@ -82,7 +83,7 @@ use crate::{ use self::{ array_expression::FormatArrayExpression, arrow_function_expression::is_multiline_template_starting_on_same_line, - class::format_grouped_parameters_with_return_type, + class::format_grouped_parameters_with_return_type_for_method, function::should_group_function_parameters, object_like::ObjectLike, object_pattern_like::ObjectPatternLike, @@ -190,7 +191,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ObjectProperty<'a>> { format_property_key(self.key(), f)?; } - format_grouped_parameters_with_return_type( + format_grouped_parameters_with_return_type_for_method( value.type_parameters(), value.this_param.as_deref(), value.params(), @@ -1541,63 +1542,6 @@ impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSSignature<'a>>> { } } -impl<'a> FormatWrite<'a> for AstNode<'a, TSCallSignatureDeclaration<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - format_grouped_parameters_with_return_type( - self.type_parameters(), - None, - self.params(), - self.return_type(), - f, - ) - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, TSMethodSignature<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - match self.kind() { - TSMethodSignatureKind::Method => {} - TSMethodSignatureKind::Get => { - write!(f, ["get", space()])?; - } - TSMethodSignatureKind::Set => { - write!(f, ["set", space()])?; - } - } - if self.computed() { - write!(f, "[")?; - } - write!(f, self.key())?; - if self.computed() { - write!(f, "]")?; - } - if self.optional() { - write!(f, "?")?; - } - - format_grouped_parameters_with_return_type( - self.type_parameters(), - self.this_param.as_deref(), - self.params(), - self.return_type(), - f, - ) - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, TSConstructSignatureDeclaration<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, ["new", space()])?; - format_grouped_parameters_with_return_type( - self.type_parameters(), - None, - self.params(), - self.return_type(), - f, - ) - } -} - impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSInterfaceHeritage<'a>>> { fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { let last_index = self.len().saturating_sub(1); @@ -1733,62 +1677,6 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSImportTypeQualifiedName<'a>> { } } -impl<'a> FormatWrite<'a> for AstNode<'a, TSFunctionType<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - let format_inner = format_with(|f| { - let type_parameters = self.type_parameters(); - write!(f, type_parameters)?; - - let params = self.params(); - let return_type = self.return_type(); - let mut format_parameters = params.memoized(); - let mut format_return_type = return_type.memoized(); - - // Inspect early, in case the `return_type` is formatted before `parameters` - // in `should_group_function_parameters`. - format_parameters.inspect(f)?; - - let group_parameters = should_group_function_parameters( - type_parameters.map(AsRef::as_ref), - params.items.len() - + usize::from(params.rest.is_some()) - + usize::from(self.this_param.is_some()), - Some(&self.return_type), - &mut format_return_type, - f, - )?; - - if group_parameters { - write!(f, [group(&format_parameters)]) - } else { - write!(f, [format_parameters]) - }?; - - write!(f, [space(), format_return_type]) - }); - - write!(f, group(&format_inner)) - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, TSConstructorType<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - let r#abstract = self.r#abstract(); - let type_parameters = self.type_parameters(); - let params = self.params(); - let return_type = self.return_type(); - - if r#abstract { - write!(f, ["abstract", space()])?; - } - write!( - f, - [group(&format_args!("new", space(), type_parameters, params, space(), return_type))] - ); - Ok(()) - } -} - impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeAssertion<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { let break_after_cast = !matches!( diff --git a/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts b/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts index d98eb66615314..0e220bd429df8 100644 --- a/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts +++ b/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts @@ -22,3 +22,29 @@ type A2 = { }> } +class A3 { + constructor( + public eventName: string, + data?: object, + ) { } +} + +class A4 { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >( + eventName: string, data?: object + ) { + } +} + +const A5 = { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >( + eventName: string, + data?: object, + ) { } +} diff --git a/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts.snap b/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts.snap index 4194336bb782b..f0040e413f387 100644 --- a/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts.snap +++ b/crates/oxc_formatter/tests/fixtures/ts/method-signatures/grouped.ts.snap @@ -26,6 +26,32 @@ type A2 = { }> } +class A3 { + constructor( + public eventName: string, + data?: object, + ) { } +} + +class A4 { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >( + eventName: string, data?: object + ) { + } +} + +const A5 = { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >( + eventName: string, + data?: object, + ) { } +} ==================== Output ==================== type A = { @@ -46,4 +72,25 @@ type A2 = { }>; }; +class A3 { + constructor( + public eventName: string, + data?: object, + ) {} +} + +class A4 { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >(eventName: string, data?: object) {} +} + +const A5 = { + publicLog< + E extends ClassifiedEvent>, + T extends IGDPRProperty, + >(eventName: string, data?: object) {}, +}; + ===================== End =====================