Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions crates/oxc_formatter/src/write/arrow_function_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,22 +855,15 @@ fn format_signature<'a, 'b>(
cache_mode: FunctionBodyCacheMode,
) -> impl Format<'a> + 'b {
format_with(move |f| {
let formatted_async_token =
format_with(|f| if arrow.r#async() { write!(f, ["async", space()]) } else { Ok(()) });

let formatted_parameters =
format_with(|f| write!(f, [arrow.type_parameters(), arrow.params()]));

let format_return_type = format_with(|f| write!(f, arrow.return_type()));

let signatures = format_once(|f| {
write!(
f,
[group(&format_args!(
maybe_space(!is_first_in_chain),
formatted_async_token,
group(&formatted_parameters),
group(&format_return_type)
arrow.r#async().then_some("async "),
arrow.type_parameters(),
arrow.params(),
group(&arrow.return_type())
))]
)
});
Expand Down
6 changes: 3 additions & 3 deletions crates/oxc_formatter/src/write/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::{

use super::{
FormatWrite,
type_parameters::{FormatTsTypeParameters, FormatTsTypeParametersOptions},
type_parameters::{FormatTSTypeParameters, FormatTSTypeParametersOptions},
};

impl<'a> FormatWrite<'a> for AstNode<'a, ClassBody<'a>> {
Expand Down Expand Up @@ -314,9 +314,9 @@ impl<'a> Format<'a> for FormatClass<'a, '_> {
if let Some(type_parameters) = &type_parameters {
write!(
f,
FormatTsTypeParameters::new(
FormatTSTypeParameters::new(
type_parameters,
FormatTsTypeParametersOptions {
FormatTSTypeParametersOptions {
group_id: type_parameters_id,
is_type_or_interface_decl: false
}
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_formatter/src/write/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ impl<'a> FormatWrite<'a> for FormatFunction<'a, '_> {
if self.r#async() {
write!(f, ["async", space()])?;
}

write!(
f,
[
Expand Down
43 changes: 4 additions & 39 deletions crates/oxc_formatter/src/write/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ use self::{
object_pattern_like::ObjectPatternLike,
parameter_list::{ParameterLayout, ParameterList},
semicolon::{ClassPropertySemicolon, OptionalSemicolon},
type_parameters::{FormatTsTypeParameters, FormatTsTypeParametersOptions},
type_parameters::{FormatTSTypeParameters, FormatTSTypeParametersOptions},
utils::{
array::{TrailingSeparatorMode, write_array_node},
statement_body::FormatStatementBody,
Expand Down Expand Up @@ -1377,44 +1377,9 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSQualifiedName<'a>> {
}
}

impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterInstantiation<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
write!(f, "<")?;
for (i, param) in self.params().iter().enumerate() {
if i != 0 {
write!(f, [",", space()])?;
}
write!(f, param)?;
}
write!(f, ">")
}
}

impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameter<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
if self.r#const() {
write!(f, ["const", space()])?;
}
if self.r#in() {
write!(f, ["in", space()])?;
}
if self.out() {
write!(f, ["out", space()])?;
}
write!(f, self.name())?;
if let Some(constraint) = &self.constraint() {
write!(f, [space(), "extends", space(), constraint])?;
}
if let Some(default) = &self.default() {
write!(f, [space(), "=", space(), default])?;
}
Ok(())
}
}

impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterDeclaration<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
write!(f, ["<", self.params(), ">"])
FormatTSTypeParameters::new(self, FormatTSTypeParametersOptions::default()).fmt(f)
}
}

Expand Down Expand Up @@ -1463,9 +1428,9 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSInterfaceDeclaration<'a>> {
if let Some(type_parameters) = type_parameters {
write!(
f,
FormatTsTypeParameters::new(
FormatTSTypeParameters::new(
type_parameters,
FormatTsTypeParametersOptions {
FormatTSTypeParametersOptions {
group_id: type_parameter_group,
is_type_or_interface_decl: true
}
Expand Down
213 changes: 205 additions & 8 deletions crates/oxc_formatter/src/write/type_parameters.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Pointer;

use oxc_allocator::{Address, Vec};
use oxc_ast::{AstKind, ast::*};

Expand All @@ -9,9 +11,49 @@ use crate::{
},
generated::ast_nodes::{AstNode, AstNodes},
options::{FormatTrailingCommas, TrailingSeparator},
utils::call_expression::is_test_call_expression,
write,
};

use super::FormatWrite;

impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameter<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
if self.r#const() {
write!(f, ["const", space()])?;
}
if self.r#in() {
write!(f, ["in", space()])?;
}
if self.out() {
write!(f, ["out", space()])?;
}
write!(f, self.name())?;

if let Some(constraint) = &self.constraint() {
let group_id = f.group_id("constraint");

write!(
f,
[
space(),
"extends",
group(&indent(&format_args!(
line_suffix_boundary(),
soft_line_break_or_space()
)))
.with_group_id(Some(group_id)),
indent_if_group_breaks(&constraint, group_id)
]
)?;
}
if let Some(default) = &self.default() {
write!(f, [space(), "=", space(), default])?;
}
Ok(())
}
}

impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSTypeParameter<'a>>> {
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
// Type parameter lists of arrow function expressions have to include at least one comma
Expand All @@ -37,35 +79,190 @@ impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSTypeParameter<'a>>> {
}
}

pub struct FormatTsTypeParametersOptions {
#[derive(Default)]
pub struct FormatTSTypeParametersOptions {
pub group_id: Option<GroupId>,
pub is_type_or_interface_decl: bool,
}

pub struct FormatTsTypeParameters<'a, 'b> {
pub struct FormatTSTypeParameters<'a, 'b> {
decl: &'b AstNode<'a, TSTypeParameterDeclaration<'a>>,
options: FormatTsTypeParametersOptions,
options: FormatTSTypeParametersOptions,
}

impl<'a, 'b> FormatTsTypeParameters<'a, 'b> {
impl<'a, 'b> FormatTSTypeParameters<'a, 'b> {
pub fn new(
decl: &'b AstNode<'a, TSTypeParameterDeclaration<'a>>,
options: FormatTsTypeParametersOptions,
options: FormatTSTypeParametersOptions,
) -> Self {
Self { decl, options }
}
}

impl<'a> Format<'a> for FormatTsTypeParameters<'a, '_> {
impl<'a> Format<'a> for FormatTSTypeParameters<'a, '_> {
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
if self.decl.params().is_empty() && self.options.is_type_or_interface_decl {
let params = self.decl.params();
if params.is_empty() && self.options.is_type_or_interface_decl {
write!(f, "<>")
} else {
write!(
f,
[group(&format_args!("<", soft_block_indent(&self.decl.params()), ">"))
[group(&format_args!("<", format_once(|f| {
if matches!( self.decl.parent.parent().parent(), AstNodes::CallExpression(call) if is_test_call_expression(call))
{
f.join_nodes_with_space().entries_with_trailing_separator(params, ",", TrailingSeparator::Omit).finish()
} else {
soft_block_indent(&params).fmt(f)
}
}), ">"))
.with_group_id(self.options.group_id)]
)
}
}
}

impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterInstantiation<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
let params = self.params();

if params.is_empty() {
// This shouldn't happen in valid TypeScript code, but handle it gracefully
return write!(
f,
[&group(&format_args!(
"<",
format_dangling_comments(self.span).with_soft_block_indent(),
">"
))]
);
}

// Check if this is in the context of an arrow function variable
let is_arrow_function_vars = is_arrow_function_variable_type_argument(self);

// Check if the first (and only) argument can be hugged
let first_arg_can_be_hugged = if params.len() == 1 {
if let Some(first_type) = params.first() {
matches!(first_type.as_ref(), TSType::TSNullKeyword(_))
|| should_hug_single_type(first_type.as_ref())
} else {
false
}
} else {
false
};

let format_params = format_once(|f| {
f.join_with(&soft_line_break_or_space())
.entries_with_trailing_separator(params, ",", TrailingSeparator::Disallowed)
.finish()
});

let should_inline =
!is_arrow_function_vars && (params.is_empty() || first_arg_can_be_hugged);

if should_inline {
write!(f, ["<", format_params, ">"])
} else {
write!(f, [group(&format_args!("<", soft_block_indent(&format_params), ">"))])
}
}
}

/// Check if a TSType is a simple type (primitives, keywords, simple references)
fn is_simple_type(ty: &TSType) -> bool {
match ty {
TSType::TSAnyKeyword(_)
| TSType::TSNullKeyword(_)
| TSType::TSThisType(_)
| TSType::TSVoidKeyword(_)
| TSType::TSNumberKeyword(_)
| TSType::TSBooleanKeyword(_)
| TSType::TSBigIntKeyword(_)
| TSType::TSStringKeyword(_)
| TSType::TSSymbolKeyword(_)
| TSType::TSNeverKeyword(_)
| TSType::TSObjectKeyword(_)
| TSType::TSUndefinedKeyword(_)
| TSType::TSTemplateLiteralType(_)
| TSType::TSLiteralType(_)
| TSType::TSUnknownKeyword(_) => true,
TSType::TSTypeReference(reference) => {
// Simple reference without type arguments
reference.type_arguments.is_none()
}
_ => false,
}
}

/// Check if a TSType is object-like (object literal, mapped type, etc.)
fn is_object_like_type(ty: &TSType) -> bool {
matches!(ty, TSType::TSTypeLiteral(_) | TSType::TSMappedType(_))
}

/// Check if a single type should be "hugged" (kept inline)
fn should_hug_single_type(ty: &TSType) -> bool {
// Simple types and object-like types can be hugged
if is_simple_type(ty) || is_object_like_type(ty) {
return true;
}

// Check for union types with mostly void types and one object type
// (e.g., `SomeType<ObjectType | null | undefined>`)
if let TSType::TSUnionType(union_type) = ty {
let types = &union_type.types;

// Must have at least 2 types
if types.len() < 2 {
return types.len() == 1 && should_hug_single_type(&types[0]);
}

let has_object_type = types
.iter()
.any(|t| matches!(t, TSType::TSTypeLiteral(_) | TSType::TSTypeReference(_)));

let void_count = types
.iter()
.filter(|t| {
matches!(
t,
TSType::TSVoidKeyword(_)
| TSType::TSNullKeyword(_)
| TSType::TSUndefinedKeyword(_)
)
})
.count();

// Union is huggable if it's mostly void types with one object/reference type
(types.len() - 1 == void_count && has_object_type) || types.len() == 1
} else {
false
}
}

/// Check if this type parameter instantiation is in an arrow function variable context
///
/// This detects patterns like:
/// ```typescript
/// const foo: SomeThing<{ [P in "x" | "y"]: number }> = () => {};
/// ```
fn is_arrow_function_variable_type_argument<'a>(
node: &AstNode<'a, TSTypeParameterInstantiation<'a>>,
) -> bool {
let Some(first) = node.params().first() else { unreachable!() };

// Skip check for single object-like types
if node.params().len() == 1 && is_object_like_type(first.as_ref()) {
return false;
}

matches!(
&node.parent,
AstNodes::TSTypeAnnotation(type_annotation)
if matches!(
&type_annotation.parent,
AstNodes::VariableDeclarator(var_decl)
if matches!(&var_decl.init, Some(Expression::ArrowFunctionExpression(_)))
)
)
}
4 changes: 1 addition & 3 deletions tasks/coverage/snapshots/formatter_typescript.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ commit: 261630d6

formatter_typescript Summary:
AST Parsed : 8816/8816 (100.00%)
Positive Passed: 8788/8816 (99.68%)
Positive Passed: 8789/8816 (99.69%)
Mismatch: tasks/coverage/typescript/tests/cases/compiler/amdLikeInputDeclarationEmit.ts

Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/arrayFromAsync.ts
Expand All @@ -15,8 +15,6 @@ Mismatch: tasks/coverage/typescript/tests/cases/compiler/complexNarrowingWithAny

Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitCastReusesTypeNode4.ts

Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitShadowingInferNotRenamed.ts

Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/genericTypeAssertions3.ts
Unexpected token
Mismatch: tasks/coverage/typescript/tests/cases/compiler/jsxNamespaceGlobalReexport.tsx
Expand Down
Loading
Loading