Skip to content

Commit cf9f3cd

Browse files
committed
Merge branch 'MAIN' into pr/2737
2 parents f8b6a44 + f077f22 commit cf9f3cd

Some content is hidden

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

55 files changed

+921
-121
lines changed

CHANGES.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,65 @@ profile. This started with version 0.26.0.
66

77
## unreleased
88

9+
### Added
10+
11+
- Added option `letop-punning` (#2746, @WardBrian) to control whether
12+
punning is used in extended binding operators.
13+
For example, the code `let+ x = x in ...` can be formatted as
14+
`let+ x in ...` when `letop-punning=always`. With `letop-punning=never`, it
15+
becomes `let+ x = x in ...`. The default is `preserve`, which will
16+
only use punning when it exists in the source.
17+
This also applies to `let%ext` bindings (#2747, @WardBrian).
18+
919
### Fixed
1020

21+
- Fix dropped comment in `(function _ -> x (* cmt *))` (#2739, @Julow)
22+
23+
- \* `cases-matching-exp-indent=compact` does not impact `begin end` nodes that
24+
don't have a match inside. (#2742, @EmileTrotignon)
25+
```ocaml
26+
(* before *)
27+
begin match () with
28+
| () -> begin
29+
f x
30+
end
31+
end
32+
(* after *)
33+
begin match () with
34+
| () -> begin
35+
f x
36+
end
37+
end
38+
```
39+
1140
- `Ast_mapper` now iterates on *all* locations inside of Longident.t,
1241
instead of only some.
1342
(#2737, @v-gb)
1443

44+
### Internal
45+
46+
- Added information on writing tests to `CONTRIBUTING.md` (#2838, @WardBrian)
47+
48+
### Changed
49+
50+
- indentation of the `end` keyword in a match-case is now always at least 2. (#2742, @EmileTrotignon)
51+
```ocaml
52+
(* before *)
53+
begin match () with
54+
| () -> begin
55+
match () with
56+
| () -> ()
57+
end
58+
end
59+
(* after *)
60+
begin match () with
61+
| () -> begin
62+
match () with
63+
| () -> ()
64+
end
65+
end
66+
```
67+
1568
## 0.28.1
1669

1770
### Highlight
@@ -40,7 +93,7 @@ profile. This started with version 0.26.0.
4093
### Added
4194

4295
- Added option `module-indent` option (#2711, @HPRIOR) to control the indentation
43-
of items within modules. This affects modules and signatures. For example,
96+
of items within modules. This affects modules and signatures. For example,
4497
module-indent=4:
4598
```ocaml
4699
module type M = sig
@@ -148,7 +201,7 @@ profile. This started with version 0.26.0.
148201
- Fix a crash where `type%e nonrec t = t` was formatted as `type nonrec%e t = t`,
149202
which is invalid syntax. (#2712, @EmileTrotignon)
150203

151-
- Fix commandline parsing being quadratic in the number of arguments
204+
- Fix commandline parsing being quadratic in the number of arguments
152205
(#2724, @let-def)
153206

154207
- \* Fix `;;` being added after a documentation comment (#2683, @EmileTrotignon)

CONTRIBUTING.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,57 @@ We heartily welcome pull requests. To ensure an effective contribution, please a
1919
1. For significant, invasive, or output-affecting changes, consider opening an issue for discussion before committing substantial time and effort.
2020
2. If you're new to the project, starting with the [Good first issues](https://github.com/ocaml-ppx/ocamlformat/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22Good-first-issue+%3Agreen_heart%3A%22) can be beneficial.
2121
3. Fork the repository and create your branch from `main`.
22-
4. If you've added code that should be tested, supplement it with tests located in the `test/` directory.
23-
5. Ensure that the test suite passes (see [Running the tests](#running-the-tests) for instructions).
22+
4. Ensure that the test suite passes (see [Running the tests](#running-the-tests) for instructions). If there are changes in the tests,
23+
verify that they are expected. If no existing test output changed, you likely need to add new tests (see [Adding a test](#adding-a-test)).
24+
25+
### Adding a Test
26+
27+
#### Golden Tests
28+
29+
The majority of tests for `ocamlformat` are so called "golden" or "expect" tests. These
30+
tests all consist of running `ocamlformat` on `.ml` files and comparing the
31+
output to expected output stored in reference files. The `dune` configuration
32+
for this is automatically generated for you based on the files present in these
33+
directories the first time `dune runtest` is executed.
34+
35+
These are separated into `test/failing` and `test/passing` directories.
36+
37+
To add a test showing currently incorrect behavior, add a `.ml` file to
38+
`test/failing/tests`. If command line arguments are needed, create a
39+
corresponding `.ml.opts` file with the same name. The output of these tests
40+
will be stored in `.ml.broken-ref` files.
41+
42+
To add a test showing correct behavior, add a `.ml` file to `test/passing/tests`
43+
or update an existing file there. These tests are similar to the failing tests,
44+
including the use of the `.ml.opts` files for command line arguments, however the
45+
same file will be tested against all of the built-in configurations (e.g.
46+
`default`, `janestreet`, etc.), with outputs appearing in sub folders named
47+
`refs.<configuration>`.
48+
49+
In both cases, if multiple sets of options are desired, you can create multiple
50+
`.ml.opts` files with names provided after a `-`, for example `mytest.ml` can have
51+
`mytest-foo.ml.opts` and `mytest-bar.ml.opts`. This will create multiple test targets.
52+
53+
The first time you [run the tests](#running-the-tests), the `dune.inc` file will
54+
be updated. Once you have promoted this, subsequent runs will show you any
55+
differences between the expected and actual outputs, which can be accepted with
56+
`dune promote`.
57+
58+
#### cram-Style Tests
59+
60+
The remaining tests are not designed to test the formatting output of
61+
`ocamlformat`, but rather the command line interface behavior, such as error
62+
messages and help text.
63+
64+
These are found in `test/cli` and are written using
65+
[cram testing](https://dune.readthedocs.io/en/latest/reference/cram.html).
66+
Each test is a `.t` file that contains shell commands and their expected
67+
outputs. To add a new test, create a new `.t` file, or add new commands (lines
68+
beginning with ` $`) to an existing file.
69+
70+
#### Unit Tests
71+
72+
Unit tests are located in the `test/unit` directory and are written using [Alcotest](https://github.com/mirage/alcotest).
2473

2574
### Running the Tests
2675

doc/manpage_ocamlformat.mld

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ OPTIONS (CODE FORMATTING STYLE)
271271
... = and before the in if the module declaration does not fit on
272272
a single line. The default value is compact.
273273

274+
--letop-punning={preserve|always|never}
275+
Name punning in bindings using extended let operators and let%ext
276+
bindings. preserve uses let-punning only when it exists in the
277+
source; the code "let* foo and* z = z in ..." will be left
278+
unchanged. always uses let-punning whenever possible; the code
279+
"let* foo and* z = z in ..." will be rewritten to "let* foo and* z
280+
in ...". never never uses let-punning; the code "let* foo and* z =
281+
z in ..." will be rewritten to "let* foo = foo and* z = z in ...".
282+
The default value is preserve.
283+
274284
--line-endings={lf|crlf}
275285
Line endings used. lf uses Unix line endings. crlf uses Windows
276286
line endings. The default value is lf. Cannot be set in

lib/Conf.ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ let conventional_profile from =
8686
; let_binding_deindent_fun= elt true
8787
; let_binding_spacing= elt `Compact
8888
; let_module= elt `Compact
89+
; letop_punning= elt `Preserve
8990
; line_endings= elt `Lf
9091
; margin= elt 80
9192
; match_indent= elt 0
@@ -156,6 +157,7 @@ let ocamlformat_profile from =
156157
; let_binding_deindent_fun= elt true
157158
; let_binding_spacing= elt `Compact
158159
; let_module= elt `Compact
160+
; letop_punning= elt `Preserve
159161
; line_endings= elt `Lf
160162
; margin= elt 80
161163
; match_indent= elt 0
@@ -225,6 +227,7 @@ let janestreet_profile from =
225227
; let_binding_deindent_fun= elt false
226228
; let_binding_spacing= elt `Double_semicolon
227229
; let_module= elt `Sparse
230+
; letop_punning= elt `Preserve
228231
; line_endings= elt `Lf
229232
; margin= elt 90
230233
; match_indent= elt 0
@@ -994,6 +997,30 @@ module Formatting = struct
994997
(fun conf elt -> update conf ~f:(fun f -> {f with let_module= elt}))
995998
(fun conf -> conf.fmt_opts.let_module)
996999

1000+
let letop_punning =
1001+
let doc =
1002+
"Name punning in bindings using extended let operators and \
1003+
$(i,let%ext) bindings."
1004+
in
1005+
let names = ["letop-punning"] in
1006+
let all =
1007+
[ Decl.Value.make ~name:"preserve" `Preserve
1008+
"$(b,preserve) uses let-punning only when it exists in the \
1009+
source; the code \"$(i,let* foo and* z = z in ...)\" will be \
1010+
left unchanged."
1011+
; Decl.Value.make ~name:"always" `Always
1012+
"$(b,always) uses let-punning whenever possible; the code \
1013+
\"$(i,let* foo and* z = z in ...)\" will be rewritten to \
1014+
\"$(i,let* foo and* z in ...)\"."
1015+
; Decl.Value.make ~name:"never" `Never
1016+
"$(b,never) never uses let-punning; the code \"$(i,let* foo and* \
1017+
z = z in ...)\" will be rewritten to \"$(i,let* foo = foo and* z \
1018+
= z in ...)\". " ]
1019+
in
1020+
Decl.choice ~names ~all ~default ~doc ~kind
1021+
(fun conf elt -> update conf ~f:(fun f -> {f with letop_punning= elt}))
1022+
(fun conf -> conf.fmt_opts.letop_punning)
1023+
9971024
let let_open =
9981025
let names = ["let-open"] in
9991026
let msg = concrete_syntax_preserved_msg in
@@ -1353,6 +1380,7 @@ module Formatting = struct
13531380
; elt let_binding_deindent_fun
13541381
; elt let_binding_spacing
13551382
; elt let_module
1383+
; elt letop_punning
13561384
; elt line_endings
13571385
; elt margin
13581386
; elt match_indent

lib/Conf_t.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ type fmt_opts =
9494
; let_binding_deindent_fun: bool elt
9595
; let_binding_spacing: [`Compact | `Sparse | `Double_semicolon] elt
9696
; let_module: [`Compact | `Sparse] elt
97+
; letop_punning: [`Always | `Preserve | `Never] elt
9798
; line_endings: [`Lf | `Crlf] elt
9899
; margin: int elt
99100
; match_indent: int elt

lib/Conf_t.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type fmt_opts =
9292
(** De-indent the [fun] in a let-binding body. *)
9393
; let_binding_spacing: [`Compact | `Sparse | `Double_semicolon] elt
9494
; let_module: [`Compact | `Sparse] elt
95+
; letop_punning: [`Always | `Preserve | `Never] elt
9596
; line_endings: [`Lf | `Crlf] elt
9697
; margin: int elt (** Format code to fit within [margin] columns. *)
9798
; match_indent: int elt

lib/Extended_ast.ml

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ let map (type a) (x : a t) (m : Ast_mapper.mapper) : a -> a =
5757
| Documentation -> Fn.id
5858

5959
module Parse = struct
60-
let normalize_mapper ~ocaml_version ~preserve_beginend =
60+
let normalize_mapper ~ocaml_version ~preserve_beginend ~prefer_let_puns =
6161
let open Asttypes in
6262
let open Ast_mapper in
6363
let enable_short_field_annot =
@@ -233,10 +233,54 @@ module Parse = struct
233233
let b' =
234234
let loc_start = b.pbop_op.loc.loc_start in
235235
let loc_end = b.pbop_exp.pexp_loc.loc_end in
236-
{b with pbop_loc= {b.pbop_loc with loc_start; loc_end}}
236+
let pbop_is_pun =
237+
match prefer_let_puns with
238+
| None -> b.pbop_is_pun
239+
| Some false -> false
240+
| Some true -> (
241+
b.pbop_is_pun
242+
||
243+
match (b.pbop_pat.ppat_desc, b.pbop_exp.pexp_desc) with
244+
| Ppat_var {txt; _}, Pexp_ident {txt= Lident e; _} ->
245+
String.equal txt e
246+
| _ -> false )
247+
in
248+
{b with pbop_loc= {b.pbop_loc with loc_start; loc_end}; pbop_is_pun}
237249
in
238250
Ast_mapper.default_mapper.binding_op m b'
239251
in
252+
let value_bindings (m : Ast_mapper.mapper) vbs =
253+
let punning is_extension vb =
254+
let is_extension =
255+
(* [and] nodes don't have extensions, so we need to track if the
256+
earlier [let] did *)
257+
is_extension || Option.is_some vb.pvb_attributes.attrs_extension
258+
in
259+
let pvb_is_pun =
260+
is_extension
261+
&&
262+
match prefer_let_puns with
263+
| None -> vb.pvb_is_pun
264+
| Some false -> false
265+
| Some true -> (
266+
vb.pvb_is_pun
267+
||
268+
match (vb.pvb_pat.ppat_desc, vb.pvb_body) with
269+
| ( Ppat_var {txt; _}
270+
, Pfunction_body {pexp_desc= Pexp_ident {txt= Lident e; _}; _}
271+
) ->
272+
String.equal txt e
273+
| _ -> false )
274+
in
275+
(is_extension, {vb with pvb_is_pun})
276+
in
277+
let vbs' =
278+
{ vbs with
279+
pvbs_bindings=
280+
snd @@ List.fold_map ~init:false ~f:punning vbs.pvbs_bindings }
281+
in
282+
Ast_mapper.default_mapper.value_bindings m vbs'
283+
in
240284
let pat m = function
241285
| {ppat_desc= Ppat_cons (_ :: _ :: _ :: _ as l); _} as p
242286
when match List.last_exn l with
@@ -305,11 +349,12 @@ module Parse = struct
305349
{p with pexp_desc= Pexp_tuple l}
306350
| e -> Ast_mapper.default_mapper.expr m e
307351
in
308-
Ast_mapper.{default_mapper with expr; pat; binding_op}
352+
Ast_mapper.{default_mapper with expr; pat; binding_op; value_bindings}
309353

310-
let ast (type a) (fg : a t) ~ocaml_version ~preserve_beginend ~input_name
311-
str : a =
312-
map fg (normalize_mapper ~ocaml_version ~preserve_beginend)
354+
let ast (type a) (fg : a t) ~ocaml_version ~preserve_beginend
355+
~prefer_let_puns ~input_name str : a =
356+
map fg
357+
(normalize_mapper ~ocaml_version ~preserve_beginend ~prefer_let_puns)
313358
@@
314359
let lexbuf = Lexing.from_string str in
315360
let ocaml_version =

lib/Extended_ast.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module Parse : sig
3737
'a t
3838
-> ocaml_version:Ocaml_version.t
3939
-> preserve_beginend:bool
40+
-> prefer_let_puns:bool option
4041
-> input_name:string
4142
-> string
4243
-> 'a

lib/Fmt_ast.ml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,7 @@ and fmt_function ?force_closing_paren ~ctx ~ctx0 ?pro ~wrap_intro
16331633
in
16341634
fmt_infix_ext_attrs c ~pro:function_ infix_ext_attrs
16351635
in
1636+
let cmt_after_cases = Cmts.fmt_after c function_loc in
16361637
let box_cases ~pro cases =
16371638
let pro_inner, pro_outer, indent =
16381639
(* Formatting of if-then-else relies on the ~box argument. *)
@@ -1653,8 +1654,9 @@ and fmt_function ?force_closing_paren ~ctx ~ctx0 ?pro ~wrap_intro
16531654
( fmt_pattern c ~pro:(if_newline "| ") (sub_pat ~ctx pc_lhs)
16541655
$ space_break $ str "->" )
16551656
$ space_break
1656-
$ cbox 0 (fmt_expression c (sub_exp ~ctx pc_rhs)) )
1657-
| _ -> (box, fmt_cases c ctx cs)
1657+
$ cbox 0 (fmt_expression c (sub_exp ~ctx pc_rhs))
1658+
$ cmt_after_cases )
1659+
| _ -> (box, fmt_cases c ctx cs $ cmt_after_cases)
16581660
in
16591661
(fun_ $ function_, box_cases cases, box, 0)
16601662
in
@@ -2570,14 +2572,16 @@ and fmt_expression c ?(box = true) ?(pro = noop) ?eol ?parens
25702572
$ fmt_atrs ) )
25712573
| Pexp_let (lbs, body, loc_in) ->
25722574
let bindings =
2573-
Sugar.Let_binding.of_let_bindings ~ctx lbs.pvbs_bindings
2575+
Sugar.Let_binding.of_let_bindings ~ctx ~cmts:c.cmts lbs.pvbs_bindings
25742576
in
25752577
let fmt_expr = fmt_expression c (sub_exp ~ctx body) in
25762578
pro
25772579
$ fmt_let_bindings c ~ctx0:ctx ~parens ~fmt_atrs ~fmt_expr ~has_attr
25782580
~loc_in lbs.pvbs_rec bindings body
25792581
| Pexp_letop {let_; ands; body; loc_in} ->
2580-
let bd = Sugar.Let_binding.of_binding_ops (let_ :: ands) in
2582+
let bd =
2583+
Sugar.Let_binding.of_binding_ops ~cmts:c.cmts (let_ :: ands)
2584+
in
25812585
let fmt_expr = fmt_expression c (sub_exp ~ctx body) in
25822586
pro
25832587
$ fmt_let_bindings c ~ctx0:ctx ~parens ~fmt_atrs ~fmt_expr ~has_attr
@@ -3266,7 +3270,7 @@ and fmt_class_expr c ({ast= exp; ctx= ctx0} as xexp) =
32663270
| _ -> c.conf.fmt_opts.indent_after_in.v
32673271
in
32683272
let bindings =
3269-
Sugar.Let_binding.of_let_bindings ~ctx lbs.pvbs_bindings
3273+
Sugar.Let_binding.of_let_bindings ~ctx ~cmts:c.cmts lbs.pvbs_bindings
32703274
in
32713275
let fmt_expr = fmt_class_expr c (sub_cl ~ctx body) in
32723276
let has_attr = not (List.is_empty pcl_attributes) in
@@ -4694,7 +4698,9 @@ and fmt_structure_item c ~last:last_item ~semisemi {ctx= parent_ctx; ast= si}
46944698
let fmt_item c ctx ~prev ~next b =
46954699
let first = Option.is_none prev in
46964700
let last = Option.is_none next in
4697-
let b = Sugar.Let_binding.of_let_binding ~ctx ~first b in
4701+
let b =
4702+
Sugar.Let_binding.of_let_binding ~ctx ~first ~cmts:c.cmts b
4703+
in
46984704
let epi =
46994705
match c.conf.fmt_opts.let_binding_spacing.v with
47004706
| `Compact -> None

0 commit comments

Comments
 (0)