Skip to content

Commit 2e532c7

Browse files
authored
Fix lost and unstable comments around callbacks (#8627)
* Fix unstable formatting of trailing callback comments Signed-off-by: Christoph Knittel <ck@cca.io> * Link callback formatting changelog entry to PR Signed-off-by: Christoph Knittel <ck@cca.io> * Clean up callback formatter layout selection and diagnostics Signed-off-by: Christoph Knittel <ck@cca.io> * Preserve trailing comments on labeled callbacks Signed-off-by: Christoph Knittel <ck@cca.io> * Preserve leading callback comments and contain trailing line comments Signed-off-by: Christoph Knittel <ck@cca.io> * Share argument locations and clarify callback layout rules Signed-off-by: Christoph Knittel <ck@cca.io> --------- Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent 0b4bd0a commit 2e532c7

8 files changed

Lines changed: 284 additions & 73 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
- 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
4141
- 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
4242
- Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619
43+
- Fix lost leading comments on labeled callbacks and unstable formatting of trailing callback comments. https://github.com/rescript-lang/rescript/pull/8627
4344
- Fix record-field completion inside constructor tuple payloads and for their destructured bindings, including both supported tuple spellings and polymorphic variants. https://github.com/rescript-lang/rescript/pull/8610
4445
- Limit constructor signature help to the argument parentheses, excluding whitespace and comments between the constructor name and its arguments, and keep unary tuple payloads on parameter zero. https://github.com/rescript-lang/rescript/pull/8610
4546
- 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

compiler/syntax/src/res_comments_table.ml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -986,13 +986,8 @@ and walk_expression expr t comments =
986986
attach t.trailing call_expr.Parsetree.pexp_loc after_expr;
987987
walk_list
988988
(arguments
989-
|> List.map (fun (lbl, expr) ->
990-
let loc =
991-
match lbl with
992-
| Asttypes.Labelled {loc} | Optional {loc} ->
993-
{loc with loc_end = expr.Parsetree.pexp_loc.loc_end}
994-
| _ -> expr.pexp_loc
995-
in
989+
|> List.map (fun ((_, expr) as argument) ->
990+
let loc = Parsetree_viewer.argument_loc argument in
996991
ExprArgument {expr; loc}))
997992
t rest
998993
in

compiler/syntax/src/res_parsetree_viewer.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
open Parsetree
22

3+
let argument_loc (lbl, (arg : Parsetree.expression)) =
4+
match lbl with
5+
| Asttypes.Labelled {loc} | Optional {loc} ->
6+
{loc with loc_end = arg.pexp_loc.loc_end}
7+
| Nolabel -> arg.pexp_loc
8+
39
let arrow_type ct =
410
match ct with
511
| {ptyp_desc = Ptyp_arrow {params; ret}; ptyp_attributes = attrs} ->

compiler/syntax/src/res_parsetree_viewer.mli

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
(* Full argument span used for comment attachment and printing. For labeled
2+
* and optional arguments it starts at the label and ends at the expression. *)
3+
val argument_loc : Asttypes.arg_label * Parsetree.expression -> Location.t
4+
15
(* Restructures a nested tree of arrow types into its args & returnType
26
* The parsetree contains: a => b => c => d, for printing purposes
37
* we restructure the tree into (a, b, c) and its returnType d *)

compiler/syntax/src/res_printer.ml

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ let has_leading_comments tbl loc =
9898
| None -> false
9999
| _ -> true
100100

101+
(* Compact layouts print expression comments but not comments on the full
102+
* labeled argument. For unlabeled arguments, leading expression comments
103+
* already work in the compact layout; forcing a different layout can make
104+
* comments exposed by removing parameter parentheses unstable. *)
105+
let argument_requires_regular_layout cmt_tbl ((lbl, arg) as argument) =
106+
let loc = Parsetree_viewer.argument_loc argument in
107+
let has_leading_label_comments =
108+
match lbl with
109+
| Asttypes.Nolabel -> false
110+
| Labelled _ | Optional _ ->
111+
has_leading_comments cmt_tbl loc
112+
|| has_leading_comments cmt_tbl arg.Parsetree.pexp_loc
113+
in
114+
(* Trailing comments can escape compact layouts when the body breaks. *)
115+
has_leading_label_comments
116+
|| has_trailing_comments cmt_tbl loc
117+
|| has_trailing_comments cmt_tbl arg.pexp_loc
118+
101119
let print_multiline_comment_content txt =
102120
(* Turns
103121
* |* first line
@@ -4526,47 +4544,41 @@ and print_pexp_apply ~state expr cmt_tbl =
45264544
| Braced braces -> print_braces doc call_expr braces
45274545
| Nothing -> doc
45284546
in
4529-
if Parsetree_viewer.requires_special_callback_printing_first_arg args then
4530-
let args_doc =
4531-
print_arguments_with_callback_in_first_position ~state ~partial args
4532-
cmt_tbl
4533-
in
4534-
Doc.concat
4535-
[print_attributes ~state attrs cmt_tbl; call_expr_doc; args_doc]
4536-
else if Parsetree_viewer.requires_special_callback_printing_last_arg args
4537-
then
4538-
let args_doc =
4539-
print_arguments_with_callback_in_last_position ~state ~partial args
4540-
cmt_tbl
4541-
in
4542-
(*
4543-
* Fixes the following layout (the `[` and `]` should break):
4544-
* [fn(x => {
4545-
* let _ = x
4546-
* }), fn(y => {
4547-
* let _ = y
4548-
* }), fn(z => {
4549-
* let _ = z
4550-
* })]
4551-
* See `Doc.willBreak documentation in interface file for more context.
4552-
* Context:
4553-
* https://github.com/rescript-lang/syntax/issues/111
4554-
* https://github.com/rescript-lang/syntax/issues/166
4555-
*)
4556-
let maybe_break_parent =
4557-
if Doc.will_break args_doc then Doc.break_parent else Doc.nil
4558-
in
4559-
Doc.concat
4560-
[
4561-
maybe_break_parent;
4562-
print_attributes ~state attrs cmt_tbl;
4563-
call_expr_doc;
4564-
args_doc;
4565-
]
4566-
else
4567-
let args_doc = print_arguments ~state ~partial args cmt_tbl in
4568-
Doc.concat
4569-
[print_attributes ~state attrs cmt_tbl; call_expr_doc; args_doc]
4547+
let requires_regular_layout =
4548+
List.exists (argument_requires_regular_layout cmt_tbl) args
4549+
in
4550+
let args_doc, maybe_break_parent =
4551+
if
4552+
(not requires_regular_layout)
4553+
&& Parsetree_viewer.requires_special_callback_printing_first_arg args
4554+
then
4555+
( print_arguments_with_callback_in_first_position ~state ~partial args
4556+
cmt_tbl,
4557+
Doc.nil )
4558+
else if
4559+
(not requires_regular_layout)
4560+
&& Parsetree_viewer.requires_special_callback_printing_last_arg args
4561+
then
4562+
let args_doc =
4563+
print_arguments_with_callback_in_last_position ~state ~partial args
4564+
cmt_tbl
4565+
in
4566+
(* Propagate breaks from the callback layout to enclosing groups, such
4567+
* as an array containing multiline calls. See Doc.will_break and
4568+
* https://github.com/rescript-lang/syntax/issues/111. *)
4569+
let maybe_break_parent =
4570+
if Doc.will_break args_doc then Doc.break_parent else Doc.nil
4571+
in
4572+
(args_doc, maybe_break_parent)
4573+
else (print_arguments ~state ~partial args cmt_tbl, Doc.nil)
4574+
in
4575+
Doc.concat
4576+
[
4577+
maybe_break_parent;
4578+
print_attributes ~state attrs cmt_tbl;
4579+
call_expr_doc;
4580+
args_doc;
4581+
]
45704582
| _ -> assert false
45714583

45724584
and print_jsx_unary_tag ~state tag_name props expr_loc cmt_tbl =
@@ -4903,24 +4915,23 @@ and print_jsx_name (tag_name : Parsetree.jsx_tag_name) =
49034915
let printed = segs |> List.map (print_ident_like ~allow_uident:true) in
49044916
Doc.join ~sep:Doc.dot printed
49054917

4918+
and print_callback_label = function
4919+
| Asttypes.Nolabel -> Doc.nil
4920+
| Asttypes.Labelled {txt} ->
4921+
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal]
4922+
| Asttypes.Optional {txt} ->
4923+
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question]
4924+
49064925
and print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl
49074926
=
4908-
(* Because the same subtree gets printed twice, we need to copy the cmt_tbl.
4909-
* consumed comments need to be marked not-consumed and reprinted…
4910-
* Cheng's different comment algorithm will solve this. *)
4927+
(* Printing consumes comments from the table. Each alternative layout needs
4928+
* its own copy so it can print the same subtree with all its comments. *)
49114929
let state = State.next_custom_layout state in
49124930
let cmt_tbl_copy = Comment_table.copy cmt_tbl in
49134931
let callback, printed_args =
49144932
match args with
49154933
| (lbl, expr) :: args ->
4916-
let lbl_doc =
4917-
match lbl with
4918-
| Asttypes.Nolabel -> Doc.nil
4919-
| Asttypes.Labelled {txt} ->
4920-
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal]
4921-
| Asttypes.Optional {txt} ->
4922-
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question]
4923-
in
4934+
let lbl_doc = print_callback_label lbl in
49244935
let callback =
49254936
Doc.concat
49264937
[
@@ -4991,24 +5002,16 @@ and print_arguments_with_callback_in_first_position ~state ~partial args cmt_tbl
49915002

49925003
and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl
49935004
=
4994-
(* Because the same subtree gets printed twice, we need to copy the cmt_tbl.
4995-
* consumed comments need to be marked not-consumed and reprinted…
4996-
* Cheng's different comment algorithm will solve this. *)
5005+
(* Printing consumes comments from the table. Each alternative layout needs
5006+
* its own copy so it can print the same subtree with all its comments. *)
49975007
let state = state |> State.next_custom_layout in
49985008
let cmt_tbl_copy = Comment_table.copy cmt_tbl in
49995009
let cmt_tbl_copy2 = Comment_table.copy cmt_tbl in
50005010
let rec loop acc args =
50015011
match args with
50025012
| [] -> (lazy Doc.nil, lazy Doc.nil, lazy Doc.nil)
50035013
| [(lbl, expr)] ->
5004-
let lbl_doc =
5005-
match lbl with
5006-
| Asttypes.Nolabel -> Doc.nil
5007-
| Asttypes.Labelled {txt} ->
5008-
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal]
5009-
| Asttypes.Optional {txt} ->
5010-
Doc.concat [Doc.tilde; print_ident_like txt; Doc.equal; Doc.question]
5011-
in
5014+
let lbl_doc = print_callback_label lbl in
50125015
let callback_fits_on_one_line =
50135016
lazy
50145017
(let pexp_fun_doc =
@@ -5046,7 +5049,7 @@ and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl
50465049
* MyModuleBlah.toList(argument)
50475050
* )
50485051
*)
5049-
let arugments_fit_on_one_line =
5052+
let arguments_fit_on_one_line =
50505053
lazy
50515054
(Doc.concat
50525055
[
@@ -5090,7 +5093,7 @@ and print_arguments_with_callback_in_last_position ~state ~partial args cmt_tbl
50905093
Doc.custom_layout
50915094
[
50925095
Lazy.force fits_on_one_line;
5093-
Lazy.force arugments_fit_on_one_line;
5096+
Lazy.force arguments_fit_on_one_line;
50945097
Lazy.force break_all_args;
50955098
]
50965099

@@ -5123,11 +5126,22 @@ and print_arguments ~state ~partial
51235126
in
51245127
Doc.concat [Doc.lparen; arg_doc; Doc.rparen]
51255128
| args ->
5129+
(* Flush a callback's line comment before closing the argument list, so
5130+
* reparsing cannot attach it to an enclosing call instead. *)
5131+
let force_break =
5132+
List.exists
5133+
(fun ((_, arg) as argument) ->
5134+
Parsetree_viewer.is_fun_expr arg
5135+
&& (has_any_trailing_line_comment cmt_tbl
5136+
(Parsetree_viewer.argument_loc argument)
5137+
|| has_any_trailing_line_comment cmt_tbl arg.pexp_loc))
5138+
args
5139+
in
51265140
(* Avoid printing trailing comma when there is ... in function application *)
51275141
let printed_args =
51285142
List.map (fun arg -> print_argument ~state arg cmt_tbl) args
51295143
in
5130-
Doc.group
5144+
Doc.breakable_group ~force_break
51315145
(Doc.concat
51325146
[
51335147
Doc.lparen;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
let rec fib = (n, k) =>
2+
switch n {
3+
| 0 | 1 => k(1)
4+
| _ =>
5+
Suspend(
6+
() =>
7+
fib(n - 1, (v0) =>
8+
fib(n - 2, (v1) =>
9+
k(v0 + v1)
10+
/* comment */
11+
)
12+
),
13+
)
14+
}
15+
16+
let first = call(x => x
17+
/* first callback */
18+
, value)
19+
20+
let last = call(value, x => x
21+
// last callback
22+
)
23+
24+
let inline = call(value, x => x // inline callback
25+
)
26+
27+
let firstInline = call(x => x // first inline callback
28+
, value)
29+
30+
let nested = call(value, x => call(value, y => y
31+
/* nested callback */
32+
))
33+
34+
let blocks = call(value, x => x /* inline block */)
35+
36+
let labeled = call(~fn=x => x /* keep labeled */)
37+
let optional = call(~fn=?x => x /* keep optional */)
38+
let labeledFirst = call(~fn=x => x /* keep first */, value)
39+
let optionalLast = call(value, ~fn=?x => x /* keep last */)
40+
let labeledBelow = call(~fn=x => x
41+
/* keep below */
42+
)
43+
let optionalLine = call(~fn=?x => x // keep line
44+
)
45+
let labeledNested = call(~fn=x => call(~fn=y => y /* keep nested */))
46+
47+
let leadingLabeled = call(/* keep leading label */ ~fn=x => x)
48+
let leadingOptional = call(/* keep leading optional */ ~fn=?x => x)
49+
let leadingFirst = call(/* keep leading first */ ~fn=x => x, value)
50+
let leadingLast = call(value, /* keep leading last */ ~fn=?x => x)
51+
let afterLabel = call(~fn=/* keep after label */ x => x)
52+
let leadingNested = call(~fn=x => call(/* keep leading nested */ ~fn=y => y))
53+
54+
outer(x => call(x => x // keep nested line
55+
))
56+
outer(x => call(~fn=x => x // keep nested labeled line
57+
))
58+
outer(x => call(~fn=?x => x // keep nested optional line
59+
))

0 commit comments

Comments
 (0)