diff --git a/CHANGELOG.md b/CHANGELOG.md
index d444142eb8..08cdfd876c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@
#### :nail_care: Polish
- Avoid running `rescript-schema-ppx` and `sury-ppx` on source files without an `@schema` annotation. https://github.com/rescript-lang/rescript/pull/8662
+- Format JSX expression children and standalone comments with braces to prepare for future literal text support. https://github.com/rescript-lang/rescript/pull/8666
#### :house: Internal
diff --git a/compiler/syntax/JSX.md b/compiler/syntax/JSX.md
index fc6f5ecf27..e929282d7a 100644
--- a/compiler/syntax/JSX.md
+++ b/compiler/syntax/JSX.md
@@ -42,6 +42,8 @@ rewrites those nodes as calls to the configured JSX module:
directly, which supports external components;
- a lowercase tag is emitted through the configured host-element module;
- fragments use the configured `jsxFragment` value;
+- comment-only child containers are retained as comments by the parser and do
+ not contribute a child; bare `{}` still denotes an empty record expression;
- one child becomes a `children` prop and multiple children become an array;
- keyed elements select the keyed runtime entry point;
- at most one props spread is accepted, and it must precede explicit props.
diff --git a/compiler/syntax/src/res_comments_table.ml b/compiler/syntax/src/res_comments_table.ml
index c60c3a7602..72917f32fa 100644
--- a/compiler/syntax/src/res_comments_table.ml
+++ b/compiler/syntax/src/res_comments_table.ml
@@ -417,6 +417,7 @@ type node =
| CoreType of Parsetree.core_type
| ExprArgument of {expr: Parsetree.expression; loc: Location.t}
| Expression of Parsetree.expression
+ | JsxChild of Parsetree.expression
| ExprRecordRow of Longident.t Asttypes.loc * Parsetree.expression
| ExtensionConstructor of Parsetree.extension_constructor
| LabelDeclaration of Parsetree.label_declaration
@@ -448,7 +449,7 @@ let get_loc node =
}
| CoreType ct -> ct.ptyp_loc
| ExprArgument {loc} -> loc
- | Expression e -> (
+ | Expression e | JsxChild e -> (
match e.pexp_attributes with
| ({txt = "res.braces" | "ns.braces"; loc}, _) :: _ -> loc
| _ -> e.pexp_loc)
@@ -640,6 +641,13 @@ and walk_node node tbl comments =
| CoreType ct -> walk_core_type ct tbl comments
| ExprArgument ea -> walk_expr_argument ea.expr ea.loc tbl comments
| Expression e -> walk_expression e tbl comments
+ | JsxChild e ->
+ (* Braces may contain comments outside the expression itself. Keep those
+ on the child, rather than moving them into record fields or call args. *)
+ let leading, inside, trailing = partition_by_loc comments e.pexp_loc in
+ attach tbl.leading e.pexp_loc leading;
+ walk_expression e tbl inside;
+ attach tbl.trailing e.pexp_loc trailing
| ExprRecordRow (ri, e) -> walk_expr_record_row (ri, e) tbl comments
| ExtensionConstructor ec -> walk_extension_constructor ec tbl comments
| LabelDeclaration ld -> walk_label_declaration ld tbl comments
@@ -658,23 +666,29 @@ and walk_node node tbl comments =
| ValueBinding vb -> walk_value_binding vb tbl comments
| JsxProp prop -> walk_jsx_prop prop tbl comments
-and walk_list : ?prev_loc:Location.t -> node list -> t -> Comment.t list -> unit
- =
- fun ?prev_loc l t comments ->
+and walk_list : ?prev_node:node -> node list -> t -> Comment.t list -> unit =
+ fun ?prev_node l t comments ->
match l with
| _ when comments = [] -> ()
| [] -> (
- match prev_loc with
- | Some loc -> attach t.trailing loc comments
+ match prev_node with
+ | Some node -> attach t.trailing (get_loc node) comments
| None -> ())
| node :: rest ->
let curr_loc = get_loc node in
let leading, inside, trailing = partition_by_loc comments curr_loc in
- (match prev_loc with
+ (match prev_node with
| None ->
(* first node, all leading comments attach here *)
attach t.leading curr_loc leading
- | Some prev_loc ->
+ | Some (JsxChild ({pexp_desc = Pexp_jsx_element _} as child))
+ when Res_parens.jsx_child_expr child = Nothing ->
+ (* Standalone containers put their comments on separate lines. Always
+ attach comments between bare JSX elements to the next child so the
+ first format and subsequent parses agree. *)
+ attach t.leading curr_loc leading
+ | Some prev_node ->
+ let prev_loc = get_loc prev_node in
(* Same line *)
if prev_loc.loc_end.pos_lnum == curr_loc.loc_start.pos_lnum then (
let after_prev, before_curr =
@@ -692,7 +706,7 @@ and walk_list : ?prev_loc:Location.t -> node list -> t -> Comment.t list -> unit
in
attach t.leading curr_loc leading);
walk_node node t inside;
- walk_list ~prev_loc:curr_loc rest t trailing
+ walk_list ~prev_node:node rest t trailing
(* The parsetree doesn't always contain location info about the opening or
* closing token of a "list-of-things". This routine visits the whole list,
@@ -1599,15 +1613,14 @@ and walk_expression expr t comments =
| Pexp_jsx_element
(Jsx_fragment
{
- jsx_fragment_opening = opening_greater_than;
+ jsx_fragment_opening = _opening_greater_than;
jsx_fragment_children = children;
jsx_fragment_closing = _closing_lesser_than;
- }) ->
- let opening_token = {expr.pexp_loc with loc_end = opening_greater_than} in
- let on_same_line, rest = partition_by_on_same_line opening_token comments in
- attach t.trailing opening_token on_same_line;
- let xs = children |> List.map (fun e -> Expression e) in
- walk_list xs t rest
+ }) -> (
+ match children with
+ | [] -> attach t.inside expr.pexp_loc comments
+ | children -> walk_list (List.map (fun e -> JsxChild e) children) t comments
+ )
| Pexp_jsx_element
(Jsx_unary_element
{
@@ -1696,12 +1709,6 @@ and walk_expression expr t comments =
rest
in
- (* comments after '>' on the same line should be attached to '>' *)
- let after_opening_greater_than, rest =
- partition_by_on_same_line opening_greater_than_loc rest
- in
- attach t.trailing opening_greater_than_loc after_opening_greater_than;
-
let comments_for_children, _rest =
match closing_tag with
| None -> (rest, [])
@@ -1712,39 +1719,9 @@ and walk_expression expr t comments =
partition_leading_trailing rest closing_tag_loc
in
match children with
- | [] -> (
- (* attach all comments to the closing tag if there are no children *)
- match closing_tag with
- | None ->
- (* if there is no closing tag, the comments will attached after the expression *)
- ()
- | Some closing_tag ->
- let closing_tag_loc =
- Parsetree_viewer.container_element_closing_tag_loc closing_tag
- in
- if
- opening_greater_than_loc.loc_end.pos_lnum
- < closing_tag_loc.loc_start.pos_lnum + 1
- then (
- (* In this case, there are no children but there are comments between the opening and closing tag,
- We can attach these the inside table, to easily print them later as indented comments
- For example:
-
- // comment 1
- // comment 2
-
- *)
- let inside_comments, leading_for_closing_tag =
- partition_between_lines opening_greater_than_loc.loc_end.pos_lnum
- closing_tag_loc.loc_start.pos_lnum comments_for_children
- in
- attach t.inside expr.pexp_loc inside_comments;
- attach t.leading closing_tag_loc leading_for_closing_tag)
- else
- (* if the closing tag is on the same line, attach comments to the opening tag *)
- attach t.leading closing_tag_loc comments_for_children)
+ | [] -> attach t.inside expr.pexp_loc comments_for_children
| children ->
- let children_nodes = List.map (fun e -> Expression e) children in
+ let children_nodes = List.map (fun e -> JsxChild e) children in
walk_list children_nodes t comments_for_children
(* It is less likely that there are comments inside the closing tag,
diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml
index c1a905c33c..fd294d0b9f 100644
--- a/compiler/syntax/src/res_core.ml
+++ b/compiler/syntax/src/res_core.ml
@@ -2964,6 +2964,22 @@ and parse_jsx_children p : Parsetree.jsx_children =
parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p
in
loop p (child :: children)
+ | Lbrace when Parser.peek2 p = Rbrace ->
+ let start_pos = Parser.start_pos p in
+ Parser.next p;
+ let comments_before = p.comments in
+ Parser.next p;
+ (* Only comment-containing containers are trivia. Keep bare {} as an
+ empty record, including when nested inside an expression container. *)
+ if p.comments != comments_before then loop p children
+ else
+ let loc = mk_loc start_pos (Parser.position p) in
+ let child =
+ parse_primary_expr
+ ~operand:(Ast_helper.Exp.record ~loc [] None)
+ ~no_call:true p
+ in
+ loop p (child :: children)
| token when Grammar.is_jsx_child_start token ->
let child =
parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p
diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml
index b24c5fbede..42324d7035 100644
--- a/compiler/syntax/src/res_parens.ml
+++ b/compiler/syntax/src/res_parens.ml
@@ -348,39 +348,17 @@ let jsx_child_expr expr =
match expr.Parsetree.pexp_desc with
| Parsetree.Pexp_let _ | Pexp_sequence _ | Pexp_letexception _
| Pexp_letmodule _ | Pexp_open _ ->
+ (* These expressions already print as braced blocks. *)
Nothing
| _ -> (
let opt_braces, _ = Parsetree_viewer.process_braces_attr expr in
match opt_braces with
| Some ({Location.loc = braces_loc}, _) -> Braced braces_loc
| _ -> (
- match expr with
- | {
- Parsetree.pexp_desc =
- Pexp_constant (Pconst_integer (x, _) | Pconst_float (x, _));
- pexp_attributes = [];
- }
- when starts_with_minus x ->
- Parenthesized
- | _ when Parsetree_viewer.expr_is_await expr -> Parenthesized
- | {
- Parsetree.pexp_desc =
- ( Pexp_ident _ | Pexp_constant _ | Pexp_regexp _ | Pexp_field _
- | Pexp_construct _ | Pexp_variant _ | Pexp_array _ | Pexp_pack _
- | Pexp_record _ | Pexp_object_literal _ | Pexp_extension _
- | Pexp_letmodule _ | Pexp_letexception _ | Pexp_open _
- | Pexp_sequence _ | Pexp_let _ | Pexp_jsx_element _ );
- pexp_attributes = [];
- } ->
- Nothing
- | {
- Parsetree.pexp_desc =
- Pexp_constraint
- ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _});
- pexp_attributes = [];
- } ->
- Nothing
- | {pexp_desc = Pexp_jsx_element _} -> Nothing
+ match expr.pexp_desc with
+ | Pexp_jsx_element _ -> Nothing
+ (* JSX child expressions use braces even when the legacy grammar accepts
+ them bare. Records need an outer pair around their own braces. *)
| _ -> Parenthesized))
let binary_expr expr =
diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli
index 8d304823f4..c036093484 100644
--- a/compiler/syntax/src/res_parens.mli
+++ b/compiler/syntax/src/res_parens.mli
@@ -23,6 +23,9 @@ val field_expr : Parsetree.expression -> kind
val ternary_operand : Parsetree.expression -> kind
val jsx_prop_expr : Parsetree.expression -> kind
+
+(* JSX children use braces for [Parenthesized]; [Nothing] is reserved for JSX
+ elements and expressions that already print as braced blocks. *)
val jsx_child_expr : Parsetree.expression -> kind
val binary_expr : Parsetree.expression -> kind
diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml
index 7854dbe709..6ddf6ce76f 100644
--- a/compiler/syntax/src/res_printer.ml
+++ b/compiler/syntax/src/res_printer.ml
@@ -59,12 +59,6 @@ let has_leading_line_comment tbl loc =
| Some comment -> Comment.is_single_line_comment comment
| None -> false
-let get_leading_line_comment_count tbl loc =
- match Hashtbl.find_opt tbl.Comment_table.leading loc with
- | Some comments ->
- List.filter Comment.is_single_line_comment comments |> List.length
- | None -> 0
-
let has_trailing_single_line_comment tbl loc =
match Hashtbl.find_opt tbl.Comment_table.trailing loc with
| Some (comment :: _) -> Comment.is_single_line_comment comment
@@ -4629,6 +4623,23 @@ and print_jsx_unary_tag ~state tag_name props expr_loc cmt_tbl =
closing_tag_doc;
])
+(* Standalone JSX comments are trivia, not expression children. Keep their
+ delimiters inside braces so future literal text cannot absorb them. *)
+and print_jsx_comment_container comments_tbl loc =
+ match Hashtbl.find_opt comments_tbl loc with
+ | None | Some [] -> Doc.nil
+ | Some comments ->
+ Hashtbl.remove comments_tbl loc;
+ let docs =
+ List.map
+ (fun comment ->
+ if Comment.is_single_line_comment comment then
+ Doc.concat [Doc.text ("//" ^ Comment.txt comment); Doc.break_parent]
+ else print_multiline_comment_content (Comment.txt comment))
+ comments
+ in
+ add_braces (Doc.join docs ~sep:Doc.hard_line)
+
and print_jsx_container_tag ~state tag_name
(opening_greater_than : Lexing.position) props
(children : Parsetree.jsx_children)
@@ -4658,12 +4669,9 @@ and print_jsx_container_tag ~state tag_name
in
let line_sep = get_line_sep_for_jsx_children children in
let print_children children =
- Doc.concat
- [
- Doc.indent
- (Doc.concat [Doc.line; print_jsx_children ~state children cmt_tbl]);
- line_sep;
- ]
+ let doc = print_jsx_children ~state children cmt_tbl in
+ if line_sep = Doc.nil then doc
+ else Doc.concat [Doc.indent (Doc.concat [line_sep; doc]); line_sep]
in
(* comments between the opening and closing tag *)
@@ -4723,7 +4731,7 @@ and print_jsx_container_tag ~state tag_name
[
(if has_children then print_children children
else if not has_comments_inside then Doc.soft_line
- else print_comments_inside cmt_tbl pexp_loc);
+ else print_jsx_comment_container cmt_tbl.inside pexp_loc);
closing_element_doc;
];
])
@@ -4751,22 +4759,30 @@ and print_jsx_fragment ~state (opening_greater_than : Lexing.position)
(Doc.concat
[
opening;
- Doc.indent
- (Doc.concat [Doc.line; print_jsx_children ~state children cmt_tbl]);
+ (let doc =
+ if has_children then print_jsx_children ~state children cmt_tbl
+ else print_jsx_comment_container cmt_tbl.inside fragment_loc
+ in
+ if line_sep = Doc.nil then doc
+ else Doc.indent (Doc.concat [line_sep; doc]));
(if has_children then line_sep else Doc.nil);
closing;
])
and get_line_sep_for_jsx_children (children : Parsetree.jsx_children) =
+ (* Keep wrapping a single expression inside its braces, never between it and
+ its tag. Layout between sibling children still uses the legacy whitespace
+ rules until literal JSX text is introduced. *)
if
List.length children > 1
|| List.exists
(function
- | {Parsetree.pexp_desc = Pexp_jsx_element _} -> true
+ | {Parsetree.pexp_desc = Pexp_jsx_element _} as child ->
+ Parens.jsx_child_expr child = Nothing
| _ -> false)
children
then Doc.hard_line
- else Doc.line
+ else Doc.nil
and print_jsx_children ~state (children : Parsetree.jsx_children) cmt_tbl =
let open Parsetree in
@@ -4784,23 +4800,59 @@ and print_jsx_children ~state (children : Parsetree.jsx_children) cmt_tbl =
in
let sep = get_line_sep_for_jsx_children children in
let print_expr (expr : Parsetree.expression) =
- let leading_line_comment_present =
- has_leading_line_comment cmt_tbl expr.pexp_loc
- in
- let expr_doc = print_expression_with_comments ~state expr cmt_tbl in
- let add_parens_or_braces expr_doc =
- (* {(20: int)} make sure that we also protect the expression inside *)
- let inner_doc =
- if Parens.braced_expr expr then add_parens expr_doc else expr_doc
+ let loc = get_loc expr in
+ match (expr.pexp_desc, Parens.jsx_child_expr expr) with
+ | Pexp_jsx_element _, Nothing ->
+ let leading = print_jsx_comment_container cmt_tbl.leading loc in
+ let trailing = print_jsx_comment_container cmt_tbl.trailing loc in
+ Doc.concat
+ [
+ leading;
+ (if leading = Doc.nil then Doc.nil else Doc.hard_line);
+ print_expression_with_comments ~state expr cmt_tbl;
+ (if trailing = Doc.nil then Doc.nil else Doc.hard_line);
+ trailing;
+ ]
+ | _ ->
+ let has_line_comment =
+ has_leading_line_comment cmt_tbl loc
+ || has_any_trailing_line_comment cmt_tbl loc
+ || has_leading_line_comment cmt_tbl expr.pexp_loc
+ || has_any_trailing_line_comment cmt_tbl expr.pexp_loc
in
- if leading_line_comment_present then add_braces inner_doc
- else Doc.concat [Doc.lbrace; inner_doc; Doc.rbrace]
- in
- match Parens.jsx_child_expr expr with
- | Nothing -> print_comments expr_doc cmt_tbl (get_loc expr)
- | Parenthesized -> add_parens_or_braces expr_doc
- | Braced braces_loc ->
- print_comments (add_parens_or_braces expr_doc) cmt_tbl braces_loc
+ (* Consume comments before printing subnodes that may share these
+ locations. Both outer and inner comments belong inside the child
+ expression's braces, outside the future JSX text region. *)
+ let leading = print_leading_comments Doc.nil cmt_tbl.leading loc in
+ let inner_leading =
+ print_leading_comments Doc.nil cmt_tbl.leading expr.pexp_loc
+ in
+ let inner_trailing =
+ print_trailing_comments Doc.nil cmt_tbl.trailing expr.pexp_loc
+ in
+ let trailing = print_trailing_comments Doc.nil cmt_tbl.trailing loc in
+ let expr_doc =
+ match expr.pexp_desc with
+ | Pexp_let _ | Pexp_sequence _ | Pexp_letexception _ | Pexp_letmodule _
+ | Pexp_open _
+ when not
+ (Parsetree_viewer.has_printable_attributes expr.pexp_attributes)
+ ->
+ print_expression_block ~state ~braces:false expr cmt_tbl
+ | _ ->
+ let doc = print_expression_with_comments ~state expr cmt_tbl in
+ if Parens.braced_expr expr then add_parens doc else doc
+ in
+ add_braces
+ (Doc.concat
+ [
+ leading;
+ inner_leading;
+ expr_doc;
+ inner_trailing;
+ trailing;
+ (if has_line_comment then Doc.break_parent else Doc.nil);
+ ])
in
match children with
| [] -> Doc.nil
@@ -4819,15 +4871,13 @@ and print_jsx_children ~state (children : Parsetree.jsx_children) cmt_tbl =
loc.loc_start.pos_lnum
in
let lines_between = start_line_y - end_line_x - 1 in
- let leading_single_line_comments =
- get_leading_line_comment_count cmt_tbl (get_loc y)
+ (* Comment containers add lines around their comments. Do not mistake
+ those generated lines for blank lines between children on reparse. *)
+ let has_between_comments =
+ has_leading_comments cmt_tbl (get_loc y)
+ || Hashtbl.mem cmt_tbl.trailing (get_loc x)
in
- (* If there are lines between the jsx elements, we preserve at least one line *)
- if
- (* Unless they are all comments *)
- (* The edge case of comment followed by blank line is not caught here *)
- lines_between > 0 && not (lines_between = leading_single_line_comments)
- then
+ if lines_between > 0 && not has_between_comments then
let doc = Doc.concat [print_expr x; sep; Doc.hard_line] in
visit (Doc.concat [acc; doc]) rest
else
diff --git a/packages/dev-playground/src/Main.res b/packages/dev-playground/src/Main.res
index 9a37f0ad1b..57670095bc 100644
--- a/packages/dev-playground/src/Main.res
+++ b/packages/dev-playground/src/Main.res
@@ -273,9 +273,7 @@ let outputNode = (output, activeTab, onSourceMapSelect): View.node => {
event->Event.preventDefault
onSourceMapSelect()
}}
- >
- {View.text(sourceMapDirective)}
- ,
+ >{View.text(sourceMapDirective)},
View.text(output->String.slice(~start=directiveEnd)),
])
}
@@ -297,9 +295,7 @@ let pushOutputText = (nodes: array, text, onSourceMapSelect) => {
event->Event.preventDefault
onSourceMapSelect()
}}
- >
- {View.text(sourceMapDirective)}
- ,
+ >{View.text(sourceMapDirective)},
)
nodes->Array.push(View.text(text->String.slice(~start=directiveEnd)))
}
@@ -366,9 +362,7 @@ let mappedJavaScriptNode = (
onMappingSelect(mapping)
}
}}
- >
- {View.text(text)}
- ,
+ >{View.text(text)},
)
}
| None => pushOutputText(nodes, text, onSourceMapSelect)
@@ -429,9 +423,7 @@ module TabButton = {
+ >{View.text(tabLabel(tab))}
}
}
@@ -439,9 +431,9 @@ module Problems = {
@jsx.component
let make = (~compileResult: Signal.t)
+ )->Array.map(version => )
),
)
@@ -488,9 +480,7 @@ module SettingsPanel = {
Signal.get(activeTab) === Settings ? "settings-panel" : "settings-panel hidden-panel"}
>
-
+
+ >{View.signalFragment(compilerVersionOptions)}
-
-
- // Must not jump inside braces
- {// But this one is inside
- React.string("Hello, World!")}
-
+
{
+ // Move this child comment inside braces
+ // This one is already inside
+ React.string("Hello, World!")
+}
-
- // Outside comment
- {
- // But this one is inside
- let x = 1
- let y = 2
- }
-
+
{
+ // Move this block comment inside braces
+ // This one is already inside
+ let x = 1
+ let y = 2
+}
let x =
<>
- // before a
- {a} // after a
- // before b
- {b} // after b
+ {
+ // before a
+ a // after a
+ }
+ {
+ // before b
+ b // after b
+ }
>
+
+let standaloneMultilineComment =
+
- // Must not jump inside braces
+ // Move this child comment inside braces
{React.string("Hello, World!")}
- // Must not jump inside braces
- {// But this one is inside
+ // Move this child comment inside braces
+ {// This one is already inside
React.string("Hello, World!")}
- // Outside comment
- {// But this one is inside
+ // Move this block comment inside braces
+ {// This one is already inside
let x = 1
let y = 2
}
@@ -81,3 +81,6 @@ let x = <>
// before b
{b} // after b
>
+
+let standaloneMultilineComment =
let aggregateTotal = (forecast, ~audienceType) =>
Nullable.toOption(forecast["audiences"])
diff --git a/tests/syntax_tests/data/printer/expr/expected/braced.res.txt b/tests/syntax_tests/data/printer/expr/expected/braced.res.txt
index a3ca220dec..5352222e30 100644
--- a/tests/syntax_tests/data/printer/expr/expected/braced.res.txt
+++ b/tests/syntax_tests/data/printer/expr/expected/braced.res.txt
@@ -282,7 +282,7 @@ apply({
a
})
-let x = {
child
}
+let x = {
{child}
}
// not valid jsx
let x = {@JSX child}
@@ -318,5 +318,5 @@ let x = {
}
// string constant should be printed correct
-<> {"\n"->React.string} >
-<> {"\""->React.string} >
+<>{"\n"->React.string}>
+<>{"\""->React.string}>
diff --git a/tests/syntax_tests/data/printer/expr/expected/callback.res.txt b/tests/syntax_tests/data/printer/expr/expected/callback.res.txt
index 3d8f6c7d55..86ff040538 100644
--- a/tests/syntax_tests/data/printer/expr/expected/callback.res.txt
+++ b/tests/syntax_tests/data/printer/expr/expected/callback.res.txt
@@ -194,25 +194,25 @@ foo(\"switch" => \"switch"())
// similar, the jsx tree should be broken over multiple lines
let f = () => {
-
}
myPromise
diff --git a/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt b/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt
index a0d856dc28..3c74ea4de5 100644
--- a/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt
+++ b/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt
@@ -54,7 +54,7 @@ let record = {field: (v :> b)}
let conditional = condition ? (v :> b) : (w :> b)
let block = {v :> b}
let default = (~arg=v :> b) => arg
-let jsx = b}> {v :> b}
+let jsx = b}>{v :> b}
// Comments must stay attached when parentheses disappear.
foo(/* before */ v /* operand */ :> /* type */ b /* after */)
diff --git a/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt b/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt
index 9ba0e0c3d3..7f05729c4c 100644
--- a/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt
+++ b/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt
@@ -61,8 +61,8 @@ let x = 1
let x =
f(a, b, _)[ix]
f(a, b, _)[ix] = 2
diff --git a/tests/syntax_tests/data/printer/expr/jsxChildren.res b/tests/syntax_tests/data/printer/expr/jsxChildren.res
new file mode 100644
index 0000000000..ac4b69a78d
--- /dev/null
+++ b/tests/syntax_tests/data/printer/expr/jsxChildren.res
@@ -0,0 +1,149 @@
+// Bare expression children migrate without changing their meaning.
+let x = hello
+let x = { hello }
+let x = user.name
+let x = "hello"
+let x = 42
+let x = (-42)
+let x = true
+let x = None
+let x = Some(hello)
+let x = #hello
+let x = [hello, world]
+let x = list{hello, world}
+let x = /hello/g
+let x = `hello ${name}`
+let x = %extension(hello)
+let x = (hello, world)
+let x = (hello: element)
+let x = (hello :> element)
+let x = {name: hello}
+let x = {"name": hello}
+let x = {}
+let x = dict{"name": hello}
+let x = module(Hello)
+let x = module(Hello: Greeting)
+
+// Nested JSX stays unwrapped, including nested fragments.
+let x = hello user.name
+let x = <> hello >
+let x = <> hello >
+let x = <> {hello} >
+let x = <> hello world >
+let x = <> >
+let x =
+
+// Existing expression braces and block braces must not accumulate.
+let x = {render(hello)}
+let x = {hello->render}
+let x = {condition ? hello : world}
+let x = {switch value { | Some(child) => child | None => fallback }}
+let x = {let child = render(hello); child}
+let x = {log(); hello}
+let x = {open Hello; child}
+let x = {module Hello = Greeting; Hello.child}
+let x = {exception Empty; hello}
+let x = {@label hello}
+let x = {@label {let child = hello; child}}
+let x = {}
+let x = /* before braced JSX */ {} // after braced JSX
+
+let x = <> // before braced fragment
+ {<> hello >} // after braced fragment
+>
+
+// Narrow layouts should retain every comment and remain stable.
+let x = /* before bare */ hello /* after bare */
+let x = {/* inside */ hello /* still inside */}
+let x = <> {/* inside fragment */ hello /* still in fragment */} >
+let x = hello /* between children */ world
+let x = {{/* record field */ name: hello}}
+// Child comments and their line breaks stay inside expression braces.
+let x = hello // bare child
+
+let x = <> hello // bare fragment child
+>
+let x = user.name // field child
+
+let x = "hello" // string child
+
+let x = {name: hello} // record child
+
+let x = {hello} // already braced child
+
+let x = hello /* block comment */ // line comment
+
+let x = // before the child
+ hello // Contains */ and must remain a line comment.
+
+let x = <> /* before record */ {name: hello} /* after record */ >
+let x =
+ // before a block
+ {let child = hello; child} // after a block
+
+let x =
+ // before a call
+ {render(hello)} // after a call
+
+let x =
+ // before bare child
+ hello
+ // after bare child
+
+let x =
+ {hello // trailing inside braces
+ }
+
+let x = <>
+ // before fragment child
+ hello
+>
+let x =
+ // before record
+ {name: hello} // after record
+
+let x =
+ aLongChildIdentifierThatWillNeedToWrapAtNarrowWidths
+
+
+let emptyWithBlockComment = /* empty */
+let emptyWithLineComment = // contains */
+
+let emptyFragmentWithInlineBlockComment = <>/* inside */>
+let emptyFragmentWithInlineLineComment = <>// inside
+>
+let emptyFragmentWithMixedComments = <>/* first */ // second
+ /* third */
+>
+let emptyFragmentWithOutsideComment = <>/* inside */> // outside
+let emptyFragmentWithComments = <>
+ // first
+ /* second */
+>
+let nestedStandaloneComments =
+ // before
+ // after
+ /* between */
+ <> /* nested */>
+ // last
+
+let nestedInlineComments =
/* before *//* after */
+let explicitCommentContainers = <>
+ {/* before */}
+
+ {// contains */
+ }
+
+ {/* after */}
+>
+let adjacentCommentContainers = {/* one */}{/* two */}
+let commentBeforeExpression = {/* before */}{hello}{/* after */}
+let emptyRecordChild = {}
+let wrappedEmptyRecordChild = {{/* record */}}
+
+let mixedStandaloneComments = <>
+ // before expression
+ {hello} // after expression
+ /* before element */
+ {/* last block */}
+>
diff --git a/tests/syntax_tests/data/printer/expr/switch.res b/tests/syntax_tests/data/printer/expr/switch.res
index 0313cab633..deaeb33a54 100644
--- a/tests/syntax_tests/data/printer/expr/switch.res
+++ b/tests/syntax_tests/data/printer/expr/switch.res
@@ -40,11 +40,11 @@ switch count {
}
switch route {
-| A =>
// div tag stays after >
+| A =>
// comment after the opening tag
{React.string("First A div")}
{React.string("Second A div")}
-| B => <> // fragment tag stays after <>
+| B => <> // comment after the fragment opener
{React.string("First B div")}
{React.string("Second B div")}
>
diff --git a/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt b/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt
index 62bf5b7504..784733579f 100644
--- a/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt
+++ b/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt
@@ -34,7 +34,7 @@ let make = (~min=50, ~max=250, ~meterSuffix=?) => {
values
onChange={v => values_set(_ => v)}
renderTrack={({props, children}) => {
- let element =
children
+ let element =
{children}
ReasonReact.cloneElement(element, ~props=Obj.magic(props), [children])
}}
diff --git a/tests/syntax_tests/data/printer/other/expected/home.res.txt b/tests/syntax_tests/data/printer/other/expected/home.res.txt
index 326697c0d0..605bde0aed 100644
--- a/tests/syntax_tests/data/printer/other/expected/home.res.txt
+++ b/tests/syntax_tests/data/printer/other/expected/home.res.txt
@@ -10,11 +10,11 @@ let make = () => {
// Template
-
{"Other stuff here"->string}
+
{"Other stuff here"->string}
- {" BPM"->string} } />
+ {" BPM"->string}} />
diff --git a/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt b/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt
index fe32b18e3c..806580c832 100644
--- a/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt
+++ b/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt
@@ -25,20 +25,20 @@ let make = () => {
}