Skip to content

Commit f52863d

Browse files
committed
refactor(formatter): improve handling of type cast node (#14815)
1 parent 302c20c commit f52863d

File tree

3 files changed

+65
-43
lines changed

3 files changed

+65
-43
lines changed

crates/oxc_formatter/src/formatter/comments.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,14 @@ impl<'a> Comments<'a> {
386386
const TYPE_PATTERN: &[u8] = b"@type";
387387
const SATISFIES_PATTERN: &[u8] = b"@satisfies";
388388

389+
/// Checks if a pattern matches at the given position.
390+
fn matches_pattern_at(bytes: &[u8], pos: usize, pattern: &[u8]) -> bool {
391+
bytes[pos..].starts_with(pattern)
392+
&& bytes
393+
.get(pos + pattern.len())
394+
.is_some_and(|&byte| byte.is_ascii_whitespace() || byte == b'{')
395+
}
396+
389397
if !matches!(comment.content, CommentContent::Jsdoc) {
390398
return false;
391399
}
@@ -459,9 +467,3 @@ impl<'a> Comments<'a> {
459467
self.view_limit = limit;
460468
}
461469
}
462-
463-
/// Checks if a pattern matches at the given position.
464-
fn matches_pattern_at(bytes: &[u8], pos: usize, pattern: &[u8]) -> bool {
465-
bytes[pos..].starts_with(pattern)
466-
&& matches!(bytes.get(pos + pattern.len()), Some(b' ' | b'\t' | b'\n' | b'\r' | b'{'))
467-
}

crates/oxc_formatter/src/utils/member_chain/mod.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ use crate::{
2323
use oxc_ast::{AstKind, ast::*};
2424
use oxc_span::{GetSpan, Span};
2525

26+
use super::typecast::is_type_cast_node;
27+
2628
#[derive(Debug)]
2729
pub struct MemberChain<'a, 'b> {
2830
root: &'b AstNode<'a, CallExpression<'a>>,
@@ -419,13 +421,7 @@ fn chain_members_iter<'a, 'b>(
419421

420422
let expression = next.take()?;
421423

422-
if f.comments().get_type_cast_comment_index(expression.span()).is_some()
423-
|| f.comments()
424-
.printed_comments()
425-
.last()
426-
.is_some_and(|c| f.comments().is_type_cast_comment(c))
427-
&& f.source_text().next_non_whitespace_byte_is(expression.span().end, b')')
428-
{
424+
if is_type_cast_node(expression, f).is_some() {
429425
return ChainMember::Node(expression).into();
430426
}
431427

crates/oxc_formatter/src/utils/typecast.rs

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,42 @@ use crate::{
1616
},
1717
};
1818

19-
/// Formats a node with TypeScript type cast comments if present.
20-
///
21-
/// This function handles the formatting of JSDoc type cast comments that appear
22-
/// immediately before parenthesized expressions, creating patterns like:
23-
/// `(/** @type {string} */ value)` or `(/** @type {number} */ (expression))`
19+
/// Checks if a node is a type cast node and returns the comments to be printed.
2420
///
25-
/// The function:
26-
/// 1. Checks if there's a closing parenthesis after the node (indicating a type cast)
27-
/// 2. Looks for associated type cast comments that precede the node
28-
/// 3. Wraps the node in parentheses with proper formatting and indentation
29-
/// 4. Handles both object/array expressions and other expression types differently
21+
/// This function detects if a node is part of a TypeScript type cast pattern
22+
/// by checking for JSDoc type cast comments and proper parenthesis structure.
3023
///
31-
/// Returns `Ok(true)` if the node was formatted as a type cast, `Ok(false)` otherwise.
32-
/// This allows callers to know whether they need to apply their own formatting.
33-
pub fn format_type_cast_comment_node<'a, T>(
34-
node: &(impl Format<'a, T> + GetSpan),
35-
is_object_or_array_expression: bool,
36-
f: &mut Formatter<'_, 'a>,
37-
) -> FormatResult<bool> {
24+
/// Returns:
25+
/// - `Some(&[])` if the node is a type cast node but no comments need to be printed
26+
/// - `Some(&[Comment, ...])` if the node is a type cast node with comments to print
27+
/// - `None` if the node is not a type cast node
28+
pub fn is_type_cast_node<'a>(node: &impl GetSpan, f: &Formatter<'_, 'a>) -> Option<&'a [Comment]> {
3829
let comments = f.context().comments();
3930
let span = node.span();
4031
let source = f.source_text();
4132

33+
// Check if there's a closing parenthesis after the node (possibly after comments)
4234
if !source.next_non_whitespace_byte_is(span.end, b')') {
4335
let comments_after_node = comments.comments_after(span.end);
4436
let mut start = span.end;
4537
// Skip comments after the node to find the next non-whitespace byte whether it's a `)`
4638
for comment in comments_after_node {
47-
if !source.all_bytes_match(start, comment.span.start, |b| b.is_ascii_whitespace()) {
39+
if !source.bytes_range(start, comment.span.start).trim_ascii_start().is_empty() {
4840
break;
4941
}
5042
start = comment.span.end;
5143
}
5244
// Still not a `)`, return early because it's not a type cast
5345
if !source.next_non_whitespace_byte_is(start, b')') {
54-
return Ok(false);
46+
return None;
5547
}
5648
}
5749

50+
// Check for type cast comment in printed or unprinted comments
5851
if !comments.is_handled_type_cast_comment()
5952
&& let Some(last_printed_comment) = comments.printed_comments().last()
6053
&& last_printed_comment.span.end <= span.start
61-
&& f.source_text().next_non_whitespace_byte_is(last_printed_comment.span.end, b'(')
54+
&& source.next_non_whitespace_byte_is(last_printed_comment.span.end, b'(')
6255
&& f.comments().is_type_cast_comment(last_printed_comment)
6356
{
6457
// Get the source text from the end of type cast comment to the node span
@@ -68,10 +61,11 @@ pub fn format_type_cast_comment_node<'a, T>(
6861
// ^^^^
6962
// Should wrap for `baz` rather than `baz.zoo`
7063
if has_closed_parentheses(node_source_text) {
71-
return Ok(false);
64+
None
65+
} else {
66+
// Type cast node, but comment was already printed
67+
Some(&[])
7268
}
73-
74-
f.context_mut().comments_mut().mark_as_type_cast_node(node);
7569
} else if let Some(type_cast_comment_index) = comments.get_type_cast_comment_index(span) {
7670
let comments = f.context().comments().unprinted_comments();
7771
let type_cast_comment = &comments[type_cast_comment_index];
@@ -83,19 +77,49 @@ pub fn format_type_cast_comment_node<'a, T>(
8377
// ^^^^
8478
// Should wrap for `baz` rather than `baz.zoo`
8579
if has_closed_parentheses(node_source_text) {
86-
return Ok(false);
80+
None
81+
} else {
82+
// Type cast node with comments to print
83+
Some(&comments[..=type_cast_comment_index])
8784
}
88-
89-
let type_cast_comments = &comments[..=type_cast_comment_index];
90-
91-
write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?;
92-
f.context_mut().comments_mut().mark_as_type_cast_node(node);
93-
// If the printed cast comment is already handled, return early to avoid infinite recursion.
9485
} else {
9586
// No typecast comment
87+
None
88+
}
89+
}
90+
91+
/// Formats a node with TypeScript type cast comments if present.
92+
///
93+
/// This function handles the formatting of JSDoc type cast comments that appear
94+
/// immediately before parenthesized expressions, creating patterns like:
95+
/// `(/** @type {string} */ value)` or `(/** @type {number} */ (expression))`
96+
///
97+
/// The function:
98+
/// 1. Checks if there's a closing parenthesis after the node (indicating a type cast)
99+
/// 2. Looks for associated type cast comments that precede the node
100+
/// 3. Wraps the node in parentheses with proper formatting and indentation
101+
/// 4. Handles both object/array expressions and other expression types differently
102+
///
103+
/// Returns `Ok(true)` if the node was formatted as a type cast, `Ok(false)` otherwise.
104+
/// This allows callers to know whether they need to apply their own formatting.
105+
pub fn format_type_cast_comment_node<'a, T>(
106+
node: &(impl Format<'a, T> + GetSpan),
107+
is_object_or_array_expression: bool,
108+
f: &mut Formatter<'_, 'a>,
109+
) -> FormatResult<bool> {
110+
// Check if this is a type cast node and get the comments to print
111+
let Some(type_cast_comments) = is_type_cast_node(node, f) else {
96112
return Ok(false);
113+
};
114+
115+
// Print the type cast comments if any
116+
if !type_cast_comments.is_empty() {
117+
write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?;
97118
}
98119

120+
let span = node.span();
121+
f.context_mut().comments_mut().mark_as_type_cast_node(node);
122+
99123
// https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/print/estree.js#L117-L120
100124
if is_object_or_array_expression && !f.comments().has_comment_before(span.start) {
101125
write!(f, group(&format_args!("(", &format_once(|f| node.fmt(f)), ")")))?;

0 commit comments

Comments
 (0)