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
37 changes: 37 additions & 0 deletions crates/oxc_formatter/src/formatter/source_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,43 @@ impl<'a> SourceText<'a> {
false
}

/// Check for a newline after an opening brace `{`.
/// Unlike `has_newline_after`, this method scans through comments to find newlines.
/// This matches Prettier's behavior for detecting newlines in `{ /* comment */\n`.
pub fn has_newline_after_opening_brace(&self, position: u32) -> bool {
let mut iter = self.bytes_from(position + 1).peekable();

while let Some(byte) = iter.next() {
match byte {
b'\n' | b'\r' => return true,
b' ' | b'\t' => {}
b'/' => match iter.peek() {
Some(&b'/') => {
iter.next();
// Line comment: scan until newline or EOF
return iter.any(|b| b == b'\n' || b == b'\r');
}
Some(&b'*') => {
iter.next();
// Block comment: scan for */ and check for newlines
while let Some(b) = iter.next() {
if matches!(b, b'\n' | b'\r') {
return true;
}
if b == b'*' && matches!(iter.peek(), Some(&b'/')) {
iter.next();
break;
}
}
}
_ => return false,
},
_ => return false,
}
}
false
}

// Byte range operations
/// Check if byte range contains specific byte
pub fn bytes_contain(&self, start: u32, end: u32, byte: u8) -> bool {
Expand Down
25 changes: 10 additions & 15 deletions crates/oxc_formatter/src/print/mapped_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use oxc_ast::ast::{TSMappedType, TSMappedTypeModifierOperator};

use crate::{
ast_nodes::AstNode,
formatter::{Formatter, SourceText, prelude::*, trivia::FormatLeadingComments},
formatter::{Formatter, prelude::*, trivia::FormatLeadingComments},
print::semicolon::OptionalSemicolon,
utils::suppressed::FormatSuppressedNode,
write,
Expand All @@ -19,7 +19,15 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSMappedType<'a>> {
let key = self.key();
let constraint = self.constraint();
let name_type = self.name_type();
let should_expand = has_line_break_after_opening_brace(self, f.source_text());
// Check if the user introduced a new line immediately after the opening brace.
// For example, this would break:
// {
// readonly [A in B]: T}
// Because the line break occurs right after `{`. But this would _not_ break:
// { readonly
// [A in B]: T}
// Because the break is not immediately after `{`.
let should_expand = f.source_text().has_newline_after_opening_brace(self.span.start);

let format_inner = format_with(|f| {
if should_expand {
Expand Down Expand Up @@ -83,16 +91,3 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSMappedType<'a>> {
);
}
}

/// Check if the user introduced a new line immediately after the opening brace.
/// For example, this would break:
/// {
/// readonly [A in B]: T}
/// Because the line break occurs right after `{`. But this would _not_ break:
/// { readonly
/// [A in B]: T}
/// Because the break is not immediately after `{`.
fn has_line_break_after_opening_brace(node: &TSMappedType, f: SourceText) -> bool {
// Check if there's a newline immediately after `{` (before any non-whitespace)
f.has_newline_after(node.span.start + 1)
}
73 changes: 37 additions & 36 deletions crates/oxc_formatter/src/utils/assignment_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -957,10 +957,9 @@ fn is_poorly_breakable_member_or_call_chain<'a>(
return false;
}

let is_breakable_type_arguments = match &call_expression.type_arguments {
Some(type_arguments) => is_complex_type_arguments(type_arguments),
None => false,
};
let is_breakable_type_arguments = call_expression
.type_arguments()
.is_some_and(|type_arguments| is_complex_type_arguments(type_arguments, f));

if is_breakable_type_arguments {
return false;
Expand Down Expand Up @@ -1017,44 +1016,46 @@ fn is_short_argument(argument: &Expression, threshold: u16, f: &Formatter) -> bo
/// If the type arguments is complex the function call is breakable.
///
/// NOTE: This function does not follow Prettier exactly.
/// [Prettier applies]: <https://github.com/prettier/prettier/blob/a043ac0d733c4d53f980aa73807a63fc914f23bd/src/language-js/print/assignment.js#L432>
fn is_complex_type_arguments(type_arguments: &TSTypeParameterInstantiation) -> bool {
let is_complex_ts_type = |ts_type: &TSType| {
matches!(
ts_type,
TSType::TSUnionType(_)
| TSType::TSIntersectionType(_)
| TSType::TSTypeLiteral(_)
// NOTE: Prettier does not contain `TSMappedType` in its check.
// But it makes sense to consider mapped types as complex,
// because it is the same as type literals in terms of structure.
| TSType::TSMappedType(_)
)
////// <https://github.com/prettier/prettier/blob/a043ac0d733c4d53f980aa73807a63fc914f23bd/src/language-js/print/assignment.js#L432-L459>
fn is_complex_type_arguments<'a>(
type_arguments: &TSTypeParameterInstantiation<'a>,
f: &Formatter<'_, 'a>,
) -> bool {
let is_complex_ts_type = |ts_type: &TSType| match ts_type {
TSType::TSUnionType(_) | TSType::TSIntersectionType(_) | TSType::TSTypeLiteral(_) => true,
// Check for newlines after `{` in mapped types, as it will expand to multiple lines if so
TSType::TSMappedType(mapped) => f.source_text().has_newline_after(mapped.span.start + 1),
_ => false,
};

if type_arguments.params.len() > 1 {
let is_complex_type_reference = |reference: &TSTypeReference| {
reference.type_arguments.as_ref().is_some_and(|type_arguments| {
type_arguments.params.iter().any(|param| match param {
TSType::TSTypeLiteral(literal) => literal.members.len() > 1,
_ => false,
})
})
};

let params = &type_arguments.params;
if params.len() > 1 {
return true;
}

type_arguments.params.first().is_some_and(|first_argument| {
if is_complex_ts_type(first_argument) {
return true;
}

// NOTE: Prettier checks `willBreak(print(typeArgs))` here.
// Our equivalent is `type_arguments.memoized().inspect(f).will_break()`,
// but we avoid using it because:
// - `inspect(f)` (= `f.intern()`) will update the comment counting state in `f`
// - And resulted IRs are discarded after this check
// So we approximate it by checking if the type arguments contain complex types.
if let TSType::TSTypeReference(type_ref) = first_argument
&& let Some(type_args) = &type_ref.type_arguments
{
return type_args.params.iter().any(is_complex_ts_type);
}
// NOTE: Prettier checks `willBreak(print(typeArgs))` here.
// Our equivalent is `type_arguments.memoized().inspect(f).will_break()`,
// but we avoid using it because:
// - `inspect(f)` (= `f.intern()`) will update the comment counting state in `f`
// - And resulted IRs are discarded after this check
// So we approximate it by checking if the type arguments contain complex types.
if params.first().is_some_and(|param| match param {
TSType::TSTypeReference(reference) => is_complex_type_reference(reference),
ts_type => is_complex_ts_type(ts_type),
}) {
return true;
}

false
})
f.comments().has_comment_in_span(type_arguments.span)
}

/// [Prettier applies]: <https://github.com/prettier/prettier/blob/fde0b49d7866e203ca748c306808a87b7c15548f/src/language-js/print/assignment.js#L278>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const fooRef =
useRef<Record<string, LazyFooThingFD<T, TError> | null | undefined>>(cache);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
assertion_line: 229
---
==================== Input ====================
const fooRef =
useRef<Record<string, LazyFooThingFD<T, TError> | null | undefined>>(cache);

==================== Output ====================
------------------
{ printWidth: 80 }
------------------
const fooRef =
useRef<Record<string, LazyFooThingFD<T, TError> | null | undefined>>(cache);

-------------------
{ printWidth: 100 }
-------------------
const fooRef = useRef<Record<string, LazyFooThingFD<T, TError> | null | undefined>>(cache);

===================== End =====================
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class A {
readonly customHeaderTemplate =
viewChild.required<TemplateRef<{ total: number }>>("customHeader");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
class A {
readonly customHeaderTemplate =
viewChild.required<TemplateRef<{ total: number }>>("customHeader");
}

==================== Output ====================
------------------
{ printWidth: 80 }
------------------
class A {
readonly customHeaderTemplate =
viewChild.required<TemplateRef<{ total: number }>>("customHeader");
}

-------------------
{ printWidth: 100 }
-------------------
class A {
readonly customHeaderTemplate =
viewChild.required<TemplateRef<{ total: number }>>("customHeader");
}

===================== End =====================
Loading