From f52863d6e41d2fd0608ea25b39044cbec681ac19 Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Tue, 21 Oct 2025 04:36:32 +0000 Subject: [PATCH] refactor(formatter): improve handling of type cast node (#14815) --- .../oxc_formatter/src/formatter/comments.rs | 14 ++-- .../src/utils/member_chain/mod.rs | 10 +-- crates/oxc_formatter/src/utils/typecast.rs | 84 ++++++++++++------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/crates/oxc_formatter/src/formatter/comments.rs b/crates/oxc_formatter/src/formatter/comments.rs index 534fcd4d46cfa..d3933a40380aa 100644 --- a/crates/oxc_formatter/src/formatter/comments.rs +++ b/crates/oxc_formatter/src/formatter/comments.rs @@ -386,6 +386,14 @@ impl<'a> Comments<'a> { const TYPE_PATTERN: &[u8] = b"@type"; const SATISFIES_PATTERN: &[u8] = b"@satisfies"; + /// Checks if a pattern matches at the given position. + fn matches_pattern_at(bytes: &[u8], pos: usize, pattern: &[u8]) -> bool { + bytes[pos..].starts_with(pattern) + && bytes + .get(pos + pattern.len()) + .is_some_and(|&byte| byte.is_ascii_whitespace() || byte == b'{') + } + if !matches!(comment.content, CommentContent::Jsdoc) { return false; } @@ -459,9 +467,3 @@ impl<'a> Comments<'a> { self.view_limit = limit; } } - -/// Checks if a pattern matches at the given position. -fn matches_pattern_at(bytes: &[u8], pos: usize, pattern: &[u8]) -> bool { - bytes[pos..].starts_with(pattern) - && matches!(bytes.get(pos + pattern.len()), Some(b' ' | b'\t' | b'\n' | b'\r' | b'{')) -} diff --git a/crates/oxc_formatter/src/utils/member_chain/mod.rs b/crates/oxc_formatter/src/utils/member_chain/mod.rs index 3604a1dd6202d..e93db632ed829 100644 --- a/crates/oxc_formatter/src/utils/member_chain/mod.rs +++ b/crates/oxc_formatter/src/utils/member_chain/mod.rs @@ -23,6 +23,8 @@ use crate::{ use oxc_ast::{AstKind, ast::*}; use oxc_span::{GetSpan, Span}; +use super::typecast::is_type_cast_node; + #[derive(Debug)] pub struct MemberChain<'a, 'b> { root: &'b AstNode<'a, CallExpression<'a>>, @@ -419,13 +421,7 @@ fn chain_members_iter<'a, 'b>( let expression = next.take()?; - if f.comments().get_type_cast_comment_index(expression.span()).is_some() - || f.comments() - .printed_comments() - .last() - .is_some_and(|c| f.comments().is_type_cast_comment(c)) - && f.source_text().next_non_whitespace_byte_is(expression.span().end, b')') - { + if is_type_cast_node(expression, f).is_some() { return ChainMember::Node(expression).into(); } diff --git a/crates/oxc_formatter/src/utils/typecast.rs b/crates/oxc_formatter/src/utils/typecast.rs index 801f83f13a423..375c3be3c59e7 100644 --- a/crates/oxc_formatter/src/utils/typecast.rs +++ b/crates/oxc_formatter/src/utils/typecast.rs @@ -16,49 +16,42 @@ use crate::{ }, }; -/// Formats a node with TypeScript type cast comments if present. -/// -/// This function handles the formatting of JSDoc type cast comments that appear -/// immediately before parenthesized expressions, creating patterns like: -/// `(/** @type {string} */ value)` or `(/** @type {number} */ (expression))` +/// Checks if a node is a type cast node and returns the comments to be printed. /// -/// The function: -/// 1. Checks if there's a closing parenthesis after the node (indicating a type cast) -/// 2. Looks for associated type cast comments that precede the node -/// 3. Wraps the node in parentheses with proper formatting and indentation -/// 4. Handles both object/array expressions and other expression types differently +/// This function detects if a node is part of a TypeScript type cast pattern +/// by checking for JSDoc type cast comments and proper parenthesis structure. /// -/// Returns `Ok(true)` if the node was formatted as a type cast, `Ok(false)` otherwise. -/// This allows callers to know whether they need to apply their own formatting. -pub fn format_type_cast_comment_node<'a, T>( - node: &(impl Format<'a, T> + GetSpan), - is_object_or_array_expression: bool, - f: &mut Formatter<'_, 'a>, -) -> FormatResult { +/// Returns: +/// - `Some(&[])` if the node is a type cast node but no comments need to be printed +/// - `Some(&[Comment, ...])` if the node is a type cast node with comments to print +/// - `None` if the node is not a type cast node +pub fn is_type_cast_node<'a>(node: &impl GetSpan, f: &Formatter<'_, 'a>) -> Option<&'a [Comment]> { let comments = f.context().comments(); let span = node.span(); let source = f.source_text(); + // Check if there's a closing parenthesis after the node (possibly after comments) if !source.next_non_whitespace_byte_is(span.end, b')') { let comments_after_node = comments.comments_after(span.end); let mut start = span.end; // Skip comments after the node to find the next non-whitespace byte whether it's a `)` for comment in comments_after_node { - if !source.all_bytes_match(start, comment.span.start, |b| b.is_ascii_whitespace()) { + if !source.bytes_range(start, comment.span.start).trim_ascii_start().is_empty() { break; } start = comment.span.end; } // Still not a `)`, return early because it's not a type cast if !source.next_non_whitespace_byte_is(start, b')') { - return Ok(false); + return None; } } + // Check for type cast comment in printed or unprinted comments if !comments.is_handled_type_cast_comment() && let Some(last_printed_comment) = comments.printed_comments().last() && last_printed_comment.span.end <= span.start - && f.source_text().next_non_whitespace_byte_is(last_printed_comment.span.end, b'(') + && source.next_non_whitespace_byte_is(last_printed_comment.span.end, b'(') && f.comments().is_type_cast_comment(last_printed_comment) { // 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>( // ^^^^ // Should wrap for `baz` rather than `baz.zoo` if has_closed_parentheses(node_source_text) { - return Ok(false); + None + } else { + // Type cast node, but comment was already printed + Some(&[]) } - - f.context_mut().comments_mut().mark_as_type_cast_node(node); } else if let Some(type_cast_comment_index) = comments.get_type_cast_comment_index(span) { let comments = f.context().comments().unprinted_comments(); let type_cast_comment = &comments[type_cast_comment_index]; @@ -83,19 +77,49 @@ pub fn format_type_cast_comment_node<'a, T>( // ^^^^ // Should wrap for `baz` rather than `baz.zoo` if has_closed_parentheses(node_source_text) { - return Ok(false); + None + } else { + // Type cast node with comments to print + Some(&comments[..=type_cast_comment_index]) } - - let type_cast_comments = &comments[..=type_cast_comment_index]; - - write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; - f.context_mut().comments_mut().mark_as_type_cast_node(node); - // If the printed cast comment is already handled, return early to avoid infinite recursion. } else { // No typecast comment + None + } +} + +/// Formats a node with TypeScript type cast comments if present. +/// +/// This function handles the formatting of JSDoc type cast comments that appear +/// immediately before parenthesized expressions, creating patterns like: +/// `(/** @type {string} */ value)` or `(/** @type {number} */ (expression))` +/// +/// The function: +/// 1. Checks if there's a closing parenthesis after the node (indicating a type cast) +/// 2. Looks for associated type cast comments that precede the node +/// 3. Wraps the node in parentheses with proper formatting and indentation +/// 4. Handles both object/array expressions and other expression types differently +/// +/// Returns `Ok(true)` if the node was formatted as a type cast, `Ok(false)` otherwise. +/// This allows callers to know whether they need to apply their own formatting. +pub fn format_type_cast_comment_node<'a, T>( + node: &(impl Format<'a, T> + GetSpan), + is_object_or_array_expression: bool, + f: &mut Formatter<'_, 'a>, +) -> FormatResult { + // Check if this is a type cast node and get the comments to print + let Some(type_cast_comments) = is_type_cast_node(node, f) else { return Ok(false); + }; + + // Print the type cast comments if any + if !type_cast_comments.is_empty() { + write!(f, [FormatLeadingComments::Comments(type_cast_comments)])?; } + let span = node.span(); + f.context_mut().comments_mut().mark_as_type_cast_node(node); + // https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/print/estree.js#L117-L120 if is_object_or_array_expression && !f.comments().has_comment_before(span.start) { write!(f, group(&format_args!("(", &format_once(|f| node.fmt(f)), ")")))?;