Skip to content

Keep a line comment in a block construct's operand in the operand - #9061

Merged
cknitt merged 4 commits into
masterfrom
operand-line-suffix
Oct 27, 2026
Merged

cknitt merged 4 commits into
masterfrom
operand-line-suffix

Conversation

@cknitt

@cknitt cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member

A trailing line comment in the operand of switch, if, while, for, for..of, for await..of or try was printed after the block's { and, on the next run, moved into the block. Now, when such a comment is still pending at the end of the operand (Doc.has_pending_line_suffix), the operand goes into parentheses with the comment before the ): switch (\n obj.x // c\n) {.

Only operands whose comment would have moved get the parentheses, so no existing snapshot changes.

Fixes #9060.

New printer fixture comments/operandLineComment.res covers every reproduction from the issue.

Interaction with #8906: it adds print_operand_before_brace at the same call sites, breaking groups before a line comment and adding parentheses for comments outside any group. Combined, its has_ungrouped_line_suffix would be replaced by this has_pending_line_suffix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo

Why:
    A trailing line comment is printed at the next line break of a breaking
    group. In a flat operand such as `obj.x`, `obj["m"]` or `obj->f`, that
    is the break after the block's `{`, so formatting moved the comment
    after the `{` and on the next run into the block. When the comment is
    still pending at the end of the operand of `switch`, `if`, `while`,
    `for`, `for..of`, `for await..of` or `try`, the operand now goes into
    parentheses with the comment before the `)`.

Format modified (switch operand):
Example:
    switch obj // c
    .x {
    | _ => ()
    }
Before (rescript): first run, then second run
    switch obj.x { // c
    | _ => ()
    }
    ⇝
    switch obj.x {
    // c
    | _ => ()
    }
After (rescript): stable
    switch (
      obj.x // c
    ) {
    | _ => ()
    }

Review: Does code that formatted stably before change? No: an operand only
gets parentheses when a line comment in it is still queued at its end,
which always moved the comment. No existing snapshot changes.

Fixes: #9060
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Keep a line comment in the operand of switch, if, try or a loop in the operand instead of moving it into the block when formatting.

Semantic Diff

4 commits · Full page

Keep a line comment in a block construct's operand in the operand

e5db72d · cknitt + Claude · Oct 27, 2026, 07:03 UTC

Why

A trailing line comment is printed at the next line break of a breaking group. In a flat operand such as obj.x, obj["m"] or obj->f, that is the break after the block's {, so formatting moved the comment after the { and on the next run into the block. When the comment is still pending at the end of the operand of switch, if, while, for, for..of, for await..of or try, the operand now goes into parentheses with the comment before the ).

Format modified (switch operand)

switch obj // c
.x {
| _ => ()
}

rescript · Before: first run, then second run · After: stable

-switch obj.x { // c
-| _ => ()
-}
-⇝
-switch obj.x {
-// c
+switch (
+  obj.x // c
+) {
 | _ => ()
 }

Review

  • Does code that formatted stably before change? No: an operand only gets parentheses when a line comment in it is still queued at its end, which always moved the comment. No existing snapshot changes.
Keep a line comment in an operand that has its own parentheses

8bf8c32 · cknitt + Claude · Oct 27, 2026, 08:14 UTC

Why

A coercion or type constraint operand is printed in its own parentheses. A comment still pending at its end got a second pair around them and was printed after the inner ). That position is not adjacent to the operand, so the next format attached the comment to the first case and moved it into the block (#9060).

Such an operand now breaks its own parentheses when a comment is pending, which keeps the comment before the ). A while condition with its own parentheses no longer gets the extra pair it adds when a condition breaks.

Format modified (parenthesized switch operand)

switch (f(
  a, // c
  b,
) :> int) {
| _ => ()
}

rescript · Before: first run, then second run · After: stable

 switch (
-  (f(a, b) :> int) // c
+  f(a, b) :> int // c
 ) {
 | _ => ()
 }
-⇝
-switch (f(a, b) :> int) {
-// c
-
-| _ => ()
-}
✨ Print the operands of switch, try and loops with one helper

631dbaf · cknitt + Claude · Oct 27, 2026, 10:35 UTC · new

