Skip to content

Commit e73be3d

Browse files
committed
Format a blank line around module items that span several lines
Why: Dense code, often written by AI agents, runs multi-line modules and functions together, which makes a file hard to scan (#8960). The formatter already collapses several blank lines into one; it now also separates an item that prints on more than one line from its neighbours in structures and signatures. Blocks inside expressions keep their layout. Format modified (structure items): Example: module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed Before (rescript): module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed After (rescript): module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed Format unchanged (one-line items with comments): Example: let a = 1 // about b let b = 2 // trailing let c = 3 After (rescript): let a = 1 // about b let b = 2 // trailing let c = 3 Review: Is "multi-line" decided on the source or on the output? On the output: the new Res_doc.item_separator measures the items on each side with the printer's own layout decisions, stopping at the first line break, so an item that only breaks at the line width counts and formatting twice gives the same text. All 1025 printer and idempotency fixtures are stable across two passes. Review: Why a Res_doc.comment marker? Leading comments print inside the item's document (a value binding carries them), so a `//` line above a one-line item would make it multi-line. The marker keeps comments out of the measurement; doc comments are attributes and still count. Review: Why move own-line trailing comments below the blank line? A comment inside the previous item's source, such as `f(a // c => 1)`, prints on its own line after the item. Formatting again makes it a leading comment of the next item, after the blank line, so the first pass puts it there too. Review: Does this reformat existing code? Yes: 346 of the 1870 .res/.resi files `make format` covers gain blank lines. They are not reformatted in this commit, so `make checkformat` reports them. Signed-off-by: Roland Peelen <roland@magic.dev>
1 parent c0fd17f commit e73be3d

117 files changed

Lines changed: 776 additions & 25 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Format a blank line before and after a top-level or module-level item that spans several lines.

‎compiler/syntax/src/res_doc.ml‎

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type t =
2323
| Group of {mutable should_break: bool; doc: t}
2424
| CustomLayout of t list
2525
| BreakParent
26+
| ItemSeparator of {prev: t; next: t} (* see [item_separator] *)
27+
| Comment of t (* see [comment] *)
2628

2729
let nil = Nil
2830
let line = LineBreak Classic
@@ -52,6 +54,8 @@ let group d = Group {should_break = false; doc = d}
5254
let breakable_group ~force_break d = Group {should_break = force_break; doc = d}
5355
let custom_layout gs = CustomLayout gs
5456
let break_parent = BreakParent
57+
let item_separator ~prev ~next = ItemSeparator {prev; next}
58+
let comment d = Comment d
5559

5660
let space = Text " "
5761
let comma = Text ","
@@ -75,9 +79,9 @@ let propagate_forced_breaks doc =
7579
match doc with
7680
| Text _ | Nil | LineSuffix _ -> false
7781
| BreakParent -> true
78-
| LineBreak (Hard | Literal) -> true
82+
| LineBreak (Hard | Literal) | ItemSeparator _ -> true
7983
| LineBreak (Classic | Soft) -> false
80-
| Indent children ->
84+
| Indent children | Comment children ->
8185
let child_forces_break = walk children in
8286
child_forces_break
8387
| IfBreaks ({yes = true_doc; no = false_doc} as ib) ->
@@ -115,9 +119,12 @@ let propagate_forced_breaks doc =
115119
(* See documentation in interface file *)
116120
let rec will_break doc =
117121
match doc with
118-
| LineBreak (Hard | Literal) | BreakParent | Group {should_break = true} ->
122+
| LineBreak (Hard | Literal)
123+
| BreakParent | ItemSeparator _
124+
| Group {should_break = true} ->
119125
true
120-
| Group {doc} | Indent doc | CustomLayout (doc :: _) -> will_break doc
126+
| Group {doc} | Indent doc | Comment doc | CustomLayout (doc :: _) ->
127+
will_break doc
121128
| Concat docs -> List.exists will_break docs
122129
| IfBreaks {yes; no} -> will_break yes || will_break no
123130
| _ -> false
@@ -151,10 +158,12 @@ let fits w stack =
151158
| _, Nil | _, LineSuffix _ | _, BreakParent -> ()
152159
| _, Text txt -> width := width.contents - String.length txt
153160
| _, Indent doc -> calculate (indent + 2) mode doc
161+
| _, Comment doc -> calculate indent mode doc
154162
| Flat, LineBreak Hard | Flat, LineBreak Literal -> result := Some true
155163
| Flat, LineBreak Classic -> width := width.contents - 1
156164
| Flat, LineBreak Soft -> ()
157165
| Break, LineBreak _ -> result := Some true
166+
| _, ItemSeparator _ -> result := Some true
158167
| _, Group {should_break = true; doc} -> calculate indent Break doc
159168
| _, Group {doc} -> calculate indent mode doc
160169
| _, IfBreaks {yes = break_doc; broken = true} ->
@@ -184,6 +193,61 @@ let fits w stack =
184193
in
185194
calculate_all stack
186195

196+
let select_custom_layout ~width ~pos ind rest docs =
197+
let rec find_group_that_fits groups =
198+
match groups with
199+
| [] -> Nil
200+
| [last_group] -> last_group
201+
| doc :: docs ->
202+
if fits (width - pos) ((ind, Flat, doc) :: rest) then doc
203+
else find_group_that_fits docs
204+
in
205+
find_group_that_fits docs
206+
207+
(* Whether [doc], printed from the start of a line indented by [ind], spans more
208+
than one line. Takes the layout decisions of [to_string] up to the first line
209+
break. Comments and line suffixes do not count. *)
210+
let spans_lines ~width ind mode doc =
211+
let rec loop ~pos stack =
212+
match stack with
213+
| [] -> false
214+
| (ind, mode, doc) :: rest -> (
215+
match doc with
216+
| Nil | BreakParent | LineSuffix _ | Comment _ -> loop ~pos rest
217+
| Text txt ->
218+
String.contains txt '\n' || loop ~pos:(pos + String.length txt) rest
219+
| Concat docs ->
220+
loop ~pos (List.map (fun doc -> (ind, mode, doc)) docs @ rest)
221+
| Indent doc -> loop ~pos ((ind + 2, mode, doc) :: rest)
222+
| IfBreaks {yes; no; broken} ->
223+
let doc = if broken || mode = Break then yes else no in
224+
loop ~pos ((ind, mode, doc) :: rest)
225+
| LineBreak (Hard | Literal) | ItemSeparator _ -> true
226+
| LineBreak Classic -> mode = Break || loop ~pos:(pos + 1) rest
227+
| LineBreak Soft -> mode = Break || loop ~pos rest
228+
| Group {should_break; doc} ->
229+
let flat = (ind, Flat, doc) :: rest in
230+
if should_break || not (fits (width - pos) flat) then
231+
loop ~pos ((ind, Break, doc) :: rest)
232+
else loop ~pos flat
233+
| CustomLayout docs ->
234+
let doc = select_custom_layout ~width ~pos ind rest docs in
235+
loop ~pos ((ind, Flat, doc) :: rest))
236+
in
237+
loop ~pos:ind [(ind, mode, doc)]
238+
239+
(* Own-line trailing comments are line suffixes that start with a hard line. *)
240+
let starts_with_hard_line doc =
241+
match doc with
242+
| LineBreak Hard | Concat (LineBreak Hard :: _) -> true
243+
| _ -> false
244+
245+
let rec drop_leading_hard_lines doc =
246+
match doc with
247+
| LineBreak Hard -> Nil
248+
| Concat (LineBreak Hard :: docs) -> drop_leading_hard_lines (Concat docs)
249+
| doc -> doc
250+
187251
let to_string ~width doc =
188252
propagate_forced_breaks doc;
189253
let buffer = Mini_buffer.create 1000 in
@@ -201,6 +265,7 @@ let to_string ~width doc =
201265
let ops = List.map (fun doc -> (ind, mode, doc)) docs in
202266
process ~pos line_suffices (List.append ops rest)
203267
| Indent doc -> process ~pos line_suffices ((ind + 2, mode, doc) :: rest)
268+
| Comment doc -> process ~pos line_suffices ((ind, mode, doc) :: rest)
204269
| IfBreaks {yes = break_doc; broken = true} ->
205270
process ~pos line_suffices ((ind, mode, break_doc) :: rest)
206271
| IfBreaks {yes = break_doc; no = flat_doc} ->
@@ -243,16 +308,28 @@ let to_string ~width doc =
243308
then process ~pos line_suffices ((ind, Break, doc) :: rest)
244309
else process ~pos line_suffices ((ind, Flat, doc) :: rest)
245310
| CustomLayout docs ->
246-
let rec find_group_that_fits groups =
247-
match groups with
248-
| [] -> Nil
249-
| [last_group] -> last_group
250-
| doc :: docs ->
251-
if fits (width - pos) ((ind, Flat, doc) :: rest) then doc
252-
else find_group_that_fits docs
253-
in
254-
let doc = find_group_that_fits docs in
255-
process ~pos line_suffices ((ind, Flat, doc) :: rest))
311+
let doc = select_custom_layout ~width ~pos ind rest docs in
312+
process ~pos line_suffices ((ind, Flat, doc) :: rest)
313+
| ItemSeparator {prev; next} ->
314+
if spans_lines ~width ind mode prev || spans_lines ~width ind mode next
315+
then
316+
(* Own-line trailing comments of [prev] go below the blank line, where
317+
they print once they are leading comments of [next]. *)
318+
let separator = (ind, mode, hard_line) in
319+
let same_line, below =
320+
List.partition
321+
(fun (_, _, doc) -> not (starts_with_hard_line doc))
322+
(List.rev line_suffices)
323+
in
324+
let rest =
325+
match below with
326+
| [] -> rest
327+
| (ind, mode, doc) :: below ->
328+
((ind, mode, drop_leading_hard_lines doc) :: below)
329+
@ (separator :: rest)
330+
in
331+
process ~pos (List.rev same_line) (separator :: separator :: rest)
332+
else process ~pos line_suffices ((ind, mode, hard_line) :: rest))
256333
| [] -> (
257334
match line_suffices with
258335
| [] -> ()
@@ -332,6 +409,8 @@ let debug t =
332409
| Literal -> "Literal"
333410
in
334411
text ("LineBreak(" ^ break_txt ^ ")")
412+
| ItemSeparator _ -> text "ItemSeparator"
413+
| Comment doc -> concat [text "comment("; to_doc doc; text ")"]
335414
| Group {should_break; doc} ->
336415
group
337416
(concat
@@ -356,5 +435,5 @@ let is_nil doc =
356435
match doc with
357436
| Nil -> true
358437
| Text _ | LineSuffix _ | Concat _ | Indent _ | IfBreaks _ | LineBreak _
359-
| Group _ | CustomLayout _ | BreakParent ->
438+
| Group _ | CustomLayout _ | BreakParent | ItemSeparator _ | Comment _ ->
360439
false

‎compiler/syntax/src/res_doc.mli‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ val breakable_group : force_break:bool -> t -> t
1818
* will be checked until one fits. *)
1919
val custom_layout : t list -> t
2020
val break_parent : t
21+
22+
val item_separator : prev:t -> next:t -> t
23+
(** A hard line, and a blank line when [prev] or [next], the documents printed
24+
on each side of it, span several lines. It measures them but does not print
25+
them. *)
26+
27+
val comment : t -> t
28+
(** Prints like its argument, which [item_separator] does not measure. *)
29+
2130
val join : sep:t -> t list -> t
2231

2332
(* [(doc1, sep1); (doc2,sep2)] joins as doc1 sep1 doc2 *)

‎compiler/syntax/src/res_printer.ml‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,8 @@ let print_leading_comments node tbl loc =
326326
else if diff > 1 then Doc.concat [Doc.hard_line; Doc.hard_line]
327327
else Doc.hard_line
328328
in
329-
let doc =
330-
Doc.group
331-
(Doc.concat [Doc.concat (List.rev (cmt_doc :: acc)); separator; node])
332-
in
329+
let comments = Doc.concat (List.rev (separator :: cmt_doc :: acc)) in
330+
let doc = Doc.group (Doc.concat [Doc.comment comments; node]) in
333331
doc
334332
| comment :: (next_comment :: _comments as rest) ->
335333
let cmt_doc = print_leading_comment ~next_comment comment in
@@ -387,7 +385,9 @@ let print_comments_before_marker doc (tbl : Comment_table.t) loc marker =
387385
Hashtbl.replace tbl.trailing loc after_marker;
388386
print_trailing_comments doc tbl.trailing loc
389387

390-
let print_list ~get_loc ~nodes ~print ?(force_break = false) t =
388+
(* [~space_multiline] adds a blank line next to nodes printed on several lines *)
389+
let print_list ~get_loc ~nodes ~print ?(force_break = false)
390+
?(space_multiline = false) t =
391391
let rec loop first_loc_opt prev_loc_opt acc nodes =
392392
match nodes with
393393
| [] -> (first_loc_opt, prev_loc_opt, Doc.concat (List.rev acc))
@@ -403,12 +403,14 @@ let print_list ~get_loc ~nodes ~print ?(force_break = false) t =
403403
else
404404
let doc = print_comments raw_doc t loc in
405405
let acc =
406-
match prev_loc_opt with
407-
| None -> [doc]
408-
| Some prev_loc ->
406+
match (prev_loc_opt, acc) with
407+
| None, _ | _, [] -> [doc]
408+
| Some prev_loc, prev_doc :: _ ->
409409
let sep =
410410
if start_pos.pos_lnum - prev_loc.Location.loc_end.pos_lnum > 1
411411
then Doc.concat [Doc.hard_line; Doc.hard_line]
412+
else if space_multiline then
413+
Doc.item_separator ~prev:prev_doc ~next:doc
412414
else Doc.hard_line
413415
in
414416
doc :: sep :: acc
@@ -690,7 +692,7 @@ let rec print_structure ~state (s : Parsetree.structure) t =
690692
| [] -> print_comments_inside_file t
691693
| structure ->
692694
let previous_can_continue = ref false in
693-
print_list
695+
print_list ~space_multiline:true
694696
~get_loc:(fun s -> s.Parsetree.pstr_loc)
695697
~nodes:structure
696698
~print:(fun item cmt_tbl ->
@@ -1108,7 +1110,7 @@ and print_signature ~state signature cmt_tbl =
11081110
match signature with
11091111
| [] -> print_comments_inside_file cmt_tbl
11101112
| signature ->
1111-
print_list
1113+
print_list ~space_multiline:true
11121114
~get_loc:(fun s -> s.Parsetree.psig_loc)
11131115
~nodes:signature
11141116
~print:(print_signature_item ~state)

‎tests/syntax_tests/data/ast-mapping/expected/Braces.res.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
let value = {42}
2+
23
let branch = if true {
34
1
45
} else {
56
2
67
}
8+
79
let case = switch value {
810
| 42 => value
911
| _ => 0

‎tests/syntax_tests/data/ast-mapping/expected/FunctionsAndArrows.res.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ let id = x => x
77

88
// labeled, optional, and default parameters
99
let labeled = (~x, ~y) => x - y
10+
1011
let optional = (~x=?, ~y=1, z) => {
1112
switch x {
1213
| Some(x) => x + y + z

‎tests/syntax_tests/data/ast-mapping/expected/JSXElements.res.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ let elementWithConditionalChildren = ReactDOM.jsx(
3939
}),
4040
},
4141
)
42+
4243
let commentOnly = ReactDOM.jsx("div", {/* no child */})
44+
4345
let commentsBetweenElements = ReactDOM.jsx(
4446
"div",
4547
{

‎tests/syntax_tests/data/ast-mapping/expected/JSXFragments.res.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ let nestedFragments = React.jsxs(
2222
]),
2323
},
2424
)
25+
2526
let commentOnly = React.jsx(React.jsxFragment, {}) // no child
27+
2628
let commentsBetweenElements = React.jsxs(
2729
React.jsxFragment /* between */,
2830
{children: React.array([ReactDOM.jsx("span", {}), ReactDOM.jsx("span", {})])},

‎tests/syntax_tests/data/ast-mapping/expected/JsxComponents.res.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ module A = {
33
type props<'x> = {
44
x: 'x,
55
}
6+
67
let make = ({x, _}: props<_>): React.element => React.string(x)
8+
79
let make = React.component({
810
let \"JsxComponents$A" = (props: props<_>) => make(props)
911
\"JsxComponents$A"
@@ -15,6 +17,7 @@ module B = {
1517

1618
/** Doc */
1719
@deprecated @genType let make = ({x, _}: props): React.element => React.string(x)
20+
1821
/** Doc */
1922
@deprecated @genType
2023
let make = React.component({
@@ -26,6 +29,7 @@ module B = {
2629

2730
module C = {
2831
let make = (props: props): React.element => React.string(props.x)
32+
2933
let make = React.component({
3034
let \"JsxComponents$C" = (props: props): React.element => make(props)
3135
\"JsxComponents$C"
@@ -34,6 +38,7 @@ module C = {
3438

3539
module D = {
3640
let make = (props: props): React.element => React.string(props.x)
41+
3742
let make = React.component({
3843
let \"JsxComponents$D" = (props: props): React.element => make(props)
3944
\"JsxComponents$D"
@@ -57,6 +62,7 @@ module F: {
5762
}
5863

5964
let make: React.component<props<string>>
65+
6066
@jsx.componentWithProps
6167
let other: props => React.element
6268
} = {
@@ -66,6 +72,7 @@ module F: {
6672
}
6773

6874
let make = ({x, _}: props<_>): React.element => React.string(x)
75+
6976
let make = React.component({
7077
let \"JsxComponents$F" = (props: props<_>) => make(props)
7178

‎tests/syntax_tests/data/ast-mapping/expected/RawExtensions.res.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
let add = %raw("(a, b) => a + b")
2+
23
let template = %raw(`function (a) {
34
return "x" + a
45
}`)
6+
57
let escaped = %raw(`"a\`b\${c}"`)
68
let fn = %ffi("(a) => a + 1")
79
let applied = %ffi(`(a, b) => a * b`)(2, 3)

0 commit comments

Comments
 (0)