Skip to content

format JSX expression children with explicit braces - #8666

Merged
tsnobip merged 14 commits into
masterfrom
wrap-children-in-jsx
Oct 15, 2026
Merged

tsnobip merged 14 commits into
masterfrom
wrap-children-in-jsx

Conversation

@tsnobip

@tsnobip tsnobip commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Prepare JSX for future literal text support by formatting expression children with explicit braces and no surrounding inline spaces:

// Before
let x = <span> hello </span>

// After
let x = <span>{hello}</span>

Existing unbraced expressions remain valid syntax. Nested JSX elements stay unwrapped, and existing expression braces do not accumulate. Single expression children stay inline when they fit; multiline children remain indented between the opening and closing tags.

Standalone comments use their own comment-only containers, including before or after expression children and in empty elements or fragments. Separate containers, comment order, and intentional blank-line separation are preserved:

let x =
  <div>
    {/* before */}
    <Icon />

    {/* after */}
    {hello}
  </div>

To reduce formatting churn, safe JSX child line comments become block comments. Conversion is limited to comments whose text contains neither /* nor */; comments containing either delimiter retain // and the newlines needed to keep the closing brace outside the comment. Comments already inside an expression container stay with that expression:

// Before
let x = <span> hello // note
</span>

// After
let x = <span>{hello /* note */}</span>

The parser retains comment-only containers as trivia without adding a child, preserving the distinction between zero, one, and multiple children for React and custom JSX. Bare {} still denotes an empty record child; {{/* comment */}} keeps the comment inside that record.

Literal text parsing remains future work, including handling formatting indentation around children. The next migration step is to reject unbraced expressions before enabling literal text, so existing variable references cannot silently become strings.

Validation:

  • Formatting regressions cover tags and fragments, standalone and expression comments, separate containers, blank lines, inline block comments, and safe/unsafe line-comment conversion. They check expression and comment-text preservation and repeated formatting at five widths; printing also leaves the original comment objects unchanged.
  • Parser and JSX transform tests verify unchanged child counts for React and custom JSX. Malformed containers remain parse errors, and AST bridge fixtures and generated JavaScript cover transformation and JSX-preserve paths.
  • make test, make test-syntax, make test-syntax-roundtrip, and yarn changelog:check pass locally.

Supersedes #7863.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 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-14T15:15:36.250042Z 239d943 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.

@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch 2 times, most recently from 4495a55 to 78476d8 Compare September 22, 2026 11:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a479f8349e

ℹ️ 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".

Comment thread CHANGELOG.md Outdated
@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch from 78476d8 to 701313d Compare September 22, 2026 12:36
@tsnobip

tsnobip commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 701313d0e0

ℹ️ 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".

Comment thread compiler/syntax/src/res_printer.ml Outdated
@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.47514% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.54%. Comparing base (50a4103) to head (239d943).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
compiler/syntax/src/res_printer.ml 94.44% 7 Missing ⚠️
compiler/syntax/src/res_doc.ml 50.00% 2 Missing ⚠️
compiler/syntax/src/res_core.ml 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8666      +/-   ##
==========================================
+ Coverage   80.51%   80.54%   +0.03%     
==========================================
  Files         447      447              
  Lines       61060    61155      +95     
==========================================
+ Hits        49162    49259      +97     
+ Misses      11898    11896       -2     
Files with missing lines Coverage Δ
compiler/syntax/src/res_comment.ml 82.05% <100.00%> (+5.38%) ⬆️
compiler/syntax/src/res_comments_table.ml 91.32% <100.00%> (-0.13%) ⬇️
compiler/syntax/src/res_parens.ml 86.95% <ø> (+2.89%) ⬆️
compiler/syntax/src/res_core.ml 91.99% <94.11%> (+<0.01%) ⬆️
compiler/syntax/src/res_doc.ml 68.22% <50.00%> (-0.39%) ⬇️
compiler/syntax/src/res_printer.ml 93.61% <94.44%> (+0.04%) ⬆️
🚀 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 Sep 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8666

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 239d943

@github-actions

Copy link
Copy Markdown

@tsnobip

tsnobip commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 6d6005ba9f

