Release of OCamlFormat 0.10

by Guillaume Petiot on Jun 27th, 2019

We are pleased to announce the release of OCamlFormat 0.10 (available on opam).

There have been numerous changes since the last release, so here is a comprehensive list of the new features and breaking changes to help the transition from OCamlFormat 0.9.

ocamlformat-0.10 now works on the 4.08 AST, although the formatting should not differ greatly from the one of ocamlformat-0.9 in this regard. Please note that it is necessary to build ocamlformat with 4.08 to be able to parse new features like let*.

Upgrading from ocamlformat-0.9 requires to install the following dependencies:

  • ocaml-migrate-parsetree >= 1.3.1 (upgrade)
  • uuseg >= 10.0.0 (new)
  • uutf >= 1.0.1 (upgrade)

This release focuses on preserving the style of the original source and on handling more ocp-indent options.

Style preservation

Expression grouping

The new option exp-grouping has been added to preserve the keywords begin/end that are used to delimit expressions instead of parentheses. exp-grouping=parens always uses parentheses to delimit expressions. exp-grouping=preserve preserves the original grouping syntax (parentheses or begin/end).

Horizontal alignment

Horizontal alignment is something that users often use to make pattern-matching or type declarations easier to read, and it is a feature that has been requested many times. Three new options have been added to horizontally align the lines.

align-cases horizontally aligns the match/try cases:

let fooooooooooo =
  match foooooooooooooooooooooooo with
  | Bfooooooooooooooooo -> foooooooooooo
  | C (a, b, c, d)      -> fooooooooooooooooooo
  | _                   -> fooooooooooooooooooo

align-constructors-decl horizontally aligns type declarations:

type t =
  | ( :: ) of a * b
  | []     of looooooooooooooooooooooooooooooooooooooong_break

align-variants-decl horizontally aligns variants type declarations:

type x =
  [ `Foooooooo      of int
  | `Fooooooooooooo of int ]

Preserve blank lines in sequences

The new option sequence-blank-line decides whether a blank line is preserved between expressions of a sequence. sequence-blank-line=compact will not keep any blank line between expressions of a sequence, this is still the default behavior. sequence-blank-line=preserve will keep a blank line between two expressions of a sequence if the input contains at least one.

This option can help preserving the readability of the code in this situation:

let foo x y =
  do_some_setup y ;

  important_function x

Supporting more ocp-indent options

The long term goal of ocamlformat is to handle every ocp-indent option, this release got closer to this goal as the following ocp-indent options are now supported by ocamlformat:

  • max_indent
  • with
  • strict_with
  • ppx_stritem_ext
  • base
  • in
  • type

Offset added to a new line

The new option max-indent sets the maximum offset (number of columns) added to a new line in addition to the offset of the previous line. If this offset is set to 2 columns, then each new line can only be indented by 2 columns more in addition to the previous line, for example:

let () =
  fooooo
  |> List.iter (fun x ->
    let x = x $ y in
    fooooooooooo x)

This option is equivalent to the max_indent option of ocp-indent, and it will be set if max_indent is set in an .ocp-indent configuration file.

Indentation of pattern matching cases

The new options funtion-indent and match-indent respectively decide the indentation of function cases and the indentation of match/try cases. These options are equivalent to the with option of ocp-indent, and they will be set if with is set in an ocp-indent configuration file. If the indentation is set to 4 columns, cases are formatted like this:

let foooooooo = function
    | fooooooooooooooooooooooo -> foooooooooooooooooooooooooo

let foooooooo =
  match fooooooooooooooooooooooo with
      | fooooooooooooooooooooooo -> foooooooooooooooooooooooooo

The new options function-indent-nested and match-indent-nested respectively decide whether the function-indent and the match-indent parameters should be applied even when in a sub-block. If these options are set to never, it only applies function-indent or match-indent if the function or match block starts a line. If these options are set to always, then the indent parameters are always applied. The auto value applies the indentation parameter when seen fit.

These options are equivalent to the strict_with option of ocp-indent, and they will be set if strict_with is set in an ocp-indent configuration file.

