Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(js_formatter): handle nestling comments to support jsdoc overloads #1195

Merged
merged 2 commits into from
Dec 16, 2023

Conversation

faultyserver
Copy link
Contributor

Summary

There were a few diffs in the Prettier report that dealt with JSDoc nestling comments. These are a wholly-undocumented featured of JSDoc that's been around for more than a decade now (jsdoc/jsdoc.github.io#40) that causes JSDoc to consider adjacent doc-style comments as overloads for the same subject, rather than as distinct things.

A nestled comment is one that appears immediately after a previous doc comment, with absolutely no space in between, like:

/**
 * @param {string} foo
 *//** <- notice there is no space or line break here.
 * @param {number} foo
 */

Formatting for these comments must preserve the adjacency for JSDoc to understand it as an overload.

Unfortunately, this is a little awkward to implement. So awkward that Prettier actually just merges the comments into a single comment directly in the parsed syntax tree. But I don't think that's the greatest approach, since it loses the context that they are actually distinct comments. Parsing JSDoc from there in the future then becomes awkward, too, since you have to split on the *//** content rather than just using consecutive leading comments to determine the overloading. But that's all a discussion for another time.

In this PR, I implemented support for preserving these nestled comments. It's currently in biome_formatter, but I think it should really live in biome_js_formatter. The only reason it's not is because all of the formatting for Leading, Trailing, and Dangling comment sets is done in the generic formatter, while individual comments are done in the language-specific one. I think that should probably change in the future as we eventually support languages that use comment syntax other than // and /**/, as it's otherwise hardcoded in there right now.

This implementation in generally brings our comment formatting a little closer to prettier overall.

Test Plan

Three Prettier diff snapshots are gone! There's also one spec test that changed, but I checked that against the output from Prettier and the change matches their output, so I think it's a positive change (other things in that file are still formatted quite differently).

Copy link

netlify bot commented Dec 15, 2023

Deploy Preview for biomejs canceled.

Name Link
🔨 Latest commit 88cab29
🔍 Latest deploy log https://app.netlify.com/sites/biomejs/deploys/657caea7c6fc250008f1aa20

@github-actions github-actions bot added A-Formatter Area: formatter L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages labels Dec 15, 2023
/// /*
/// *
/// this line doesn't start with a star
/// */
/// "#)));
/// ```
pub fn is_doc_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
pub fn is_alignable_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this to is_alignable_comment, since it's technically not the same as a doc comment when there's only one star, but we can still format the alignment even in that case. This also more closely matches Prettier's isIndentableBlockComment naming.

All of the existing usages have also been renamed, of which there were only like 2 anyway.

/// */
/// "#)));
/// ```
pub fn is_doc_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now truly checking for "doc" comments, as defined by JSDoc's syntax. Again, I think this should probably be in the biome_js_formatter crate rather than here, but all of the logic that uses it is in this generic one right now, too. Once we start implementing HTML formatting this will likely have to get moved, but that's a future time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth adding a TODO saying what you say here.

Comment on lines +36 to +40
first.has_newline()
&& second.has_newline()
&& (second.text_range().start()).sub(first.text_range().end()) == TextSize::from(0)
&& is_doc_comment(first)
&& is_doc_comment(second)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried ordering this to make it do the least work possible. I imagine the text range stuff gets inlined into just integer comparisons and a subtraction by the compiler, but i'm not totally sure. Probably doesn't really matter since this function is unlikely to be called in most cases anyway.

@@ -37,14 +66,23 @@ where
FormatLeadingComments::Comments(comments) => comments,
};