ℹ️ 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 commented Sep 26, 2026

Copy link
Copy Markdown
Member

Good to be moving closer to JS here in terms of whitespace handling.

The multiline formatting as can be seen in the snapshot diffs seems weird to me though, and doesn't seem to be necessary / match JS behavior.

ReScript, before the change:

let x =
  <div>
    {value
      ? <XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX />
      : <YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY />}
  </div>

ReScript, after the change:

let x =
  <div>{
    value
      ? <XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX />
      : <YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY />
  }</div>

Prettier (JS):

let x = (
  <div>
    {value ? (
      <XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX />
    ) : (
      <YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY />
    )}
  </div>
);

Babel output for the latter:

import { jsx as _jsx } from "react/jsx-runtime";
let x = /*#__PURE__*/_jsx("div", {
  children: value ? /*#__PURE__*/_jsx(XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX, {}) : /*#__PURE__*/_jsx(YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY, {})
});

So it is not necessary that the opening/closing brace moves to the line of the opening/closing JSX tag.

@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch from e1ac4c7 to 7442c14 Compare October 2, 2026 13:33
@tsnobip
tsnobip requested a review from cknitt October 5, 2026 13:47
@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch 2 times, most recently from 4697172 to e7c4842 Compare October 10, 2026 15:52
@cknitt

cknitt commented Oct 11, 2026

Copy link
Copy Markdown
Member

@codex review

@cknitt

cknitt commented Oct 11, 2026

Copy link
Copy Markdown
Member

Tested against a large company project of ours. Looks perfect now, only the expected changes, no more unexpected multiline format changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b62553851

ℹ️ 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".

Comment thread compiler/syntax/src/res_printer.ml Outdated
Comment thread CHANGELOG.md Outdated
@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch from a98621e to 0635ad6 Compare October 12, 2026 14:54
@cknitt

cknitt commented Oct 12, 2026

Copy link
Copy Markdown
Member

Thanks for working on this! I had Claude review the PR. It built res_cli.exe from this branch and ran the formatter on a set of edge cases. Every case below gives the same output on a second formatting pass, so the output is stable but wrong.

Correctness

  1. A comment after the last expression child ends up outside the braces (res_printer.ml ~L4844). The trailing comment is printed through print_trailing_comments, which wraps it in Doc.line_suffix. That only flushes at the next line break, which comes after the closing }.

    <div>
      {x}
      /* a */
    </div>

    This prints /* a */ as bare JSX text, and {/* a */} in the same spot loses its braces too. This is exactly what the PR is meant to prevent, so I'd consider it the main blocker.

  2. Bad indentation when a block comment comes before an expression (~L4860). wrap only switches to the indented add_braces layout for line comments or block expressions. With a block comment on its own line, the leading hard_line ends up inside a flat {…}:

    <div>
      {x}
    
      /* c */
      {y}
    </div>

    This formats to {/* c */ followed by y} on the next line.

  3. Blank lines move or disappear (~L4910). The blank-line check looks at the next child's leading comments, but the blank line is always printed before those comments.

    • <span /> / /* a */ / blank line / <b /> moves the blank line above the comment.
    • {/* a */} / blank line / {/* b */} / <b /> merges both into a single { /* a */ /* b */ } with no blank line.

    This contradicts the CHANGELOG's claim that blank-line separation is kept.

  4. Separate comment containers are always merged (~L4817). print_jsx_comment_container puts all leading or trailing comments of an element into one container, so

    <div>
      {/* a */}
    
      {/* b */}
    </div>

    becomes a single { /* a */ /* b */ } block.

  5. A standalone comment gets pulled into the next expression's braces (res_comments_table.ml ~L701). Comments after a JSX element are always attached to the next child. When that child is an expression, the comment lands inside its braces:

    • <div> {x}{/* c */}{y} </div> becomes {/* c */ y}
    • <span /> /* c */ {x} becomes {/* c */ x}

    So the result depends on whether the previous sibling was an element or an expression.

  6. Inline block comments are always split onto multiple lines (~L4664). The container joins comments with Doc.hard_line, so <div> {/* a */ /* b */} <span /> </div> becomes a three-line block even though it fits on one line.

