mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): keep JSDoc cast parens with a comment inside them (#26374)
Fixes #26274, #26275.
This commit is contained in:
1 parent
b3613b176a
commit
104061b98a
10 files changed
+454
-52
No files matched your search
@@ -135,7 +135,7 @@ pub struct Comments<'a> {
|
||||
///
|
||||
/// When set, [`Self::unprinted_comments()`] will only return comments up to this index,
|
||||
/// effectively hiding comments beyond this point from the formatter.
|
||||
pub view_limit: Option<usize>,
|
||||
view_limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Comments<'a> {
|
||||
@@ -275,6 +275,42 @@ impl<'a> Comments<'a> {
|
||||
unreachable!("the caller guarantees the character occurs outside a comment")
|
||||
}
|
||||
|
||||
/// The run of comments after `pos` separated only by whitespace (a prefix of `comments`).
|
||||
fn comment_run_after(&self, comments: &'a [Comment], pos: u32) -> &'a [Comment] {
|
||||
let mut cursor = pos;
|
||||
let mut count = 0;
|
||||
for comment in comments {
|
||||
if comment.span.start < cursor
|
||||
|| !self
|
||||
.source_text
|
||||
.all_bytes_match(cursor, comment.span.start, |b| b.is_ascii_whitespace())
|
||||
{
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
cursor = comment.span.end;
|
||||
}
|
||||
&comments[..count]
|
||||
}
|
||||
|
||||
/// Whether the first non-whitespace byte after `pos` outside comments is `)`.
|
||||
///
|
||||
/// Skips every comment by span, printed and hidden ones included, so the answer does not depend on the comment view:
|
||||
/// a cast target hidden behind an ancestor's trailing-comment limit must still be recognized, or its parens drop.
|
||||
pub(crate) fn is_followed_by_closing_paren(&self, pos: u32) -> bool {
|
||||
// Only a comment can hide the `)`, so anything but `/` answers without touching the comments;
|
||||
// a `/` that is an operator instead is settled by the comment spans below (no adjacent comment: `false`)
|
||||
match self.source_text.next_non_whitespace_byte(pos) {
|
||||
Some(b')') => return true,
|
||||
Some(b'/') => {}
|
||||
_ => return false,
|
||||
}
|
||||
let first = self.inner.partition_point(|comment| comment.span.end <= pos);
|
||||
let run = self.comment_run_after(&self.inner[first..], pos);
|
||||
let end = run.last().map_or(pos, |comment| comment.span.end);
|
||||
self.source_text.next_non_whitespace_byte_is(end, b')')
|
||||
}
|
||||
|
||||
/// Comments sitting between `pos` and a closing source paren:
|
||||
/// the run of comments after `pos` separated only by whitespace,
|
||||
/// whose next non-whitespace character is `)`.
|
||||
@@ -284,24 +320,9 @@ impl<'a> Comments<'a> {
|
||||
/// (e.g. an expression end up to the statement end)
|
||||
/// A range containing a string literal would false-match a `)` inside it.
|
||||
pub(crate) fn comments_before_closing_paren(&self, pos: u32) -> Option<&'a [Comment]> {
|
||||
let comments = self.comments_after(pos);
|
||||
|
||||
let mut pos = pos;
|
||||
let mut count = 0;
|
||||
for comment in comments {
|
||||
if comment.span.start < pos
|
||||
|| !self
|
||||
.source_text
|
||||
.all_bytes_match(pos, comment.span.start, |b| b.is_ascii_whitespace())
|
||||
{
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
pos = comment.span.end;
|
||||
}
|
||||
|
||||
(count > 0 && self.source_text.next_non_whitespace_byte_is(pos, b')'))
|
||||
.then(|| &comments[..count])
|
||||
let run = self.comment_run_after(self.comments_after(pos), pos);
|
||||
let end = run.last()?.span.end;
|
||||
self.source_text.next_non_whitespace_byte_is(end, b')').then_some(run)
|
||||
}
|
||||
|
||||
/// Whether the range holds a `;` or a `)` outside comments (`foo /* ; */` doesn't count).
|
||||
|
||||
@@ -637,7 +637,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatClassElementWithSemicolon<'a,
|
||||
// the comment still ends up behind the semicolon like Prettier.
|
||||
let content_end = value
|
||||
.as_ref()
|
||||
.map(assignment_chain_leaf_end)
|
||||
.map(|value| assignment_chain_leaf_end(value, f))
|
||||
.or_else(|| type_annotation.as_ref().map(|ta| ta.span.end))
|
||||
.unwrap_or_else(|| key.span().end);
|
||||
// `node_end` bound: the element may have no source `;` at all
|
||||
|
||||
@@ -39,6 +39,7 @@ pub use arrow_function_expression::{
|
||||
};
|
||||
pub use binary_like_expression::{BinaryLikeExpression, should_flatten};
|
||||
pub use fragment::{FormatFunctionParams, FormatTypeParameters};
|
||||
pub use semicolon::write_comments_before_closing_paren;
|
||||
pub use union_type::{
|
||||
alias_union_breaks_after_operator, is_line_ending_trailing_jsdoc_comment, type_alias_left_end,
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for ReturnAndThrowStatement<'a, '_> {
|
||||
|
||||
if let Some(argument) = self.argument() {
|
||||
write!(f, space());
|
||||
let argument_leaf_end = assignment_chain_leaf_end(argument.as_ref());
|
||||
let argument_leaf_end = assignment_chain_leaf_end(argument.as_ref(), f);
|
||||
if f.comments().has_comment_in_range(argument_leaf_end, self.span().end) {
|
||||
// Comments inside the argument's source parentheses belong to the argument
|
||||
// and stay in place (`return (\n a && b // c\n);` keeps the comment inside, like Prettier);
|
||||
|
||||
@@ -6,7 +6,10 @@ use crate::{
|
||||
ast_nodes::AstNodes,
|
||||
formatter::{JsFormatContext, JsFormatter, trivia::FormatTrailingComments},
|
||||
options::Semicolons,
|
||||
utils::format_node_without_trailing_comments::format_content_without_comments_after,
|
||||
utils::{
|
||||
format_node_without_trailing_comments::format_content_without_comments_after,
|
||||
typecast::cast_target_end,
|
||||
},
|
||||
write,
|
||||
};
|
||||
|
||||
@@ -101,7 +104,11 @@ pub fn keeps_trailing_comment_inside_parens(expr: &Expression<'_>, gated: bool)
|
||||
///
|
||||
/// The chain also passes through arrow expression bodies (prettier#19930 family);
|
||||
/// bodies matching [`arrow_body_keeps_trailing_comment_inside_parens`] stop the walk.
|
||||
pub fn assignment_chain_leaf_end(expr: &Expression<'_>) -> u32 {
|
||||
///
|
||||
/// A JSDoc cast target leaf ends past its cast parens (`cast_target_end`), like the keeps above;
|
||||
/// not via `end_including_source_parens`: the walk has no `node_end` bound,
|
||||
/// and the cast's matching `)` is counted, not the last one in a window.
|
||||
pub fn assignment_chain_leaf_end(expr: &Expression<'_>, f: &JsFormatter<'_, '_>) -> u32 {
|
||||
let mut leaf = expr;
|
||||
loop {
|
||||
match leaf {
|
||||
@@ -115,7 +122,7 @@ pub fn assignment_chain_leaf_end(expr: &Expression<'_>) -> u32 {
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
leaf.span().end
|
||||
cast_target_end(leaf.span(), f).unwrap_or(leaf.span().end)
|
||||
}
|
||||
|
||||
/// Content end for a semicolon-terminated expression site, pairing the two functions above:
|
||||
@@ -133,7 +140,7 @@ pub fn semicolon_terminated_expression_content_end(
|
||||
if keeps_trailing_comment_inside_parens(expr, gated) {
|
||||
f.context().comments().end_including_source_parens(paren_scan_start, node_end)
|
||||
} else {
|
||||
assignment_chain_leaf_end(expr)
|
||||
assignment_chain_leaf_end(expr, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +185,15 @@ pub fn write_trailing_comments_inside_parens<'a>(
|
||||
AstNodes::AssignmentExpression(_) => is_sequence,
|
||||
_ => false,
|
||||
};
|
||||
if parens_survive
|
||||
&& let Some(comments) = f.context().comments().comments_before_closing_paren(node_end)
|
||||
{
|
||||
if parens_survive {
|
||||
write_comments_before_closing_paren(f, node_end);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints the comments sitting right before the closing source paren after `end`,
|
||||
/// inside the parentheses, for a node that re-prints them.
|
||||
pub fn write_comments_before_closing_paren(f: &mut JsFormatter<'_, '_>, end: u32) {
|
||||
if let Some(comments) = f.context().comments().comments_before_closing_paren(end) {
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
formatter::{
|
||||
JsFormatContext, JsFormatter, JsFormatterExt as _, trivia::format_leading_comments,
|
||||
},
|
||||
utils::suppressed::FormatSuppressedNode,
|
||||
utils::{suppressed::FormatSuppressedNode, typecast::cast_target_end},
|
||||
};
|
||||
|
||||
/// Generic wrapper for formatting a node without its trailing comments.
|
||||
@@ -20,7 +20,11 @@ where
|
||||
T: Format<'a, JsFormatContext<'a>> + GetSpan,
|
||||
{
|
||||
fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
|
||||
let node_end = self.0.span().end;
|
||||
// A JSDoc cast target's hidden range starts past its cast parens (see `cast_target_end`);
|
||||
// a suppression comment there keeps the plain span for the suppressed path below.
|
||||
let node_end = cast_target_end(self.0.span(), f)
|
||||
.filter(|end| !f.comments().has_trailing_suppression_comment(*end))
|
||||
.unwrap_or(self.0.span().end);
|
||||
|
||||
if f.comments().has_trailing_suppression_comment(node_end) {
|
||||
format_leading_comments(self.0.span()).fmt(f);
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
prelude::*,
|
||||
trivia::{FormatLeadingComments, format_leading_comments},
|
||||
},
|
||||
print::write_comments_before_closing_paren,
|
||||
utils::suppressed::FormatSuppressedNode,
|
||||
write,
|
||||
};
|
||||
@@ -25,10 +26,11 @@ pub enum TypeCast<'a> {
|
||||
/// /** @type {Document} */ (root.head ?? fallback)
|
||||
/// ^^^^^^^^^^^^^^^^^^^^^ Target
|
||||
/// ```
|
||||
/// The slice holds the comments still to be printed;
|
||||
/// `pending` holds the comments still to be printed;
|
||||
/// it is empty when the cast comment was already printed
|
||||
/// (re-entry from [`format_type_cast_comment_node`], or printed by an ancestor).
|
||||
Target(&'a [Comment]),
|
||||
/// `cast_comment` is the cast comment either way.
|
||||
Target { pending: &'a [Comment], cast_comment: &'a Comment },
|
||||
/// The node's unprinted leading comments end with a cast comment that binds
|
||||
/// to an inner expression (its parenthesis closes before the node ends):
|
||||
/// ```js
|
||||
@@ -51,7 +53,7 @@ pub enum TypeCast<'a> {
|
||||
|
||||
impl TypeCast<'_> {
|
||||
pub fn is_target(&self) -> bool {
|
||||
matches!(self, TypeCast::Target(_))
|
||||
matches!(self, TypeCast::Target { .. })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +72,8 @@ pub fn classify_type_cast<'a>(span: Span, f: &JsFormatter<'_, 'a>) -> TypeCast<'
|
||||
&& comments.is_type_cast_comment_followed_by_paren(last_printed_comment)
|
||||
{
|
||||
match classify_cast_comment_gap(last_printed_comment.span.end, span.start, f) {
|
||||
CastCommentGap::ParensAndTrivia if is_followed_by_closing_paren(span, f) => {
|
||||
return TypeCast::Target(&[]);
|
||||
CastCommentGap::ParensAndTrivia if comments.is_followed_by_closing_paren(span.end) => {
|
||||
return TypeCast::Target { pending: &[], cast_comment: last_printed_comment };
|
||||
}
|
||||
// The printed comment is unrelated to this node;
|
||||
// fall through to look for the node's own unprinted cast comment.
|
||||
@@ -90,8 +92,11 @@ pub fn classify_type_cast<'a>(span: Span, f: &JsFormatter<'_, 'a>) -> TypeCast<'
|
||||
CastCommentGap::Trivia => {
|
||||
TypeCast::BindsInner(&unprinted_comments[..=type_cast_comment_index])
|
||||
}
|
||||
CastCommentGap::ParensAndTrivia if is_followed_by_closing_paren(span, f) => {
|
||||
TypeCast::Target(&unprinted_comments[..=type_cast_comment_index])
|
||||
CastCommentGap::ParensAndTrivia if comments.is_followed_by_closing_paren(span.end) => {
|
||||
TypeCast::Target {
|
||||
pending: &unprinted_comments[..=type_cast_comment_index],
|
||||
cast_comment: type_cast_comment,
|
||||
}
|
||||
}
|
||||
_ => TypeCast::None,
|
||||
};
|
||||
@@ -100,11 +105,31 @@ pub fn classify_type_cast<'a>(span: Span, f: &JsFormatter<'_, 'a>) -> TypeCast<'
|
||||
TypeCast::None
|
||||
}
|
||||
|
||||
/// Whether the next non-whitespace byte after the node (skipping comments adjacent to it) is `)`.
|
||||
/// i.e. source parentheses close right after the node, as a cast target requires.
|
||||
fn is_followed_by_closing_paren(span: Span, f: &JsFormatter<'_, '_>) -> bool {
|
||||
f.source_text().next_non_whitespace_byte_is(span.end, b')')
|
||||
|| f.context().comments().comments_before_closing_paren(span.end).is_some()
|
||||
/// A cast target's pending comments and the span of its cast parentheses
|
||||
/// (from the first `(` after the cast comment to after the matching `)`), `None` for any other node.
|
||||
fn cast_target_parens<'a>(span: Span, f: &JsFormatter<'_, 'a>) -> Option<(&'a [Comment], Span)> {
|
||||
// Every cast target closes with `)`: the cheapest rejection, before any comment lookup
|
||||
if !f.context().comments().is_followed_by_closing_paren(span.end) {
|
||||
return None;
|
||||
}
|
||||
let TypeCast::Target { pending, cast_comment } = classify_type_cast(span, f) else {
|
||||
return None;
|
||||
};
|
||||
cast_parens_span(cast_comment.span.end, span, f).map(|parens| (pending, parens))
|
||||
}
|
||||
|
||||
/// The position after the cast parentheses of a cast target, `None` for any other node.
|
||||
///
|
||||
/// A site that hides a child's trailing comments must hide from here, not from the child's span end:
|
||||
/// the target prints its parens and the comments inside them,
|
||||
/// and hiding those blinds its trailing-comments pass, so the comment escapes the parens.
|
||||
/// ```js
|
||||
/// const x = /** @type {T} */ (
|
||||
/// value // must stay inside
|
||||
/// );
|
||||
/// ```
|
||||
pub fn cast_target_end(span: Span, f: &JsFormatter<'_, '_>) -> Option<u32> {
|
||||
cast_target_parens(span, f).map(|(_, parens)| parens.end)
|
||||
}
|
||||
|
||||
/// Formats a node that is the target of a JSDoc type cast (see [`TypeCast::Target`]):
|
||||
@@ -120,7 +145,8 @@ pub fn format_type_cast_comment_node<'a>(
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
) -> bool {
|
||||
// Check if this node is a cast target and get the comments to print
|
||||
let TypeCast::Target(type_cast_comments) = classify_type_cast(node.span(), f) else {
|
||||
let TypeCast::Target { pending: type_cast_comments, .. } = classify_type_cast(node.span(), f)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -132,11 +158,23 @@ pub fn format_type_cast_comment_node<'a>(
|
||||
let span = node.span();
|
||||
f.context_mut().comments_mut().mark_as_type_cast_node(node);
|
||||
|
||||
let format_node = format_with(|f| {
|
||||
node.fmt(f);
|
||||
// Own-line comments left before the cast `)` have no following node inside the parens to lead;
|
||||
// deferred, they would lead the next sibling (a call's arguments) outside the parens.
|
||||
// Same-line ones are already printed by the node's trailing pass.
|
||||
write_comments_before_closing_paren(f, span.end);
|
||||
});
|
||||
|
||||
// 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_with(|f| node.fmt(f)), ")")));
|
||||
// A comment before the cast `)` (the only thing that can precede it) breaks the hug too
|
||||
let hugs = is_object_or_array_expression
|
||||
&& !f.comments().has_comment_before(span.start)
|
||||
&& f.source_text().next_non_whitespace_byte_is(span.end, b')');
|
||||
if hugs {
|
||||
write!(f, group(&format_args!("(", &format_node, ")")));
|
||||
} else {
|
||||
write!(f, group(&format_args!("(", soft_block_indent(&format_with(|f| node.fmt(f))), ")")));
|
||||
write!(f, group(&format_args!("(", soft_block_indent(&format_node), ")")));
|
||||
}
|
||||
|
||||
true
|
||||
@@ -154,9 +192,8 @@ pub fn format_type_cast_comment_node<'a>(
|
||||
/// An empty `pending` (the cast comment printed by an ancestor) cannot reach here suppressed,
|
||||
/// and falls through regardless.
|
||||
pub fn write_suppressed_cast_target(span: Span, f: &mut JsFormatter<'_, '_>) -> bool {
|
||||
if let TypeCast::Target(pending) = classify_type_cast(span, f)
|
||||
&& let Some(cast_comment) = pending.last()
|
||||
&& let Some(verbatim_span) = cast_parens_span(cast_comment.span.end, span, f)
|
||||
if let Some((pending, verbatim_span)) = cast_target_parens(span, f)
|
||||
&& !pending.is_empty()
|
||||
{
|
||||
write!(f, [FormatLeadingComments::Comments(pending)]);
|
||||
FormatSuppressedNode(verbatim_span).fmt(f);
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// A comment inside the parens of a JSDoc type cast never drops the parens
|
||||
// (without them tsc ignores the cast) and stays inside them, like Prettier.
|
||||
// The traps are the sites that hide a child's trailing comments while printing it:
|
||||
// statement terminators, callees, heritage (issues #26274, #26275).
|
||||
|
||||
// Statement terminators: same-line and own-line comments stay inside.
|
||||
const declared = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const ownLine = /** @type {string} */ (
|
||||
value
|
||||
// own-line
|
||||
);
|
||||
/** @type {string} */ (
|
||||
statement // trailing
|
||||
);
|
||||
export default /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
chained = other = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const arrow = () => /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
class Property {
|
||||
field = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
function returned() {
|
||||
return assigned = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
// No source `;` (ASI): the cast `)` is not a dropped paren.
|
||||
const asi = /** @type {string} */ (
|
||||
value // trailing
|
||||
)
|
||||
|
||||
// Object and array targets lose the hug form, the comment breaks the parens.
|
||||
const object = /** @type {{ value: string }} */ ({ value } // trailing
|
||||
);
|
||||
const array = /** @type {string[]} */ ([value]
|
||||
// own-line
|
||||
);
|
||||
const hugged = /** @type {{ value: string }} */ ({ value });
|
||||
|
||||
// Callee: the comment stays with the callee, not the arguments.
|
||||
const called = /** @type {(a: number) => number} */ (
|
||||
fn
|
||||
// own-line
|
||||
)(1);
|
||||
const calledTrailing = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(1);
|
||||
// A comment between the cast `)` and the arguments stays outside.
|
||||
const between = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
) /* between */ (1);
|
||||
|
||||
// Other hiding sites (`FormatNodeWithoutTrailingComments`): class heritage.
|
||||
class Heritage extends /** @type {typeof Base} */ (
|
||||
Base // trailing
|
||||
) {}
|
||||
|
||||
// Member object.
|
||||
const member = foo(/** @type {X} */ (
|
||||
fn
|
||||
// own-line
|
||||
).prop);
|
||||
|
||||
// Same-line comments after the cast `)` still move behind the `;`.
|
||||
const after = /** @type {string} */ (value) /* after */;
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
---
|
||||
source: crates/oxc_formatter/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// A comment inside the parens of a JSDoc type cast never drops the parens
|
||||
// (without them tsc ignores the cast) and stays inside them, like Prettier.
|
||||
// The traps are the sites that hide a child's trailing comments while printing it:
|
||||
// statement terminators, callees, heritage (issues #26274, #26275).
|
||||
|
||||
// Statement terminators: same-line and own-line comments stay inside.
|
||||
const declared = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const ownLine = /** @type {string} */ (
|
||||
value
|
||||
// own-line
|
||||
);
|
||||
/** @type {string} */ (
|
||||
statement // trailing
|
||||
);
|
||||
export default /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
chained = other = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const arrow = () => /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
class Property {
|
||||
field = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
function returned() {
|
||||
return assigned = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
// No source `;` (ASI): the cast `)` is not a dropped paren.
|
||||
const asi = /** @type {string} */ (
|
||||
value // trailing
|
||||
)
|
||||
|
||||
// Object and array targets lose the hug form, the comment breaks the parens.
|
||||
const object = /** @type {{ value: string }} */ ({ value } // trailing
|
||||
);
|
||||
const array = /** @type {string[]} */ ([value]
|
||||
// own-line
|
||||
);
|
||||
const hugged = /** @type {{ value: string }} */ ({ value });
|
||||
|
||||
// Callee: the comment stays with the callee, not the arguments.
|
||||
const called = /** @type {(a: number) => number} */ (
|
||||
fn
|
||||
// own-line
|
||||
)(1);
|
||||
const calledTrailing = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(1);
|
||||
// A comment between the cast `)` and the arguments stays outside.
|
||||
const between = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
) /* between */ (1);
|
||||
|
||||
// Other hiding sites (`FormatNodeWithoutTrailingComments`): class heritage.
|
||||
class Heritage extends /** @type {typeof Base} */ (
|
||||
Base // trailing
|
||||
) {}
|
||||
|
||||
// Member object.
|
||||
const member = foo(/** @type {X} */ (
|
||||
fn
|
||||
// own-line
|
||||
).prop);
|
||||
|
||||
// Same-line comments after the cast `)` still move behind the `;`.
|
||||
const after = /** @type {string} */ (value) /* after */;
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
// A comment inside the parens of a JSDoc type cast never drops the parens
|
||||
// (without them tsc ignores the cast) and stays inside them, like Prettier.
|
||||
// The traps are the sites that hide a child's trailing comments while printing it:
|
||||
// statement terminators, callees, heritage (issues #26274, #26275).
|
||||
|
||||
// Statement terminators: same-line and own-line comments stay inside.
|
||||
const declared = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const ownLine = /** @type {string} */ (
|
||||
value
|
||||
// own-line
|
||||
);
|
||||
/** @type {string} */ (
|
||||
statement // trailing
|
||||
);
|
||||
export default /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
chained = other = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const arrow = () =>
|
||||
/** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
class Property {
|
||||
field = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
function returned() {
|
||||
return (assigned = /** @type {string} */ (
|
||||
value // trailing
|
||||
));
|
||||
}
|
||||
// No source `;` (ASI): the cast `)` is not a dropped paren.
|
||||
const asi = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
|
||||
// Object and array targets lose the hug form, the comment breaks the parens.
|
||||
const object = /** @type {{ value: string }} */ (
|
||||
{ value } // trailing
|
||||
);
|
||||
const array = /** @type {string[]} */ (
|
||||
[value]
|
||||
// own-line
|
||||
);
|
||||
const hugged = /** @type {{ value: string }} */ ({ value });
|
||||
|
||||
// Callee: the comment stays with the callee, not the arguments.
|
||||
const called = /** @type {(a: number) => number} */ (
|
||||
fn
|
||||
// own-line
|
||||
)(1);
|
||||
const calledTrailing = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(1);
|
||||
// A comment between the cast `)` and the arguments stays outside.
|
||||
const between = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(/* between */ 1);
|
||||
|
||||
// Other hiding sites (`FormatNodeWithoutTrailingComments`): class heritage.
|
||||
class Heritage
|
||||
extends /** @type {typeof Base} */ (
|
||||
Base // trailing
|
||||
) {}
|
||||
|
||||
// Member object.
|
||||
const member = foo(
|
||||
/** @type {X} */ (
|
||||
fn
|
||||
// own-line
|
||||
).prop,
|
||||
);
|
||||
|
||||
// Same-line comments after the cast `)` still move behind the `;`.
|
||||
const after = /** @type {string} */ (value); /* after */
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
// A comment inside the parens of a JSDoc type cast never drops the parens
|
||||
// (without them tsc ignores the cast) and stays inside them, like Prettier.
|
||||
// The traps are the sites that hide a child's trailing comments while printing it:
|
||||
// statement terminators, callees, heritage (issues #26274, #26275).
|
||||
|
||||
// Statement terminators: same-line and own-line comments stay inside.
|
||||
const declared = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const ownLine = /** @type {string} */ (
|
||||
value
|
||||
// own-line
|
||||
);
|
||||
/** @type {string} */ (
|
||||
statement // trailing
|
||||
);
|
||||
export default /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
chained = other = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
const arrow = () =>
|
||||
/** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
class Property {
|
||||
field = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
}
|
||||
function returned() {
|
||||
return (assigned = /** @type {string} */ (
|
||||
value // trailing
|
||||
));
|
||||
}
|
||||
// No source `;` (ASI): the cast `)` is not a dropped paren.
|
||||
const asi = /** @type {string} */ (
|
||||
value // trailing
|
||||
);
|
||||
|
||||
// Object and array targets lose the hug form, the comment breaks the parens.
|
||||
const object = /** @type {{ value: string }} */ (
|
||||
{ value } // trailing
|
||||
);
|
||||
const array = /** @type {string[]} */ (
|
||||
[value]
|
||||
// own-line
|
||||
);
|
||||
const hugged = /** @type {{ value: string }} */ ({ value });
|
||||
|
||||
// Callee: the comment stays with the callee, not the arguments.
|
||||
const called = /** @type {(a: number) => number} */ (
|
||||
fn
|
||||
// own-line
|
||||
)(1);
|
||||
const calledTrailing = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(1);
|
||||
// A comment between the cast `)` and the arguments stays outside.
|
||||
const between = /** @type {(a: number) => number} */ (
|
||||
fn // trailing
|
||||
)(/* between */ 1);
|
||||
|
||||
// Other hiding sites (`FormatNodeWithoutTrailingComments`): class heritage.
|
||||
class Heritage
|
||||
extends /** @type {typeof Base} */ (
|
||||
Base // trailing
|
||||
) {}
|
||||
|
||||
// Member object.
|
||||
const member = foo(
|
||||
/** @type {X} */ (
|
||||
fn
|
||||
// own-line
|
||||
).prop,
|
||||
);
|
||||
|
||||
// Same-line comments after the cast `)` still move behind the `;`.
|
||||
const after = /** @type {string} */ (value); /* after */
|
||||
|
||||
===================== End =====================
|
||||
@@ -66,11 +66,14 @@ impl<'a> SourceText<'a> {
|
||||
}
|
||||
|
||||
// Byte checking
|
||||
/// First non-whitespace byte at or after position
|
||||
pub fn next_non_whitespace_byte(&self, position: u32) -> Option<u8> {
|
||||
self.bytes_from(position).find(|byte| !byte.is_ascii_whitespace())
|
||||
}
|
||||
|
||||
/// Check if first non-whitespace byte at position matches expected
|
||||
pub fn next_non_whitespace_byte_is(&self, position: u32, expected_byte: u8) -> bool {
|
||||
self.bytes_from(position)
|
||||
.find(|byte| !byte.is_ascii_whitespace())
|
||||
.is_some_and(|b| b == expected_byte)
|
||||
self.next_non_whitespace_byte(position) == Some(expected_byte)
|
||||
}
|
||||
|
||||
// Byte range operations
|
||||
|
||||
Reference in new issue
Block a user