[ANN] Release of OCamlFormat 0.9

Release of OCamlFormat 0.9

We are pleased to announce the release of OCamlFormat (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.8.

Additional dependencies

OCamlFormat now requires:

  • ocaml >= 4.06 (up from 4.04.1)
  • dune >= 1.1.1
  • octavius >= 1.2.0
  • uutf

OCamlFormat_Reason now requires:

  • ocaml >= 4.06
  • dune >= 1.1.1
  • ocaml-migrate-parsetree >= 1.0.10 (up from 1.0.6)
  • octavius >= 1.2.0
  • uutf
  • reason >= 3.2.0 (up from 1.13.4)

New preset profiles

The ocamlformat profile aims to take advantage of the strengths of a parsetree-based auto-formatter, and to limit the consequences of the weaknesses imposed by the current implementation. This is a style which optimizes for what the formatter can do best, rather than to match the style of any existing code.

General guidelines that have directed the design include:

  • Legibility, in the sense of making it as hard as possible for quick visual parsing to give the wrong interpretation, is of highest priority;
  • Whenever possible the high-level structure of the code should be obvious by looking only at the left margin, in particular, it should not be necessary to visually jump from left to right hunting for critical keywords, tokens, etc;
  • All else equal compact code is preferred as reading without scrolling is easier, so indentation or white space is avoided unless it helps legibility;
  • Attention has been given to making some syntactic gotchas visually obvious.
    ocamlformat is the new default profile.

The conventional profile aims to be as familiar and “conventional” appearing as the available options allow.

The default profile is ocamlformat with break-cases=fit. default is deprecated and will be removed in version 0.10.

OCamlFormat diff tool

ocamlformat-diff is a tool that uses OCamlFormat to apply the same formatting to compared OCaml files, so that the formatting differences between the two files are not displayed.

Note that ocamlformat-diff comes in a separate opam package and is not included in the ocamlformat package.

The file comparison is then performed by any diff backend. The options’ documentation is available through ocamlformat-diff --help. The option --diff allows you to configure the diff command that is used to compare the formatted files. The default value is the vanilla diff, but you can also use patdiff or any other similar comparison tool.

ocamlformat-diff can be integrated with git diff, as explained in the online documentation.

Formatting docstrings

Previously, the docstrings (** This is a docstring *) could only be formatted like regular comments, a new option --parse-docstrings has been added so that docstrings can be nicely formatted.

Here is a small example:

(** {1 Printers and escapes used by Cmdliner module} *)

val subst_vars : subst:(string -> string option) -> Buffer.t -> string -> string
(** [subst b ~subst s], using [b], substitutes in [s] variables of the form
    "$(doc)" by their [subst] definition. This leaves escapes and markup
    directives $(markup,...) intact.
    @raise Invalid_argument in case of illegal syntax. *)

Note that this option is disabled by default and you have to set it manually by adding --parse-docstrings to your command line or parse-docstrings=true to your .ocamlformat file.

If you get the following error message:

Error: Formatting of (** ... *) is unstable (e.g. parses as a list or not depending on the margin), please tighten up this comment in the source or disable the formatting using the option --no-parse-docstrings.

It means the original docstring cannot be formatted (e.g. because it does not comply with the odoc syntax) and you have to edit it or disable the formatting of docstrings. Of course if you think your docstring complies with the odoc syntax and there might be a bug in OCamlFormat, feel free to file an issue on github.

Print the configuration

The new --print-config flag prints the configuration determined by the environment variable, the configuration files, preset profiles and command line. Attributes are not considered. It provides the full list of options with the values they are set to, and the source of this value.

For example ocamlformat --print-config prints:

profile=ocamlformat (file .ocamlformat:1)
quiet=false (profile ocamlformat (file .ocamlformat:1))
max-iters=10 (profile ocamlformat (file .ocamlformat:1))
comment-check=true (profile ocamlformat (file .ocamlformat:1))
wrap-fun-args=true (profile ocamlformat (file .ocamlformat:1))
wrap-comments=true (file .ocamlformat:5)
type-decl=compact (profile ocamlformat (file .ocamlformat:1))
space-around-collection-expressions=false (profile ocamlformat (file .ocamlformat:1))
single-case=compact (profile ocamlformat (file .ocamlformat:1))
sequence-style=separator (profile ocamlformat (file .ocamlformat:1))
parse-docstrings=true (file .ocamlformat:4)
parens-tuple-patterns=multi-line-only (profile ocamlformat (file .ocamlformat:1))
parens-tuple=always (profile ocamlformat (file .ocamlformat:1))
parens-ite=false (profile ocamlformat (file .ocamlformat:1))
ocp-indent-compat=false (profile ocamlformat (file .ocamlformat:1))
module-item-spacing=sparse (profile ocamlformat (file .ocamlformat:1))
margin=77 (file .ocamlformat:3)
let-open=preserve (profile ocamlformat (file .ocamlformat:1))
let-binding-spacing=compact (profile ocamlformat (file .ocamlformat:1))
let-and=compact (profile ocamlformat (file .ocamlformat:1))
leading-nested-match-parens=false (profile ocamlformat (file .ocamlformat:1))
infix-precedence=indent (profile ocamlformat (file .ocamlformat:1))
indicate-nested-or-patterns=space (profile ocamlformat (file .ocamlformat:1))
indicate-multiline-delimiters=true (profile ocamlformat (file .ocamlformat:1))
if-then-else=compact (profile ocamlformat (file .ocamlformat:1))
field-space=tight (profile ocamlformat (file .ocamlformat:1))
extension-sugar=preserve (profile ocamlformat (file .ocamlformat:1))
escape-strings=preserve (profile ocamlformat (file .ocamlformat:1))
escape-chars=preserve (profile ocamlformat (file .ocamlformat:1))
doc-comments-tag-only=default (profile ocamlformat (file .ocamlformat:1))
doc-comments-padding=2 (profile ocamlformat (file .ocamlformat:1))
doc-comments=after (profile ocamlformat (file .ocamlformat:1))
disable=false (profile ocamlformat (file .ocamlformat:1))
cases-exp-indent=4 (profile ocamlformat (file .ocamlformat:1))
break-struct=force (profile ocamlformat (file .ocamlformat:1))
break-string-literals=wrap (profile ocamlformat (file .ocamlformat:1))
break-sequences=false (profile ocamlformat (file .ocamlformat:1))
break-separators=before (profile ocamlformat (file .ocamlformat:1))
break-infix-before-func=true (profile ocamlformat (file .ocamlformat:1))
break-infix=wrap (profile ocamlformat (file .ocamlformat:1))
break-fun-decl=wrap (profile ocamlformat (file .ocamlformat:1))
break-collection-expressions=fit-or-vertical (profile ocamlformat (file .ocamlformat:1))
break-cases=fit (file .ocamlformat:2)

If many input files are specified, only print the configuration for the first file. If no input file is specified, print the configuration for the root directory if specified, or for the current working directory otherwise.

Parentheses around if-then-else branches

A new option parens-ite has been added to decide whether to use parentheses around if-then-else branches that spread across multiple lines.

If this option is set, the following function:

let rec loop count a =
  if count >= self#len
  then a
  else
    let a' = f cur#get count a in
    cur#incr ();
    loop (count + 1) a'

will be formatted as:

let rec loop count a =
  if count >= self#len
  then a
  else (
    let a' = f cur#get count a in
    cur#incr ();
    loop (count + 1) a' )

Parentheses around tuple patterns

A new option parens-tuple-patterns has been added, that mimics parens-tuple but only applies to patterns, whereas parens-tuples only applies to expressions. parens-tuple-patterns=multi-line-only mode will try to skip parentheses for single-line tuple patterns,
this is the default value. parens-tuple-patterns=always always uses parentheses around tuples patterns.

For example:

(* with parens-tuple-patterns=always *)
let (a, b) = (1, 2)

(* with parens-tuple-patterns=multi-line-only *)
let a, b = (1, 2)

Single-case pattern-matching expressions

The new option single-case defines the style of pattern-matching expressions with only a single case. single-case=compact will try to format a single case on a single line, this is the default value. single-case=sparse will always break the line before a single case.

For example:

(* with single-case=compact *)
try some_irrelevant_expression
with Undefined_recursive_module _ -> true

(* with single-case=sparse *)
try some_irrelevant_expression
with
| Undefined_recursive_module _ -> true

Space around collection expressions

The new option space-around-collection-expressions decides whether to add a space
inside the delimiters of collection expressions (lists, arrays, records).

For example:

(* by default *)
type wkind = {f : 'a. 'a tag -> 'a kind}
let l = ["Nil", TCnoarg Thd; "Cons", TCarg (Ttl Thd, tcons)]

(* with space-around-collection-expressions *)
type wkind = { f : 'a. 'a tag -> 'a kind }
let l = [ "Nil", TCnoarg Thd; "Cons", TCarg (Ttl Thd, tcons) ]

Break separators

The new option break-separators decides whether to break before or after separators such as ; in list or record expressions, * in tuples or -> in arrow types. break-separators=before breaks the expressions before the separator, this is the default value.
break-separators=after breaks the expressions after the separator. break-separators=after-and-docked breaks the expressions after the separator and docks the brackets for records.

For example:

(* with break-separators=before *)
type t =
  { foooooooooooooooooooooooo: foooooooooooooooooooooooooooooooooooooooo
  ; fooooooooooooooooooooooooooooo: fooooooooooooooooooooooooooo }

(* with break-separators=after *)
type t =
  { foooooooooooooooooooooooo: foooooooooooooooooooooooooooooooooooooooo;
    fooooooooooooooooooooooooooooo: fooooooooooooooooooooooooooo }

(* with break-separators=after-and-docked *)
type t = {
  foooooooooooooooooooooooo: foooooooooooooooooooooooooooooooooooooooo;
  fooooooooooooooooooooooooooooo: fooooooooooooooooooooooooooo
}

Not breaking before bind/map operators

The new option break-infix-before-func decides whether to break infix operators
whose right arguments are anonymous functions specially. This option is set by default, if you disable it with --no-break-infix-before-func, it will not break before the operator so that the first line of the function appears docked at the end of line after the operator.

For example:

(* by default *)
f x
>>= fun y ->
g y
>>= fun () ->
f x >>= fun y -> g y >>= fun () -> f x >>= fun y -> g y >>= fun () -> y ()

(* with break-infix-before-func = false *)
f x >>= fun y ->
g y >>= fun () ->
f x >>= fun y -> g y >>= fun () -> f x >>= fun y -> g y >>= fun () -> y ()

Break toplevel cases

There is a new value for the break-cases option: toplevel, that forces top-level cases (i.e. not nested or-patterns) to break across lines, otherwise breaks naturally at the margin.

For example:

let f =
  let g = function
    | H when x y <> k -> 2
    | T | P | U -> 3
  in
  fun x g t h y u ->
    match x with
    | E -> 4
    | Z | P | M -> (
      match y with
      | O -> 5
      | P when h x -> (
          function
          | A -> 6 ) )

Number of spaces before docstrings

The new option doc-comments-padding controls how many spaces are printed before doc comments in type declarations. The default value is 2.

For example:

(* with doc-comments-padding = 2 *)
type t = {a: int  (** a *); b: int  (** b *)}

(* with doc-comments-padding = 1 *)
type t = {a: int (** a *); b: int (** b *)}

Ignore files

An .ocamlformat-ignore file specifies files that OCamlFormat should ignore. Each line in an .ocamlformat-ignore file specifies a filename relative to the directory containing the .ocamlformat-ignore file. Lines starting with # are ignored and can be used as comments.

Here is an example of such .ocamlformat-ignore file:

#This is a comment
dir2/ignore_1.ml

Tag-only docstrings

The new option doc-comments-tag-only controls the position of doc comments only containing tags. doc-comments-tag-only=default means no special treatment is done, this is the default value. doc-comments-tag-only=fit puts doc comments on the same line if it fits.

For example:

(* with doc-comments-tag-only = default *)

(** @deprecated  *)
open Module

(* with doc-comments-tag-only = fit *)

open Module (** @deprecated  *)

Fit or vertical mode for if-then-else

There is a new value for the option if-then-else: fit-or-vertical. fit-or-vertical vertically breaks all branches if they do not fit on a single line. Compared to the compact (default) value, it breaks all branches if at least one of them does not fit on a single line.

For example:

(* with if-then-else = compact *)
let _ =
  if foo then
    let a = 1 in
    let b = 2 in
    a + b
  else if foo then 12
  else 0

(* with if-then-else = fit-or-vertical *)
let _ =
  if foo then
    let a = 1 in
    let b = 2 in
    a + b
  else if foo then
    12
  else
    0

Check mode

A new --check flag has been added. It checks whether the input files already are formatted. This flag is mutually exclusive with --inplace and --output. It returns 0 if the input files are indeed already formatted, or 1 otherwise.

Break function declarations

The new option break-fun-decl controls the style for function declarations and types. break-fun-decl=wrap breaks only if necessary, this is the default value. break-fun-decl=fit-or-vertical vertically breaks arguments if they do not fit on a single line. break-fun-decl=smart is like fit-or-vertical but try to fit arguments on their line if they fit. The wrap-fun-args option now only controls the style for function calls, and no more for function declarations.

For example:

(* with break-fun-decl = wrap *)
let ffffffffffffffffffff aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb
    cccccccccccccccccccccc =
  g
  
(* with break-fun-decl = fit-or-vertical *)
let ffffffffffffffffffff
    aaaaaaaaaaaaaaaaaaaaaa
    bbbbbbbbbbbbbbbbbbbbbb
    cccccccccccccccccccccc =
  g

(* with break-fun-decl = smart *)
let ffffffffffffffffffff
    aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccc =
  g

Disable configuration in files and attributes

Two new options have been added so that .ocamlformat configuration files and attributes in OCaml files do not change the configuration. These options can be useful if you use some preset profile and you do not want attributes and .ocamlformat files to interfere with your preset configuration. --disable-conf-attrs disables the configuration in attributes, and --disable-conf-files disables .ocamlformat configuration files.

Preserve module items spacing

There is a new value for the option module-item-spacing: preserve, that will not leave open lines between one-liners of similar sorts unless there is an open line in the input.

For example the line breaks are preserved in the following code:

let cmos_rtc_seconds = 0x00
let cmos_rtc_seconds_alarm = 0x01
let cmos_rtc_minutes = 0x02

let x = o

let log_other = 0x000001
let log_cpu = 0x000002
let log_fpu = 0x000004

Breaking changes

  • When --disable-outside-detected-project is set, disable ocamlformat when no .ocamlformat file is found.
  • Files are not parsed when ocamlformat is disabled.
  • Disallow - with other input files.
  • The wrap-fun-args option now only controls the style for function calls, and no more for function declarations.
  • The default profile is now named ocamlformat.
  • The deprecated syntax for .ocamlformat files: option value is no more supported anymore and you should use the option = value syntax instead.

Miscellaneous bugfixes

  • Preserve shebang (e.g. #!/usr/bin/env ocaml) at the beginning of a file.
  • Improve the formatting when ocp-indent-compat is set.
  • UTF8 characters are now correctly printed in comments.
  • Add parentheses around a constrained any-pattern (e.g. let (_ : int) = x1).
  • Emacs: the temporary buffer is now killed.
  • Emacs: add the keybinding in tuareg’s map instead of merlin’s.
  • Lots of improvements on the comments, docstrings, attributes formatting.
  • Lots of improvements on the formatting of modules.
  • Lots of improvements in the Reason support.
  • Do not rely on the file-system to format sources.
  • The --debug mode is more user-friendly.

Credits

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

Special thanks to our maintainers and contributors for this release: Jules Aguillon, Mathieu Barbin, Josh Berdine, Jérémie Dimino, Hugo Heuzard, Ludwig Pacifici, Guillaume Petiot, Nathan Rebours and Louis Roché.

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

27 Likes

Fantastic work on ocamlformat! It’s very much shaping up now as something that more projects are switching to with their own configuration defaults. As a reminder, if you are participating in a larger project that is unable to switch due to some limitation, we would greatly appreciate an issue so that we can track it.

It’ll take some time and work, but I’m really hoping that using ocamlformat will be a suitable default for the majority of OCaml users.

4 Likes