Cleanups

  1. when Res_parens.jsx_child_expr child = Nothing (in res_comments_table.ml ~L701, get_line_sep_for_jsx_children ~L4799, and the print_expr match) is always true for an unbraced Pexp_jsx_element, since jsx_child_expr returns Nothing for every bare JSX element. Matching on Pexp_jsx_element _ alone would be clearer.

  2. jsx_child_expr (res_parens.ml ~L364) still classifies let, sequence, open and braced-block expressions. The new print_expr now sends every non-JSX Nothing case to the brace-wrapping branch and handles blocks itself, so that classification no longer does anything and the .mli comment is misleading. Two places now decide how block children print (jsx_child_expr and print_child_expr/is_block_expr), which makes it easy to update one and forget the other. jsx_child_expr could probably be reduced to the JSX-element versus braced distinction.

Each of these cases would make a good addition to the syntax test fixtures, together with make test-syntax-roundtrip.

@tsnobip
tsnobip requested review from cknitt and removed request for cknitt October 13, 2026 11:21
@cknitt

cknitt commented Oct 13, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 7a28d87d18

ℹ️ 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 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, this looks solid. I ran a batch of edge cases through it at several widths: all stable, comments kept, no semantic changes. It also fixes several cases where the current formatter moves comments around. A few remaining points:

Design: print line comments as block comments

Line comments in children currently expand to multiline blocks:

// standalone `// c` becomes
{
  // c
}

// `x // note` becomes
{
  x // note
}

// `{x} // note` gets detached from its expression
{x}
{
  // note
}

Would it be worth turning them into block comments when the text doesn't contain */? Then these would print as {/* c */} and {x /* note */}, and x // note and {x} // note would format the same way. I understand keeping the comment style was deliberate. I'm raising it because of the churn this will cause in existing codebases.

Nits

  • res_printer.ml:4841: if doc = Doc.nil relies on structural equality on Doc.t; branching on has_children (or matching) would be clearer.
  • res_core.ml:2926: the p.comments != comments_before check only works because Parser.next commits a token's leading comments when it consumes that token, so the second next adds exactly the comments inside the braces. A short comment saying so would help, since it depends on parser internals.
  • res_comment.ml:16: jsx_container is mutable state on the comment object, so with_checkpoint doesn't roll it back. It's harmless today, since the only speculative parses are attributes, which re-parse the same way. It would be worth a note in case JSX ever gets parsed speculatively.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FxijgTFejGpN71aUm7dRdY

@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch from 5bb2161 to 3f84589 Compare October 14, 2026 11:14
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
Signed-Off-By: Paul Tsnobiladzé <dontshootthink@gmail.com>
@tsnobip
tsnobip force-pushed the wrap-children-in-jsx branch from 3f84589 to 239d943 Compare October 14, 2026 13:59
@cknitt

cknitt commented Oct 14, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cknitt

cknitt commented Oct 14, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 239d94325e

ℹ️ 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 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, the nits are all addressed and the block-comment conversion works nicely. I checked the edge cases: //*doc doesn't turn into a doc comment, and comments that contain a delimiter keep the line form. Formatting is stable at every width I tried.

Two very minor things left, both optional:

  • A line comment after an already braced child still ends up in its own container:

    // input
    <div> {x} // note
    </div>
    
    // output
    <div>
      {x}
      {/* note */}
    </div>

    The unbraced form x // note prints as <div>{x /* note */}</div>. It would be nice if both did that.

  • Line comments on adjacent lines become a multiline container with one block comment per line:

    {
      /* c1 */
      /* c2 */
    }

    {/* c1 */ /* c2 */} would be more compact, but following the source line layout is reasonable too.

Neither needs to block this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FxijgTFejGpN71aUm7dRdY

@tsnobip

tsnobip commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@cknitt I'm merging it as is, I feel like we might presume too much of the user's intentions when merging the line-comment inside the braces or merging multiple block comments. We can of course revisit this later after some usage.

@tsnobip
tsnobip merged commit ef72591 into master Oct 15, 2026
26 checks passed
@tsnobip
tsnobip deleted the wrap-children-in-jsx branch October 15, 2026 09:17
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.

2 participants