Indentation inside extension nodes

The new option extension-indent sets the indentation of items (that are not at structure level) inside extension nodes. The new option stritem-extension-indent sets the indentation of structure items inside extension nodes. This option is equivalent to the ppx_stritem_ext option of ocp-indent, and it will be set if ppx_stritem_ext is set in an .ocp-indent configuration file.

For example if extension-indent is set to 5 and stritem-extension-indent is set to 3:

let foo =
  [%foooooooooo
       fooooooooooooooooooooooooooo foooooooooooooooooooooooooooooooooo
         foooooooooooooooooooooooooooo]
  [@@foooooooooo
       fooooooooooooooooooooooooooo foooooooooooooooooooooooooooooooooo
         foooooooooooooooooooooooooooo]

[@@@foooooooooo
   fooooooooooooooooooooooooooo foooooooooooooooooooooooooooooooooo
     foooooooooooooooooooooooooooo]

Let-binding indentation

The new option let-binding-indent sets the indentation of let binding expressions if they do not fit on a single line. This option is equivalent to the base option of ocp-indent. The new option indent-after-in sets the indentation after let ... in, unless followed by another let. This option is equivalent to the in option of ocp-indent. The new option type-decl-indent sets the indentation of type declarations if they do not fit on a single line. This option is equivalent to the type option of ocp-indent.

These options will be set if their ocp-indent counterparts are set in an .ocp-indent configuration file.

Miscellaneous features

This release also brings some new options, new values for existing features, or corrects erroneous behaviours.

Indicate multiline delimiters

The former indicate-multiline-delimiters boolean option is now a 3-valued option:

  • indicate-multiline-delimiters=space (was equivalent to true) prints a space inside the delimiter to indicate the matching one is on a different line.
  • indicate-multiline-delimiters=no (was equivalent to false) doesn't do anything special to indicate the closing delimiter.
  • indicate-multiline-delimiters=closing-on-separate-line is the new feature of this option, it makes sure that the closing delimiter is on its own line.

On this example we can see the closing parenthesis delimiting the nested pattern-matchings are on their own line and are aligned with the matching opening parenthesis:

let () =
   match v with
   | None -> None
   | Some x ->
       ( match x with
       | None -> None
       | Some x ->
           ( match x with
           | None -> None
           | Some x -> x
           )
       )

Formatting of literal strings

break-string-literals=newlines now takes into account pretty-printing commands like @,, @; and @\n to produce more readable strings. A new value for this option has been added, break-string-literals=newlines-and-wrap, to break lines at newlines delimiters (including pretty-printing commands) and also wrap the string literals at the margin.

Here is how break-string-literals=newlines-and-wrap formats a string:

let fooooooooooo =
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod \
   tempor incididunt ut labore et dolore magna aliqua.@;\
   Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \
   ut aliquip ex ea commodo consequat.@;\
   Duis aute irure dolor in reprehenderit in voluptate velit esse cillum \
   dolore eu fugiat nulla pariatur.@;\
   Excepteur sint occaecat cupidatat non proident, sunt in culpa qui \
   officia deserunt mollit anim id est laborum."

Warning: the break-string-literals will likely be removed in the next release and the default behavior would be newlines-and-wrap.

Break before the in keyword

The new option break-before-in has been added to decide whether the line should break before the in keyword of a let binding. break-before-in=fit-or-vertical will always break the line before the in keyword if the whole let binding does not fit on a single line, it is still the default behavior. break-before-in=auto will only break the line if the in keyword does not fit on the previous line.

For example:

let _ =
  let short = this is short in
  let fooo =
    (this is very long) but (the in keyword can fit) on the same line in
  foooooo

Indentation of nested pattern-matching

The new option nested-match defines the style of pattern-matchings nested in the last case of another pattern-matching. nested-match=wrap wraps the nested pattern-matching with parentheses and adds indentation, this is still the default behavior. nested-match=align vertically aligns the nested pattern-matching under the encompassing pattern-matching, for example:

let () =
  match v with
  | None -> None
  | Some x ->
  match x with
  | None -> None
  | Some x -> x

