fix(formatter): treat /*** comments as JSDoc (#27035)

As is well known, what counts as JSDoc depends on the consumer, no spec.
e.g. Original JSDoc is strictly limited to `/**`, but TypeScript is not.

Since this doesn't break JSDoc 3, it's more convenient for a formatter to take broader.
This commit is contained in:
leaysgur committed 2026-09-25 07:10:09 +00:00
1 parent d77531504d
commit 3be5d94d2f
8 files changed
+85 -17

No files matched your search

+31
View File
@@ -661,3 +661,34 @@ var target = x ? y : /** @type {Document} */ ((root).head ?? fallback);
A cast comment types the parenthesized expression directly after it.
When the comment binds to an inner expression and the formatter adds parentheses around the whole (a `??` in a conditional branch, a sequence, a return argument), it prints inside the added pair so the cast keeps its target.
Printed outside, the cast covers the whole expression and tsc types `root` as its uncast type again (`Property 'head' does not exist on type 'Node'`).
## triple-star-jsdoc-hard-break
- Why: uniform-rule (a block comment starting with `/**` is JSDoc)
- Pin: `tests/fixtures/js/comments/jsdoc-trailing-double-space.js`
```js
// input
/***
* a··
* b
*/
// ours
/***
* a··
* b
*/
// prettier
/***
* a
* b
*/
```
`·` marks a trailing space.
Prettier keeps the Markdown hard break in `/**` only, following jsdoc3.
Tools disagree on `/***`: TypeScript (`isJSDocLikeText`, so editor hovers) treats it as JSDoc, jsdoc3 ignores it.
Formatting it as JSDoc does not break jsdoc3, which ignores it either way.
Type cast comments already follow the rule, in Prettier too.
@@ -113,10 +113,12 @@
//! ## References
//! - [Prettier handles special comments](https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/comments/handle-comments.js)
//! - [Prettier pre-processes comments](https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/main/comments/attach.js)
use oxc_ast::{Comment, CommentContent};
use oxc_ast::Comment;
use oxc_formatter_core::SourceText;
use oxc_span::{GetSpan, Span};
use super::trivia::is_jsdoc_comment;
/// Saved comment cursor state for [`Comments::snapshot`] / [`Comments::restore`].
#[derive(Clone, Copy)]
pub struct CommentSnapshot {
@@ -713,7 +715,7 @@ impl Comments<'_> {
.is_some_and(|&byte| byte.is_ascii_whitespace() || byte == b'{')
}
if !matches!(comment.content, CommentContent::Jsdoc) {
if !is_jsdoc_comment(comment) {
return false;
}
+9 -2
View File
@@ -564,8 +564,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatCommentText<'_> {
let first_line = lines.next().unwrap();
write!(f, [text(first_line.trim_end())]);
// Not `comment.is_jsdoc()`, which also accepts `/***`
let is_jsdoc = content.starts_with("/**") && !content.starts_with("/***");
let is_jsdoc = is_jsdoc_comment(comment);
// Indent the remaining lines by one space so that all `*` are aligned.
for line in lines {
@@ -659,3 +658,11 @@ fn is_alignable_comment(lines: &str) -> bool {
pub fn is_alignable_block_comment(comment: &Comment, source_text: SourceText) -> bool {
comment.is_multiline_block() && is_alignable_comment(source_text.text_for(&comment.span))
}
/// A block comment starting with `/**`, including `/***`, at any position.
///
/// Not `Comment::is_jsdoc()`, which is leading only.
/// For `/***`, see `DIVERGENCES.md#triple-star-jsdoc-hard-break`.
pub fn is_jsdoc_comment(comment: &Comment) -> bool {
matches!(comment.content, CommentContent::Jsdoc | CommentContent::JsdocLegal)
}
+6 -5
View File
@@ -19,7 +19,10 @@ use crate::{
formatter::{
Comments, JsFormatter,
prelude::*,
trivia::{FormatLeadingComments, FormatTrailingComments, is_alignable_block_comment},
trivia::{
FormatLeadingComments, FormatTrailingComments, is_alignable_block_comment,
is_jsdoc_comment,
},
},
parentheses::NeedsParentheses,
print::FormatWrite,
@@ -302,7 +305,7 @@ impl LeadingCommentsInfo {
info.has_line_ending_trailing_non_jsdoc_block_comment |= comment.is_block()
&& comment.is_trailing()
&& comment.followed_by_newline()
&& !matches!(comment.content, CommentContent::Jsdoc | CommentContent::JsdocLegal);
&& !is_jsdoc_comment(comment);
info.has_line_ending_trailing_jsdoc_comment |=
is_line_ending_trailing_jsdoc_comment(comment);
}
@@ -313,9 +316,7 @@ impl LeadingCommentsInfo {
/// A trailing (same-line start) jsdoc comment that ends its source line;
/// NOT own-line (that vocabulary means preceded by a newline, see `formatter/trivia.rs`)
pub fn is_line_ending_trailing_jsdoc_comment(comment: &Comment) -> bool {
matches!(comment.content, CommentContent::Jsdoc | CommentContent::JsdocLegal)
&& comment.is_trailing()
&& comment.followed_by_newline()
is_jsdoc_comment(comment) && comment.is_trailing() && comment.followed_by_newline()
}
/// Whether `ty` is a union that runs this printer.
@@ -35,10 +35,11 @@ function nested() {
return 1;
}
// DIVERGENCES.md#triple-star-jsdoc-hard-break
/***
* Not JSDoc, spaces go.
* Also JSDoc, spaces stay.
*/
function notJsdoc() {}
function tripleStar() {}
/*
* Plain block comment, spaces go.
@@ -39,10 +39,11 @@ function nested() {
return 1;
}
// DIVERGENCES.md#triple-star-jsdoc-hard-break
/***
* Not JSDoc, spaces go.
* Also JSDoc, spaces stay.
*/
function notJsdoc() {}
function tripleStar() {}
/*
* Plain block comment, spaces go.
@@ -90,10 +91,11 @@ function nested() {
return 1;
}
// DIVERGENCES.md#triple-star-jsdoc-hard-break
/***
* Not JSDoc, spaces go.
* Also JSDoc, spaces stay.
*/
function notJsdoc() {}
function tripleStar() {}
/*
* Plain block comment, spaces go.
@@ -140,10 +142,11 @@ function nested() {
return 1;
}
// DIVERGENCES.md#triple-star-jsdoc-hard-break
/***
* Not JSDoc, spaces go.
* Also JSDoc, spaces stay.
*/
function notJsdoc() {}
function tripleStar() {}
/*
* Plain block comment, spaces go.
@@ -0,0 +1,2 @@
// A legal JSDoc is a type cast too
const x = /** @license @type {A} */ (y);
@@ -0,0 +1,21 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// A legal JSDoc is a type cast too
const x = /** @license @type {A} */ (y);
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// A legal JSDoc is a type cast too
const x = /** @license @type {A} */ (y);
-------------------
{ printWidth: 100 }
-------------------
// A legal JSDoc is a type cast too
const x = /** @license @type {A} */ (y);
===================== End =====================