From 906230093e3153b600c533e3146ddb6eb71cf122 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Sep 2026 21:55:32 +0200 Subject: [PATCH 1/6] Fix unstable formatting of trailing callback comments Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 + compiler/syntax/src/res_printer.ml | 18 +++++++- .../printer/comments/callbackTrailing.res | 34 +++++++++++++++ .../expected/callbackTrailing.res.txt | 41 +++++++++++++++++++ tests/syntax_tests/res_test.ml | 21 ++++++++++ 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/syntax_tests/data/printer/comments/callbackTrailing.res create mode 100644 tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index bae0c93df7..68287da8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ - Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/pull/8630 - Fix the side-effect analysis treating bigint exponentiation and bounds-checked array and string reads as pure, which let dead-code elimination drop an unused one that throws: `let _ = 2n ** -1n` no longer raised. https://github.com/rescript-lang/rescript/pull/8617 - Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619 +- Fix unstable formatting of trailing comments in nested callbacks. https://github.com/rescript-lang/rescript/issues/6976 + - Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611 - Report an error instead of crashing when an integer in a variant constructor's `@as` annotation exceeds the compiler's integer range. https://github.com/rescript-lang/rescript/pull/8619 - Warn about an `@as` on a record field whose payload does not name the field, such as `@as(42)`. It renamed nothing and was silently accepted. https://github.com/rescript-lang/rescript/pull/8619 diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 33597a300d..9ae5abf5dd 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -4662,14 +4662,28 @@ and print_pexp_apply ~state expr cmt_tbl = | Braced braces -> print_braces doc call_expr braces | Nothing -> doc in - if Parsetree_viewer.requires_special_callback_printing_first_arg args then + (* Use the regular argument layout for trailing comments. Compact callback + * layouts can detach comments from the body when it breaks, making + * subsequent formatting unstable. *) + let args_have_trailing_comments = + List.exists + (fun (_, (arg : Parsetree.expression)) -> + has_trailing_comments cmt_tbl arg.pexp_loc) + args + in + if + (not args_have_trailing_comments) + && Parsetree_viewer.requires_special_callback_printing_first_arg args + then let args_doc = print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl in Doc.concat [print_attributes ~state attrs cmt_tbl; call_expr_doc; args_doc] - else if Parsetree_viewer.requires_special_callback_printing_last_arg args + else if + (not args_have_trailing_comments) + && Parsetree_viewer.requires_special_callback_printing_last_arg args then let args_doc = print_arguments_with_callback_in_last_position ~state ~partial args diff --git a/tests/syntax_tests/data/printer/comments/callbackTrailing.res b/tests/syntax_tests/data/printer/comments/callbackTrailing.res new file mode 100644 index 0000000000..53ac38e8bf --- /dev/null +++ b/tests/syntax_tests/data/printer/comments/callbackTrailing.res @@ -0,0 +1,34 @@ +let rec fib = (n, k) => + switch n { + | 0 | 1 => k(1) + | _ => + Suspend( + () => + fib(n - 1, (v0) => + fib(n - 2, (v1) => + k(v0 + v1) + /* comment */ + ) + ), + ) + } + +let first = call(x => x + /* first callback */ +, value) + +let last = call(value, x => x + // last callback +) + +let inline = call(value, x => x // inline callback +) + +let firstInline = call(x => x // first inline callback +, value) + +let nested = call(value, x => call(value, y => y + /* nested callback */ +)) + +let blocks = call(value, x => x /* inline block */) diff --git a/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt new file mode 100644 index 0000000000..12a9e9cc03 --- /dev/null +++ b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt @@ -0,0 +1,41 @@ +let rec fib = (n, k) => + switch n { + | 0 | 1 => k(1) + | _ => + Suspend( + () => + fib(n - 1, v0 => + fib( + n - 2, + v1 => k(v0 + v1), + /* comment */ + ) + ), + ) + } + +let first = call( + x => x, + /* first callback */ + value, +) + +let last = call( + value, + x => x, + // last callback +) + +let inline = call(value, x => x) // inline callback + +let firstInline = call(x => x, value) // first inline callback + +let nested = call(value, x => + call( + value, + y => y, + /* nested callback */ + ) +) + +let blocks = call(value, x => x /* inline block */) diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index 47810416ed..cc39993753 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -28,6 +28,27 @@ let x: int let () = print_endline "✅ multi printer api tests" +let () = + let filename = + Filename.concat data_dir "printer/comments/callbackTrailing.res" + in + let source = IO.read_file ~filename in + let format ~width source = + let result = + Res_driver.parse_implementation_from_source ~for_printer:true + ~display_filename:filename ~source + in + assert (not result.invalid); + Res_printer.print_implementation ~width result.parsetree + ~comments:result.comments + in + List.iter + (fun width -> + let printed = format ~width source in + assert (printed = format ~width printed)) + [20; 40; 80; 100; 120]; + print_endline "✅ callback trailing comments are stable at multiple widths" + module Outcome_printer_tests = struct let signature_to_outcome structure = Lazy.force Res_outcome_printer.setup; From ed1c9adaf50eabf2e8b299cb7e97370c26de5364 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Sep 2026 21:56:08 +0200 Subject: [PATCH 2/6] Link callback formatting changelog entry to PR Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68287da8da..0571ff1100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/pull/8630 - Fix the side-effect analysis treating bigint exponentiation and bounds-checked array and string reads as pure, which let dead-code elimination drop an unused one that throws: `let _ = 2n ** -1n` no longer raised. https://github.com/rescript-lang/rescript/pull/8617 - Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619 -- Fix unstable formatting of trailing comments in nested callbacks. https://github.com/rescript-lang/rescript/issues/6976 +- Fix unstable formatting of trailing comments in nested callbacks. https://github.com/rescript-lang/rescript/pull/8627 - Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611 - Report an error instead of crashing when an integer in a variant constructor's `@as` annotation exceeds the compiler's integer range. https://github.com/rescript-lang/rescript/pull/8619 From faabd790b084403cc56dfc5d53a227d96f04520a Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 5 Sep 2026 07:10:34 +0200 Subject: [PATCH 3/6] Clean up callback formatter layout selection and diagnostics Signed-off-by: Christoph Knittel --- compiler/syntax/src/res_printer.ml | 117 ++++++++++++----------------- tests/syntax_tests/res_test.ml | 11 ++- 2 files changed, 57 insertions(+), 71 deletions(-) diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 9ae5abf5dd..a2600f4bbf 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -4671,52 +4671,38 @@ and print_pexp_apply ~state expr cmt_tbl = has_trailing_comments cmt_tbl arg.pexp_loc) args in - if - (not args_have_trailing_comments) - && Parsetree_viewer.requires_special_callback_printing_first_arg args - then - let args_doc = - print_arguments_with_callback_in_first_position ~state ~partial args - cmt_tbl - in - Doc.concat - [print_attributes ~state attrs cmt_tbl; call_expr_doc; args_doc] - else if - (not args_have_trailing_comments) - && Parsetree_viewer.requires_special_callback_printing_last_arg args - then - let args_doc = - print_arguments_with_callback_in_last_position ~state ~partial args - cmt_tbl - in - (* - * Fixes the following layout (the `[` and `]` should break): - * [fn(x => { - * let _ = x - * }), fn(y => { - * let _ = y - * }), fn(z => { - * let _ = z - * })] - * See `Doc.willBreak documentation in interface file for more context. - * Context: - * https://github.com/rescript-lang/syntax/issues/111 - * https://github.com/rescript-lang/syntax/issues/166 - *) - let maybe_break_parent = - if Doc.will_break args_doc then Doc.break_parent else Doc.nil - in - Doc.concat - [ - maybe_break_parent; - print_attributes ~state attrs cmt_tbl; - call_expr_doc; - args_doc; - ] - else - let args_doc = print_arguments ~state ~partial args cmt_tbl in - Doc.concat - [print_attributes ~state attrs cmt_tbl; call_expr_doc; args_doc] + let args_doc, maybe_break_parent = + if + (not args_have_trailing_comments) + && Parsetree_viewer.requires_special_callback_printing_first_arg args + then + ( print_arguments_with_callback_in_first_position ~state ~partial args + cmt_tbl, + Doc.nil ) + else if + (not args_have_trailing_comments) + && Parsetree_viewer.requires_special_callback_printing_last_arg args + then + let args_doc = + print_arguments_with_callback_in_last_position ~state ~partial args + cmt_tbl + in + (* Propagate breaks from the callback layout to enclosing groups, such + * as an array containing multiline calls. See Doc.will_break and + * https://github.com/rescript-lang/syntax/issues/111. *) + let maybe_break_parent = + if Doc.will_break args_doc then Doc.break_parent else Doc.nil + in + (args_doc, maybe_break_parent) + else (print_arguments ~state ~partial args cmt_tbl, Doc.nil) + in + Doc.concat + [ + maybe_break_parent; + print_attributes ~state attrs cmt_tbl; + call_expr_doc; + args_doc; + ] | _ -> assert false and print_jsx_unary_tag ~state tag_name props expr_loc cmt_tbl = @@ -5053,24 +5039,23 @@ and print_jsx_name (tag_name : Parsetree.jsx_tag_name) = let printed = segs |> List.map (print_ident_like ~allow_uident:true) in Doc.join ~sep:Doc.dot printed +and print_callback_label = function + | Asttypes.Nolabel -> Doc.nil + | Asttypes.Labelled {txt} -> + Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal] + | Asttypes.Optional {txt} -> + Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question] + and print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl = - (* Because the same subtree gets printed twice, we need to copy the cmt_tbl. - * consumed comments need to be marked not-consumed and reprinted… - * Cheng's different comment algorithm will solve this. *) + (* Printing consumes comments from the table. Each alternative layout needs + * its own copy so it can print the same subtree with all its comments. *) let state = State.next_custom_layout state in let cmt_tbl_copy = Comment_table.copy cmt_tbl in let callback, printed_args = match args with | (lbl, expr) :: args -> - let lbl_doc = - match lbl with - | Asttypes.Nolabel -> Doc.nil - | Asttypes.Labelled {txt} -> - Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal] - | Asttypes.Optional {txt} -> - Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question] - in + let lbl_doc = print_callback_label lbl in let callback = Doc.concat [ @@ -5141,9 +5126,8 @@ and print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl = - (* Because the same subtree gets printed twice, we need to copy the cmt_tbl. - * consumed comments need to be marked not-consumed and reprinted… - * Cheng's different comment algorithm will solve this. *) + (* Printing consumes comments from the table. Each alternative layout needs + * its own copy so it can print the same subtree with all its comments. *) let state = state |> State.next_custom_layout in let cmt_tbl_copy = Comment_table.copy cmt_tbl in let cmt_tbl_copy2 = Comment_table.copy cmt_tbl in @@ -5151,14 +5135,7 @@ and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl match args with | [] -> (lazy Doc.nil, lazy Doc.nil, lazy Doc.nil) | [(lbl, expr)] -> - let lbl_doc = - match lbl with - | Asttypes.Nolabel -> Doc.nil - | Asttypes.Labelled {txt} -> - Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal] - | Asttypes.Optional {txt} -> - Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question] - in + let lbl_doc = print_callback_label lbl in let callback_fits_on_one_line = lazy (let pexp_fun_doc = @@ -5196,7 +5173,7 @@ and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl * MyModuleBlah.toList(argument) * ) *) - let arugments_fit_on_one_line = + let arguments_fit_on_one_line = lazy (Doc.concat [ @@ -5240,7 +5217,7 @@ and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl Doc.custom_layout [ Lazy.force fits_on_one_line; - Lazy.force arugments_fit_on_one_line; + Lazy.force arguments_fit_on_one_line; Lazy.force break_all_args; ] diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index cc39993753..4d3207cd1c 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -45,7 +45,16 @@ let () = List.iter (fun width -> let printed = format ~width source in - assert (printed = format ~width printed)) + let reprinted = format ~width printed in + if printed <> reprinted then + failwith + (Printf.sprintf + "Callback comment formatting is unstable at width %d.\n\ + First pass:\n\ + %s\n\ + Second pass:\n\ + %s" + width printed reprinted)) [20; 40; 80; 100; 120]; print_endline "✅ callback trailing comments are stable at multiple widths" From 006bd2004bc487458ab21a601ca463b56159e255 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 6 Sep 2026 07:55:03 +0200 Subject: [PATCH 4/6] Preserve trailing comments on labeled callbacks Signed-off-by: Christoph Knittel --- compiler/syntax/src/res_printer.ml | 12 ++++++++++-- .../data/printer/comments/callbackTrailing.res | 11 +++++++++++ .../comments/expected/callbackTrailing.res.txt | 11 +++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index a2600f4bbf..0594c361f6 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -4667,8 +4667,16 @@ and print_pexp_apply ~state expr cmt_tbl = * subsequent formatting unstable. *) let args_have_trailing_comments = List.exists - (fun (_, (arg : Parsetree.expression)) -> - has_trailing_comments cmt_tbl arg.pexp_loc) + (fun (lbl, (arg : Parsetree.expression)) -> + (* Match the full argument location used by comment attachment. *) + let loc = + match lbl with + | Asttypes.Labelled {loc} | Optional {loc} -> + {loc with loc_end = arg.pexp_loc.loc_end} + | Nolabel -> arg.pexp_loc + in + has_trailing_comments cmt_tbl loc + || has_trailing_comments cmt_tbl arg.pexp_loc) args in let args_doc, maybe_break_parent = diff --git a/tests/syntax_tests/data/printer/comments/callbackTrailing.res b/tests/syntax_tests/data/printer/comments/callbackTrailing.res index 53ac38e8bf..074e6fffbc 100644 --- a/tests/syntax_tests/data/printer/comments/callbackTrailing.res +++ b/tests/syntax_tests/data/printer/comments/callbackTrailing.res @@ -32,3 +32,14 @@ let nested = call(value, x => call(value, y => y )) let blocks = call(value, x => x /* inline block */) + +let labeled = call(~fn=x => x /* keep labeled */) +let optional = call(~fn=?x => x /* keep optional */) +let labeledFirst = call(~fn=x => x /* keep first */, value) +let optionalLast = call(value, ~fn=?x => x /* keep last */) +let labeledBelow = call(~fn=x => x + /* keep below */ +) +let optionalLine = call(~fn=?x => x // keep line +) +let labeledNested = call(~fn=x => call(~fn=y => y /* keep nested */)) diff --git a/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt index 12a9e9cc03..36d3b99290 100644 --- a/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt +++ b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt @@ -39,3 +39,14 @@ let nested = call(value, x => ) let blocks = call(value, x => x /* inline block */) + +let labeled = call(~fn=x => x /* keep labeled */) +let optional = call(~fn=?x => x /* keep optional */) +let labeledFirst = call(~fn=x => x /* keep first */, value) +let optionalLast = call(value, ~fn=?x => x /* keep last */) +let labeledBelow = call( + ~fn=x => x, + /* keep below */ +) +let optionalLine = call(~fn=?x => x) // keep line +let labeledNested = call(~fn=x => call(~fn=y => y /* keep nested */)) From 3109e1ba56e85e893e3b94760d676a5c44b7c1a7 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 6 Sep 2026 11:16:31 +0200 Subject: [PATCH 5/6] Preserve leading callback comments and contain trailing line comments Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- compiler/syntax/src/res_printer.ml | 45 +++++++++++++------ .../printer/comments/callbackTrailing.res | 14 ++++++ .../expected/callbackTrailing.res.txt | 37 +++++++++++++-- tests/syntax_tests/res_test.ml | 24 +++++++++- 5 files changed, 103 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0571ff1100..c711582bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/pull/8630 - Fix the side-effect analysis treating bigint exponentiation and bounds-checked array and string reads as pure, which let dead-code elimination drop an unused one that throws: `let _ = 2n ** -1n` no longer raised. https://github.com/rescript-lang/rescript/pull/8617 - Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619 -- Fix unstable formatting of trailing comments in nested callbacks. https://github.com/rescript-lang/rescript/pull/8627 +- Fix lost leading comments on labeled callbacks and unstable formatting of trailing callback comments. https://github.com/rescript-lang/rescript/pull/8627 - Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611 - Report an error instead of crashing when an integer in a variant constructor's `@as` annotation exceeds the compiler's integer range. https://github.com/rescript-lang/rescript/pull/8619 diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 0594c361f6..c165910bae 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -93,6 +93,13 @@ let has_trailing_comments tbl loc = | None -> false | _ -> true +(* Match the full argument location used by comment attachment. *) +let argument_loc (lbl, (arg : Parsetree.expression)) = + match lbl with + | Asttypes.Labelled {loc} | Optional {loc} -> + {loc with loc_end = arg.pexp_loc.loc_end} + | Nolabel -> arg.pexp_loc + let has_leading_comments tbl loc = match Hashtbl.find_opt tbl.Comment_table.leading loc with | None -> false @@ -4662,33 +4669,35 @@ and print_pexp_apply ~state expr cmt_tbl = | Braced braces -> print_braces doc call_expr braces | Nothing -> doc in - (* Use the regular argument layout for trailing comments. Compact callback - * layouts can detach comments from the body when it breaks, making - * subsequent formatting unstable. *) - let args_have_trailing_comments = + (* Use the regular layout for comments attached to arguments. Compact + * callback layouts can detach trailing comments when the body breaks and + * skip leading comments attached to the full labeled argument. *) + let args_have_comments = List.exists (fun (lbl, (arg : Parsetree.expression)) -> - (* Match the full argument location used by comment attachment. *) - let loc = + let loc = argument_loc (lbl, arg) in + let has_leading_label_comments = match lbl with - | Asttypes.Labelled {loc} | Optional {loc} -> - {loc with loc_end = arg.pexp_loc.loc_end} - | Nolabel -> arg.pexp_loc + | Asttypes.Nolabel -> false + | Labelled _ | Optional _ -> + has_leading_comments cmt_tbl loc + || has_leading_comments cmt_tbl arg.pexp_loc in - has_trailing_comments cmt_tbl loc + has_leading_label_comments + || has_trailing_comments cmt_tbl loc || has_trailing_comments cmt_tbl arg.pexp_loc) args in let args_doc, maybe_break_parent = if - (not args_have_trailing_comments) + (not args_have_comments) && Parsetree_viewer.requires_special_callback_printing_first_arg args then ( print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl, Doc.nil ) else if - (not args_have_trailing_comments) + (not args_have_comments) && Parsetree_viewer.requires_special_callback_printing_last_arg args then let args_doc = @@ -5258,11 +5267,21 @@ and print_arguments ~state ~partial in Doc.concat [Doc.lparen; arg_doc; Doc.rparen] | args -> + (* Flush a callback's line comment before closing the argument list, so + * reparsing cannot attach it to an enclosing call instead. *) + let force_break = + List.exists + (fun ((_, arg) as argument) -> + Parsetree_viewer.is_fun_expr arg + && (has_any_trailing_line_comment cmt_tbl (argument_loc argument) + || has_any_trailing_line_comment cmt_tbl arg.pexp_loc)) + args + in (* Avoid printing trailing comma when there is ... in function application *) let printed_args = List.map (fun arg -> print_argument ~state arg cmt_tbl) args in - Doc.group + Doc.breakable_group ~force_break (Doc.concat [ Doc.lparen; diff --git a/tests/syntax_tests/data/printer/comments/callbackTrailing.res b/tests/syntax_tests/data/printer/comments/callbackTrailing.res index 074e6fffbc..f7bf699d41 100644 --- a/tests/syntax_tests/data/printer/comments/callbackTrailing.res +++ b/tests/syntax_tests/data/printer/comments/callbackTrailing.res @@ -43,3 +43,17 @@ let labeledBelow = call(~fn=x => x let optionalLine = call(~fn=?x => x // keep line ) let labeledNested = call(~fn=x => call(~fn=y => y /* keep nested */)) + +let leadingLabeled = call(/* keep leading label */ ~fn=x => x) +let leadingOptional = call(/* keep leading optional */ ~fn=?x => x) +let leadingFirst = call(/* keep leading first */ ~fn=x => x, value) +let leadingLast = call(value, /* keep leading last */ ~fn=?x => x) +let afterLabel = call(~fn=/* keep after label */ x => x) +let leadingNested = call(~fn=x => call(/* keep leading nested */ ~fn=y => y)) + +outer(x => call(x => x // keep nested line +)) +outer(x => call(~fn=x => x // keep nested labeled line +)) +outer(x => call(~fn=?x => x // keep nested optional line +)) diff --git a/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt index 36d3b99290..852da869a7 100644 --- a/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt +++ b/tests/syntax_tests/data/printer/comments/expected/callbackTrailing.res.txt @@ -26,9 +26,15 @@ let last = call( // last callback ) -let inline = call(value, x => x) // inline callback +let inline = call( + value, + x => x, // inline callback +) -let firstInline = call(x => x, value) // first inline callback +let firstInline = call( + x => x, // first inline callback + value, +) let nested = call(value, x => call( @@ -48,5 +54,30 @@ let labeledBelow = call( ~fn=x => x, /* keep below */ ) -let optionalLine = call(~fn=?x => x) // keep line +let optionalLine = call( + ~fn=?x => x, // keep line +) let labeledNested = call(~fn=x => call(~fn=y => y /* keep nested */)) + +let leadingLabeled = call(/* keep leading label */ ~fn=x => x) +let leadingOptional = call(/* keep leading optional */ ~fn=?x => x) +let leadingFirst = call(/* keep leading first */ ~fn=x => x, value) +let leadingLast = call(value, /* keep leading last */ ~fn=?x => x) +let afterLabel = call(~fn=/* keep after label */ x => x) +let leadingNested = call(~fn=x => call(/* keep leading nested */ ~fn=y => y)) + +outer(x => + call( + x => x, // keep nested line + ) +) +outer(x => + call( + ~fn=x => x, // keep nested labeled line + ) +) +outer(x => + call( + ~fn=?x => x, // keep nested optional line + ) +) diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index 4d3207cd1c..ba9dbb313d 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -39,8 +39,28 @@ let () = ~display_filename:filename ~source in assert (not result.invalid); - Res_printer.print_implementation ~width result.parsetree - ~comments:result.comments + let printed = + Res_printer.print_implementation ~width result.parsetree + ~comments:result.comments + in + let reparsed = + Res_driver.parse_implementation_from_source ~for_printer:true + ~display_filename:filename ~source:printed + in + assert (not reparsed.invalid); + let comment_texts comments = + List.map (fun comment -> String.trim (Res_comment.txt comment)) comments + in + if comment_texts result.comments <> comment_texts reparsed.comments then + failwith + (Printf.sprintf + "Callback formatting changed comments at width %d.\n\ + Source:\n\ + %s\n\ + Printed:\n\ + %s" + width source printed); + printed in List.iter (fun width -> From 8179a597957ebbc51e95da9ecaebf263826ddb4d Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 6 Sep 2026 16:06:42 +0200 Subject: [PATCH 6/6] Share argument locations and clarify callback layout rules Signed-off-by: Christoph Knittel --- compiler/syntax/src/res_comments_table.ml | 9 +--- compiler/syntax/src/res_parsetree_viewer.ml | 6 +++ compiler/syntax/src/res_parsetree_viewer.mli | 4 ++ compiler/syntax/src/res_printer.ml | 52 +++++++++---------- tests/syntax_tests/res_test.ml | 53 ++++++++++---------- 5 files changed, 62 insertions(+), 62 deletions(-) diff --git a/compiler/syntax/src/res_comments_table.ml b/compiler/syntax/src/res_comments_table.ml index bb2afa3a94..f9181092f6 100644 --- a/compiler/syntax/src/res_comments_table.ml +++ b/compiler/syntax/src/res_comments_table.ml @@ -986,13 +986,8 @@ and walk_expression expr t comments = attach t.trailing call_expr.Parsetree.pexp_loc after_expr; walk_list (arguments - |> List.map (fun (lbl, expr) -> - let loc = - match lbl with - | Asttypes.Labelled {loc} | Optional {loc} -> - {loc with loc_end = expr.Parsetree.pexp_loc.loc_end} - | _ -> expr.pexp_loc - in + |> List.map (fun ((_, expr) as argument) -> + let loc = Parsetree_viewer.argument_loc argument in ExprArgument {expr; loc})) t rest in diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index 3d462f2517..cef8640566 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -1,5 +1,11 @@ open Parsetree +let argument_loc (lbl, (arg : Parsetree.expression)) = + match lbl with + | Asttypes.Labelled {loc} | Optional {loc} -> + {loc with loc_end = arg.pexp_loc.loc_end} + | Nolabel -> arg.pexp_loc + let arrow_type ct = match ct with | {ptyp_desc = Ptyp_arrow {params; ret}; ptyp_attributes = attrs} -> diff --git a/compiler/syntax/src/res_parsetree_viewer.mli b/compiler/syntax/src/res_parsetree_viewer.mli index 8f378239f3..817d06aaf9 100644 --- a/compiler/syntax/src/res_parsetree_viewer.mli +++ b/compiler/syntax/src/res_parsetree_viewer.mli @@ -1,3 +1,7 @@ +(* Full argument span used for comment attachment and printing. For labeled + * and optional arguments it starts at the label and ends at the expression. *) +val argument_loc : Asttypes.arg_label * Parsetree.expression -> Location.t + (* Restructures a nested tree of arrow types into its args & returnType * The parsetree contains: a => b => c => d, for printing purposes * we restructure the tree into (a, b, c) and its returnType d *) diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index c7d6260419..892e22b574 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -93,18 +93,29 @@ let has_trailing_comments tbl loc = | None -> false | _ -> true -(* Match the full argument location used by comment attachment. *) -let argument_loc (lbl, (arg : Parsetree.expression)) = - match lbl with - | Asttypes.Labelled {loc} | Optional {loc} -> - {loc with loc_end = arg.pexp_loc.loc_end} - | Nolabel -> arg.pexp_loc - let has_leading_comments tbl loc = match Hashtbl.find_opt tbl.Comment_table.leading loc with | None -> false | _ -> true +(* Compact layouts print expression comments but not comments on the full + * labeled argument. For unlabeled arguments, leading expression comments + * already work in the compact layout; forcing a different layout can make + * comments exposed by removing parameter parentheses unstable. *) +let argument_requires_regular_layout cmt_tbl ((lbl, arg) as argument) = + let loc = Parsetree_viewer.argument_loc argument in + let has_leading_label_comments = + match lbl with + | Asttypes.Nolabel -> false + | Labelled _ | Optional _ -> + has_leading_comments cmt_tbl loc + || has_leading_comments cmt_tbl arg.Parsetree.pexp_loc + in + (* Trailing comments can escape compact layouts when the body breaks. *) + has_leading_label_comments + || has_trailing_comments cmt_tbl loc + || has_trailing_comments cmt_tbl arg.pexp_loc + let print_multiline_comment_content txt = (* Turns * |* first line @@ -4533,35 +4544,19 @@ and print_pexp_apply ~state expr cmt_tbl = | Braced braces -> print_braces doc call_expr braces | Nothing -> doc in - (* Use the regular layout for comments attached to arguments. Compact - * callback layouts can detach trailing comments when the body breaks and - * skip leading comments attached to the full labeled argument. *) - let args_have_comments = - List.exists - (fun (lbl, (arg : Parsetree.expression)) -> - let loc = argument_loc (lbl, arg) in - let has_leading_label_comments = - match lbl with - | Asttypes.Nolabel -> false - | Labelled _ | Optional _ -> - has_leading_comments cmt_tbl loc - || has_leading_comments cmt_tbl arg.pexp_loc - in - has_leading_label_comments - || has_trailing_comments cmt_tbl loc - || has_trailing_comments cmt_tbl arg.pexp_loc) - args + let requires_regular_layout = + List.exists (argument_requires_regular_layout cmt_tbl) args in let args_doc, maybe_break_parent = if - (not args_have_comments) + (not requires_regular_layout) && Parsetree_viewer.requires_special_callback_printing_first_arg args then ( print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl, Doc.nil ) else if - (not args_have_comments) + (not requires_regular_layout) && Parsetree_viewer.requires_special_callback_printing_last_arg args then let args_doc = @@ -5137,7 +5132,8 @@ and print_arguments ~state ~partial List.exists (fun ((_, arg) as argument) -> Parsetree_viewer.is_fun_expr arg - && (has_any_trailing_line_comment cmt_tbl (argument_loc argument) + && (has_any_trailing_line_comment cmt_tbl + (Parsetree_viewer.argument_loc argument) || has_any_trailing_line_comment cmt_tbl arg.pexp_loc)) args in diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index dfd07d7618..802f093602 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -33,39 +33,38 @@ let () = Filename.concat data_dir "printer/comments/callbackTrailing.res" in let source = IO.read_file ~filename in - let format ~width source = + let parse source = let result = - Res_driver.parse_implementation_from_source ~for_printer:true - ~display_filename:filename ~source + Res_driver.parse_implementation_from_source ~display_filename:filename + ~source in assert (not result.invalid); - let printed = - Res_printer.print_implementation ~width result.parsetree - ~comments:result.comments - in - let reparsed = - Res_driver.parse_implementation_from_source ~for_printer:true - ~display_filename:filename ~source:printed - in - assert (not reparsed.invalid); - let comment_texts comments = - List.map (fun comment -> String.trim (Res_comment.txt comment)) comments - in - if comment_texts result.comments <> comment_texts reparsed.comments then - failwith - (Printf.sprintf - "Callback formatting changed comments at width %d.\n\ - Source:\n\ - %s\n\ - Printed:\n\ - %s" - width source printed); - printed + result + in + let format ~width result = + Res_printer.print_implementation ~width result.Res_driver.parsetree + ~comments:result.comments + in + let comment_texts result = + List.map + (fun comment -> String.trim (Res_comment.txt comment)) + result.Res_driver.comments in List.iter (fun width -> - let printed = format ~width source in - let reprinted = format ~width printed in + let original = parse source in + let printed = format ~width original in + let reparsed = parse printed in + if comment_texts original <> comment_texts reparsed then + failwith + (Printf.sprintf + "Callback formatting changed comments at width %d.\n\ + Source:\n\ + %s\n\ + Printed:\n\ + %s" + width source printed); + let reprinted = format ~width reparsed in if printed <> reprinted then failwith (Printf.sprintf