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
14 changes: 8 additions & 6 deletions crates/oxc_formatter/src/formatter/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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'{'))
}
10 changes: 3 additions & 7 deletions crates/oxc_formatter/src/utils/member_chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand Down Expand Up @@ -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();
}

Expand Down
84 changes: 54 additions & 30 deletions crates/oxc_formatter/src/utils/typecast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
/// 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
Expand All @@ -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];
Expand All @@ -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<bool> {
// 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)), ")")))?;
Expand Down
Loading