Skip to content

Commit 641dd4b

Browse files
committed
Fix formatting oscillation with if-then-else=fit-or-vertical and begin...end
When `if-then-else=fit-or-vertical` formats a branch containing `begin (* long comment *) if ... then ... end`, comments between the `then` keyword and `begin` would get attached to different AST locations on each formatting pass ("after" the keyword vs "before" the begin...end expression). This caused `branch_pro` to compute different indentation each time, producing an infinite oscillation that triggered "BUG: formatting did not stabilize after 10 iterations". The fix has two parts: 1. In Fmt_ast, when the if-then-else branch is a special begin...end (wrapping match/try/function/ifthenelse), relocate "after keyword" comments to "before branch" before formatting. This ensures consistent comment placement regardless of source layout. 2. In Params, when computing branch_pro for Fit_or_vertical, also check for comments before the inner expression of a special begin...end, not just before the begin...end node itself. A new helper `Cmts.relocate_after_to_before` moves comments from the "after" position of one location to the "before" position of another.
1 parent 5ebeb8e commit 641dd4b

97 files changed

Lines changed: 664 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ profile. This started with version 0.26.0.
88

99
### Fixed
1010

11+
- Fix formatting oscillation with `if-then-else=fit-or-vertical` and
12+
`begin...end`.
13+
(#2780, @MisterDA)
14+
1115
- Fix instability on long `if-then-else` with `if-then-else=fit-or-vertical`
1216
(#2797, @MisterDA)
1317

lib/Cmts.ml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,20 @@ let relocate_cmts_before (t : t) ~src ~sep ~dst =
328328
in
329329
update_cmts t `Before ~f ; update_cmts t `Within ~f
330330

331+
let relocate_after_to_before (t : t) ~src ~dst =
332+
let merge_and_sort x y =
333+
List.rev_append x y
334+
|> List.sort ~compare:(Comparable.lift Location.compare_start ~f:Cmt.loc)
335+
in
336+
match Map.find t.cmts_after src with
337+
| None -> ()
338+
| Some cmts ->
339+
update_cmts t `After ~f:(fun map -> Map.remove map src) ;
340+
update_cmts t `Before ~f:(fun map ->
341+
Map.update map dst ~f:(function
342+
| None -> cmts
343+
| Some existing -> merge_and_sort cmts existing ) )
344+
331345
let relocate_pattern_matching_cmts (t : t) src tok ~whole_loc ~matched_loc =
332346
if not (whole_loc.Location.loc_ghost || matched_loc.Location.loc_ghost)
333347
then

lib/Cmts.mli

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ val relocate_wrongfully_attached_cmts :
4444
pattern-matching expressions ([match-with] or [try-with] expressions) but
4545
are wrongfully attached to the matched expression. *)
4646

47+
val relocate_after_to_before : t -> src:Location.t -> dst:Location.t -> unit
48+
(** [relocate_after_to_before t ~src ~dst] moves comments that are after
49+
[src] to be before [dst]. *)
50+
4751
val fmt_before :
4852
t
4953
-> Conf.t

lib/Fmt_ast.ml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2556,6 +2556,22 @@ and fmt_expression c ?(box = true) ?(pro = noop) ?eol ?parens
25562556
let parens_bch =
25572557
parenze_exp xbch && not symbol_parens
25582558
in
2559+
(* When the branch is begin...end wrapping a special
2560+
expression (match/try/function/if), comments
2561+
between the keyword and begin can end up attached
2562+
to different locations depending on source
2563+
layout. Relocate them to a consistent position to
2564+
avoid oscillation. *)
2565+
( match xbch.ast.pexp_desc with
2566+
| Pexp_beginend ({pexp_desc; _}, _)
2567+
when match pexp_desc with
2568+
| Pexp_match _ | Pexp_try _
2569+
|Pexp_function _ | Pexp_ifthenelse _ ->
2570+
true
2571+
| _ -> false ->
2572+
Cmts.relocate_after_to_before c.cmts
2573+
~src:keyword_loc ~dst:xbch.ast.pexp_loc
2574+
| _ -> () ) ;
25592575
let cmts_before_kw =
25602576
Cmts.fmt_before c keyword_loc
25612577
in

lib/Params.ml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,14 @@ let get_if_then_else (c : Conf.t) ~cmts_before_opt ~pro ~first ~last
968968
then
969969
match cmts_before_opt xbch.ast.pexp_loc with
970970
| Some cmts -> break 1000 2 $ cmts
971-
| None -> branch_pro ~begin_end_offset:0 ()
971+
| None -> (
972+
match xbch.ast.pexp_desc with
973+
| Pexp_beginend (nested_exp, _)
974+
when is_special_beginend nested_exp.pexp_desc -> (
975+
match cmts_before_opt nested_exp.pexp_loc with
976+
| Some cmts -> break 1000 2 $ cmts
977+
| None -> branch_pro ~begin_end_offset:0 () )
978+
| _ -> branch_pro ~begin_end_offset:0 () )
972979
else branch_pro ~begin_end_offset:0 () )
973980
; wrap_parens=
974981
wrap_parens
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Warning: ite-compact.ml:95 exceeds the margin
22
Warning: ite-compact.ml:100 exceeds the margin
33
Warning: ite-compact.ml:105 exceeds the margin
4+
Warning: ite-compact.ml:199 exceeds the margin

test/passing/refs.ahrefs/ite-compact.ml.ref

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,13 @@ let test =
201201
(* comment *)
202202
1
203203
else 2
204+
205+
(* Begin-end with long comment before nested if (regression test for
206+
formatting oscillation) *)
207+
let f =
208+
match x with
209+
| A ->
210+
if gf then
211+
(* aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa *)
212+
begin if a then b
213+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Warning: ite-compact_closing.ml:216 exceeds the margin

test/passing/refs.ahrefs/ite-compact_closing.ml.ref

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,13 @@ let test =
220220
(* comment *)
221221
1
222222
else 2
223+
224+
(* Begin-end with long comment before nested if (regression test for
225+
formatting oscillation) *)
226+
let f =
227+
match x with
228+
| A ->
229+
if gf then
230+
(* aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa *)
231+
begin if a then b
232+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Warning: ite-fit_or_vertical.ml:116 exceeds the margin
22
Warning: ite-fit_or_vertical.ml:121 exceeds the margin
33
Warning: ite-fit_or_vertical.ml:126 exceeds the margin
4+
Warning: ite-fit_or_vertical.ml:236 exceeds the margin

0 commit comments

Comments
 (0)