The new option cases-matching-exp-indent decides the indentation of cases right-hand sides which are match or try expressions. cases-matching-exp-indent=compact forces an indentation of 2, unless nested-match is set to align and this is the last case of the pattern matching. compact is the default behavior. cases-matching-exp-indent=normal indents as it would any other expression.

Whitelist of files to format

A new kind of configuration files is now handled by ocamlformat: .ocamlformat-enable files. If the disable option is set, an .ocamlformat-enable file can list the files that ocamlformat should format even when the disable option is set. Each line in an .ocamlformat-enable file specifies a filename relative to the directory containing the .ocamlformat-enable file.

The .ocamlformat-enable files are using the same syntax as the .ocamlformat-ignore files: lines starting with # are ignored and can be used as comments.

These new configuration files do not contradict the existing .ocamlformat-ignore files, as .ocamlformat-enable are only considered when disable is set, and .ocamlformat-ignore are only considered when disable is not set.

Disable outside detected project

The disable-outside-detected-project option is now set by default.

When the option --enable-outside-detected-project is not set, .ocamlformat files outside of the project (including the one in XDG_CONFIG_HOME) are not read. The project root of an input file is taken to be the nearest ancestor directory that contains a .git or .hg or dune-project file. If no config file is found, formatting is disabled.

Space around collection-expressions

The former option space-around-collection-expressions that was deciding whether a space should be added inside the delimiters of collection expressions (lists, arrays, records, variants) has been replaced by 4 new options: space-around-arrays, space-around-lists, space-around-records and space-around-variants, to allow a finer grain customization.

Fit-or-vertical mode for pattern matching

The break-cases option that decides the shape of pattern matching has a new value fit-or-vertical. break-cases=fit-or-vertical tries to fit all or-patterns on the same line, otherwise breaks each or-pattern (they are wrapped in other modes). For example if this set of or-patterns does not fit on a single line, we get the following output:

let ffffff =
  match foooooooooooo with
  | Aaaaaaaaaaaaaaaaa
  | Bbbbbbbbbbbbbbbbb
  | Ccccccccccccccccc
  | Ddddddddddddddddd
  | Eeeeeeeeeeeeeeeee -> foooooooooooooooooooo
  | Fffffffffffffffff -> fooooooooooooooooo

K&R style for if-then-else

The if-then-else option now has a new value k-r that uses parentheses (when necessary) to reproduce a formatting close to the K&R style. For example:

let _ =
  if b then (
    something loooooooooooooooooooooooooooooooong enough to_trigger a break ;
    this is more
  ) else if b1 then (
    something loooooooooooooooooooooooooooooooong enough to_trigger a break ;
    this is more
  ) else
    e

Breaking changes

  • the indicate-multiline-delimiters option is no longer a boolean option but now has 3 values: space, no and closing-on-separate-line that are detailed in this patch note.
  • the disable-outside-detected-project option is now set by default.
  • the default preset profile has been removed (it was equivalent to the ocamlformat profile with break-cases=fit).
  • the space-around-collection-expressions option has been replaced by 4 new options: space-around-arrays, space-around-lists, space-around-records and space-around-variants.

What's next?

We strongly encourage our users to try out the conventional preset profile, as we plan to make it the default profile in a future release. This profile's purpose is to reproduce the most commonly encountered styles, and it may be more pleasing to the eye than the current default options.

As stated previously, the break-string-literals will likely be removed in the next release and the default behavior would be newlines-and-wrap.

Credits

This release also contains many other changes and bug fixes that we cannot detail here.

We would like to thank our maintainers and contributors for this release: Jules Aguillon, Josh Berdine, Hugo Heuzard, Guillaume Petiot and Thomas Refis, and especially our industrial users Jane Street, Ahrefs and Nomadic Labs that made this work possible by funding this project and providing helpful contributions and feedback.

We would be happy to provide support for more customers, please contact us at contact@tarides.com

If you wish to get involved with OCamlFormat development or file an issue, please read the contributing guide, any contribution is welcomed.