The operands of switch, try, for, for..of and for await..of were each printed by the same block: the expression, its parentheses or braces, and print_operand_before_brace. print_block_operand does that once; if and while keep their own condition printing. The output is unchanged.

✨ Test line comments after the items of a block construct's operand

364e152 · cknitt + Claude · Oct 27, 2026, 10:36 UTC · new

These cases from the dropped #8906 guard the parentheses against a comment after an item of an array, list, tuple, record or call, after a pipe's receiver, and before the operator of a binary expression or pipe inside an item, under switch, if, while, for..of and try. Each formats the same again.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.44%. Comparing base (1a49d91) to head (364e152).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
compiler/syntax/src/res_doc.ml 91.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9061      +/-   ##
==========================================
+ Coverage   81.42%   81.44%   +0.01%     
==========================================
  Files         459      459              
  Lines       64547    64553       +6     
==========================================
+ Hits        52559    52573      +14     
+ Misses      11988    11980       -8     
Files with missing lines Coverage Δ
compiler/syntax/src/res_printer.ml 94.32% <100.00%> (+0.25%) ⬆️
compiler/syntax/src/res_doc.ml 69.60% <91.66%> (+1.37%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9061

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9061

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9061

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9061

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9061

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9061

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9061

commit: 364e152

@github-actions

Copy link
Copy Markdown

cknitt added a commit that referenced this pull request Oct 27, 2026
Why:
    #9061 keeps a line comment in the operand of switch, try, if, while or a
    loop by putting the operand into parentheses when
    Doc.has_pending_line_suffix finds the comment still queued at its end.
    A comment after an item or argument then moves to the end of the
    operand, away from the item it describes.

The operand now first breaks the groups around such a comment
(Doc.break_before_line_suffixes), so the comment stays after its item and
the parentheses are only needed when it is still pending. A binary
expression is left flat: breaking it would flush the comment after the
operator, where the next format attaches it to the right operand, so it
keeps #9061's parentheses.

Format modified (line comment in a switch operand):
Example:
    let x = switch [
      a, // c
      b,
    ] {
    | _ => 1
    }
Before (rescript):
    let x = switch (
      [a, b] // c
    ) {
    | _ => 1
    }
After (rescript):
    let x = switch [
      a, // c
      b,
    ] {
    | _ => 1
    }

Review: Why not make every same-line line comment break its group? That changes code that formats stably today, such as foo(v :> b) // comment, which would split over three lines; the drift only happens before a block.
Review: Why are binary operands exempt? With their group broken, `switch a // c\n+ b {` printed `a + // c\nb` and then `a +\n// c\nb` on the next format; flat, it prints `switch (\n  a + b // c\n) {` and stays.

Area: printer tests
Output: unchanged
Formatting: operands of switch, try, if, while and loops with a line comment
 after an item or argument keep it there; binary operands keep #9061's
 parentheses
Formats: none
PPX: unchanged
Editor: unchanged
Tests: +tests/syntax_tests/data/printer/comments/operandBeforeBlock.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUpL6dKCiSwAeriDGRHqNr
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt added this pull request to stack #9062 October 27, 2026 08:05
Why:
    A coercion or type constraint operand is printed in its own parentheses.
    A comment still pending at its end got a second pair around them and
    was printed after the inner `)`. That position is not adjacent to the
    operand, so the next format attached the comment to the first case and
    moved it into the block (#9060).

Such an operand now breaks its own parentheses when a comment is pending,
which keeps the comment before the `)`. A `while` condition with its own
parentheses no longer gets the extra pair it adds when a condition breaks.

Format modified (parenthesized switch operand):
Example:
    switch (f(
      a, // c
      b,
    ) :> int) {
    | _ => ()
    }
Before (rescript): first run, then second run
    switch (
      (f(a, b) :> int) // c
    ) {
    | _ => ()
    }
    ⇝
    switch (f(a, b) :> int) {
    // c

    | _ => ()
    }
After (rescript): stable
    switch (
      f(a, b) :> int // c
    ) {
    | _ => ()
    }

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 27, 2026
Why:
    #9061 keeps a line comment in the operand of switch, try, if, while or a
    loop by putting the operand into parentheses when
    Doc.has_pending_line_suffix finds the comment still queued at its end.
    A comment after an item or argument then moves to the end of the
    operand, away from the item it describes.

The operand now first breaks the groups around such a comment
(Doc.break_before_line_suffixes), so the comment stays after its item and
the parentheses are only needed when it is still pending. A binary
expression is left flat: breaking it would flush the comment after the
operator, where the next format attaches it to the right operand, so it
keeps #9061's parentheses. In an operand with its own parentheses, such as
a coercion, the comment also stays after its item inside them
(operandLineComment.res).

Format modified (line comment in a switch operand):
Example:
    let x = switch [
      a, // c
      b,
    ] {
    | _ => 1
    }
Before (rescript):
    let x = switch (
      [a, b] // c
    ) {
    | _ => 1
    }
After (rescript):
    let x = switch [
      a, // c
      b,
    ] {
    | _ => 1
    }

Review: Why not make every same-line line comment break its group? That changes code that formats stably today, such as foo(v :> b) // comment, which would split over three lines; the drift only happens before a block.
Review: Why are binary operands exempt? With their group broken, `switch a // c\n+ b {` printed `a + // c\nb` and then `a +\n// c\nb` on the next format; flat, it prints `switch (\n  a + b // c\n) {` and stays.

Area: printer tests
Output: unchanged
Formatting: operands of switch, try, if, while and loops with a line comment
 after an item or argument keep it there; binary operands keep #9061's
 parentheses
Formats: none
PPX: unchanged
Editor: unchanged
Tests: +tests/syntax_tests/data/printer/comments/operandBeforeBlock.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUpL6dKCiSwAeriDGRHqNr
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T11:04:30.371431Z 364e152 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 8bf8c32c22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cknitt added a commit that referenced this pull request Oct 27, 2026
Why:
    Only a binary operand was left flat. A binary expression inside another
    operand, as in `[a // c\n+ b]`, still had its group broken, so the
    comment was printed after the operator and the next format attached it
    to the right operand.

An operand is now also left flat when a binary expression or pipe in it has
its operator at the start of a line, after the left operand, which is where
such a comment sits (Parsetree_viewer.has_line_break_before_operator). The
comment then goes into #9061's parentheses. A comment after an item that
holds a binary expression keeps its place.

Format modified (binary expression in an array operand):
Example:
    let x = switch [a // c
    + b, d] { | _ => 1 }
Before (rescript): first run, then second run
    let x = switch [
      a + // c
      b,
      d,
    ] {
    | _ => 1
    }
    ⇝
    let x = switch [
      a +
      // c
      b,
      d,
    ] {
    | _ => 1
    }
After (rescript): stable
    let x = switch (
      [a + b, d] // c
    ) {
    | _ => 1
    }

Review: Why look at the line of the operator rather than at the comments? A line comment between the left operand and the operator always puts the operator on a later line, and the check needs only the expression, before printing takes the comments out of the table. A multi-line binary expression without such a comment only costs another comment in the same operand its place next to its item.

Area: printer tests
Output: unchanged
Tests: ~tests/syntax_tests/data/printer/comments/operandBeforeBlock.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt and others added 2 commits October 27, 2026 10:35
The operands of switch, try, for, for..of and for await..of were each
printed by the same block: the expression, its parentheses or braces, and
print_operand_before_brace. print_block_operand does that once; if and
while keep their own condition printing. The output is unchanged.

Area: printer
Output: unchanged
Formatting: unchanged, no snapshot changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
These cases from the dropped #8906 guard the parentheses against a comment
after an item of an array, list, tuple, record or call, after a pipe's
receiver, and before the operator of a binary expression or pipe inside an
item, under switch, if, while, for..of and try. Each formats the same
again.

Area: tests
Output: unchanged
Tests: ~tests/syntax_tests/data/printer/comments/operandLineComment.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt removed this pull request from stack #9062 October 27, 2026 10:50
@cknitt
cknitt added this pull request to stack #9064 October 27, 2026 10:50
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 364e15225c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt
cknitt marked this pull request as ready for review October 27, 2026 11:39
@cknitt
cknitt requested a review from cristianoc October 27, 2026 11:40
@cknitt
cknitt merged commit 051894f into master Oct 27, 2026
26 checks passed
@cknitt
cknitt deleted the operand-line-suffix branch October 27, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formatter moves a line comment in a switch/if/loop operand into the block

2 participants