for comment in leading_comments {
for (index, comment) in leading_comments.iter().enumerate() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea what the best way to do this is. It feels awkward to have to directly get the iterated and then enumerate it, but it's probably the most efficient way to get both the current and the next item? This is my lack of Rust experience coming through.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need both the current and next item, you can use peekable.

let mut leading_comments_iter = leading_comments.iter().peekable();
for comment in leading_comments_iter {
  let next_comment = leading_comments_iter.peek();
}

Comment on lines -120 to +163
0 | 1 => write!(f, [hard_line_break()])?,
_ if should_nestle => {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the biggest change from what existed before. We used to say that all trailing comments got put on their own lines. This now says that comments starting on the same line as the previous comment will always stay on that same line, and will just have a space if they aren't nestled.

afaict, that matches Prettier's behavior in all cases, and all of the tests still pass, so I assume that's true.

let format_dangling_comments = format_with(|f| {
// Write all comments up to the first skipped token trivia or the token
let mut join = f.join_with(hard_line_break());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question: are JoinBuilders particularly efficient for building here? Or just convenient? This can't use a joiner anymore since the joining element can be different based on the context, but idk if that has a performance hit or not.

The convenience is nice to not have to implement the awkward conditional below on Line 326 lol, but oh well.

@@ -187,8 +187,7 @@ function name(

/* leading of opening */ /* trailing of opening */ 4 + 3;

/* leading of closing */
/* trailing of closing */
/* leading of closing */ /* trailing of closing */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only change that came from that adjustment to the trailing comment logic. It matches Prettier's output now.

@faultyserver
Copy link
Contributor Author

!bench_formatter

Copy link
Contributor

Formatter Benchmark Results

group                                    main                                   pr
-----                                    ----                                   --
formatter/big5-added.json                1.00    368.9±3.27µs    45.8 MB/sec    1.03    379.8±1.25µs    44.5 MB/sec
formatter/canada.json                    1.01    171.9±3.92ms    12.5 MB/sec    1.00    170.6±3.83ms    12.6 MB/sec
formatter/checker.ts                     1.03    287.0±6.47ms     9.1 MB/sec    1.00    278.6±3.21ms     9.3 MB/sec
formatter/compiler.js                    1.00    157.5±2.47ms     6.7 MB/sec    1.00    157.0±2.20ms     6.7 MB/sec
formatter/d3.min.js                      1.00    121.4±2.78ms     2.2 MB/sec    1.00    121.6±2.27ms     2.2 MB/sec
formatter/db.json                        1.00     10.8±0.08ms    17.0 MB/sec    1.01     10.8±0.07ms    16.9 MB/sec
formatter/dojo.js                        1.00      8.6±0.03ms     8.0 MB/sec    1.00      8.6±0.03ms     8.0 MB/sec
formatter/eucjp.json                     1.00    635.9±2.16µs    61.6 MB/sec    1.04   660.5±47.47µs    59.3 MB/sec
formatter/ios.d.ts                       1.00    174.0±2.52ms    10.7 MB/sec    1.03    178.4±2.94ms    10.5 MB/sec
formatter/jquery.min.js                  1.02     35.5±0.70ms     2.3 MB/sec    1.00     35.0±0.30ms     2.4 MB/sec
formatter/math.js                        1.00    246.2±4.11ms     2.6 MB/sec    1.00    247.2±3.02ms     2.6 MB/sec
formatter/package-lock.json              1.00      4.5±0.02ms    30.5 MB/sec    1.04      4.7±0.02ms    29.5 MB/sec
formatter/parser.ts                      1.00      5.9±0.02ms     8.3 MB/sec    1.00      5.8±0.02ms     8.4 MB/sec
formatter/pixi.min.js                    1.00    132.0±2.48ms     3.3 MB/sec    1.00    132.2±2.98ms     3.3 MB/sec
formatter/react-dom.production.min.js    1.00     39.4±0.50ms     2.9 MB/sec    1.01     39.7±0.43ms     2.9 MB/sec
formatter/react.production.min.js        1.01  1998.6±21.53µs     3.1 MB/sec    1.00  1987.7±33.95µs     3.1 MB/sec
formatter/router.ts                      1.00      2.1±0.01ms    11.4 MB/sec    1.00      2.1±0.01ms    11.4 MB/sec
formatter/tex-chtml-full.js              1.00    312.2±4.91ms     2.9 MB/sec    1.01    314.3±4.63ms     2.9 MB/sec
formatter/three.min.js                   1.00    155.5±3.59ms     3.8 MB/sec    1.00    155.5±3.42ms     3.8 MB/sec
formatter/typescript.js                  1.01  1078.0±17.68ms     8.8 MB/sec    1.00  1068.0±12.24ms     8.9 MB/sec
formatter/vue.global.prod.js             1.01     54.0±1.19ms     2.2 MB/sec    1.00     53.6±1.53ms     2.2 MB/sec

Copy link
Member

@Conaclos Conaclos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, I answered some of your questions :)

/// */
/// "#)));
/// ```
pub fn is_doc_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth adding a TODO saying what you say here.

@@ -37,14 +66,23 @@ where
FormatLeadingComments::Comments(comments) => comments,
};

for comment in leading_comments {
for (index, comment) in leading_comments.iter().enumerate() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need both the current and next item, you can use peekable.

let mut leading_comments_iter = leading_comments.iter().peekable();
for comment in leading_comments_iter {
  let next_comment = leading_comments_iter.peek();
}

@faultyserver faultyserver merged commit 86235e5 into main Dec 16, 2023
18 checks passed
@faultyserver faultyserver deleted the faulty/jsdoc-comments branch December 16, 2023 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Formatter Area: formatter L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants