#erlang #formatter

bin+lib efmt

Erlang code formatter

11 releases

Uses new Rust 2021

0.1.1 Nov 29, 2021
0.1.0 Nov 28, 2021
0.0.9 Nov 27, 2021

#4 in #erlang

MIT/Apache

230KB
6.5K SLoC

efmt

efmt hex.pm version Documentation Actions Status License

An Erlang code formatter.

Features

  • Opinionated: only maximum line length is configurable by users
  • Emacs Erlang Mode friendly indentation
  • Preserves non-whitespace tokens of the original text as-is
    • Ensures the code after formatting keeps the same semantic meaning
  • Provides a rebar3 plugin: rebar3_efmt
  • Thorough macro support (MACRO_AND_PREPROCESS.md)

An Formatting Example

Before

-module(example).
-export(
  [fac/1]
).

fac(1) -> 1; fac(N) -> N*fac(N-1).

After

-module(example).
-export([fac/1]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

Please refer to FORMAT_RULES.md about the formatting style.

Installation

With Rebar3

Just add the following line to your rebar.config.

{plugins, [rebar3_efmt]}.

Then, you can run the $ rebar3 efmt command.

If you want to provide the default options via rebar.config, please specify an entry that has efmt as the key and efmt's options as the value.

{efmt, [{print_width, 100}]}.  % Sets the maximum line length hint to 100.

Note that rebar3_efmt tries to automatically download a pre-built binary (see the next section) for your environment. However, if there is not a suitable one, you need to build the efmt binary on your own.

Pre-built binaries

Pre-built binaries for Linux and MacOS are available in the releases page.

// An example to download the binary for Linux.
$ curl -L https://github.com/sile/efmt/releases/download/${VERSION}/efmt-${VERSION}.x86_64-unknown-linux-musl -o efmt
$ chmod +x efmt
$ ./efmt

With Cargo

If you have installed cargo (the package manager for Rust), you can install efmt with the following command:

$ cargo install efmt
$ efmt

Usage

Formats an Erlang file (assuming example.erl in the above example is located in the current directory):

$ efmt example.erl  # or `rebar3 efmt example.erl`

// You can specify multiple files.
$ efmt example.erl rebar.config ...

Checks diff between the original text and the formatted one:

$ efmt -c example.erl  # or `rebar3 efmt -c example.erl`
...
    1   1    | -module(example).
    2        |--export(
    3        |-  [fac/1]
    4        |-).
        2    |+-export([fac/1]).
    5   3    |
    6        |-fac(1) -> 1; fac(N) -> N*fac(N-1).
        4    |+fac(1) ->
        5    |+    1;
        6    |+fac(N) ->
        7    |+    N * fac(N - 1).
...

// If you omit the filename, all the Erlang-like files (i.e., `*.{erl, hrl, app.src}` and `rebar.config`)
// are included in the target (if you're in a git repository the files specified by `.gitignore` are excluded).
$ efmt -c

Overwrites the original file with the formatted one:

$ efmt -w example.erl  # or `rebar3 efmt -w example.erl`

// As with `-c` option, you can omit the filename arg.
$ emf -w

For the other command-line options, please see the help document:

// Short doc.
$ efmt -h  # or `rebar3 efmt -h`

// Long doc.
$ efmt --help  # or `rebar3 efmt --help`

Editor Integrations

TODO (contribution welcome)

Differences with other Erlang formatters

Since I'm not familiar with other Erlang formatters, and the README.md of erlfmt already provides a good comparison table among various formatters, I only describe the differences between efmt and erlfmt here.

Note that in the following examples, I used efmt-v0.1.0 and erlfmt-v1.0.0.

Formatting style

I think the formatting style of efmt is much different from erlfmt. IMO, this is a major point to decide which one should you choose. If you like the erlfmt style. It's okay. I recommend using erlfmt. But, if you like the efmt style. It's welcomed. Please use efmt.

It's hard work to pick up every difference points here. So I just give you some formatted code examples and hope they give you a sense.

Original code

-module(foo).

-spec hello(term(), integer()) -> {ok, integer()} | {error, Reason :: term()}.
hello({_, _, A, _, [B, _, C]}, D) ->
    {ok, A + B + C + D};
hello(Error, X) when not is_integer(X) ->
    {error, Error}.

Let's set --print-width (the maximum line length) to 30, and see how erlfmt and efmt format the above code if line-wrapping is inevitable.

erlfmt formatted code

$ erlfmt foo.erl --print-width 30

-module(foo).

-spec hello(
    term(), integer()
) ->
    {ok, integer()}
    | {error,
        Reason :: term()}.
hello(
    {_, _, A, _, [B, _, C]}, D
) ->
    {ok, A + B + C + D};
hello(Error, X) when
    not is_integer(X)
->
    {error, Error}.

efmt formatted code

$ efmt foo.erl --print-width 30

-module(foo).

-spec hello(term(),
            integer()) ->
          {ok, integer()} |
          {error,
           Reason :: term()}.
hello({_, _, A, _, [B, _, C]},
      D) ->
    {ok, A + B + C + D};
hello(Error, X)
  when not is_integer(X) ->
    {error, Error}.

Error handling

erlfmt seems to try formatting the remaining part of code even if it detected a syntax error. In contrast, efmt aborts once it detects an error.

For instance, let's format the following code.

-module(bar).

invalid_fun() ->
    : foo,
ok.

valid_fun
()->
ok.

Using erlfmt:

$ erlfmt bar.erl
-module(bar).

invalid_fun() ->
    : foo,
ok.

valid_fun() ->
    ok.
bar.erl:4:5: syntax error before: ':'
// `valid_fun/0` was formatted and the program exited with 0 (success)

Using efmt:

$ efmt bar.erl
[2021-11-28T11:30:06Z ERROR efmt] Failed to format "bar.erl"
    Parse failed:
    --> bar.erl:4:5
    4 |     : foo,
      |     ^ unexpected token

Error: Failed to format the following files:
- bar.erl
// The program exited with 1 (error)

Macro handling

efmt, as much as possible, processes macros as the Erlang preprocessor does.

Thus, it can cover a wide range of tricky cases. Let's format the following code which is based on a macro usage in sile/jsone/src/jsone.erl:

-module(baz).

-ifdef('OTP_RELEASE').
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
-define(CAPTURE_STACKTRACE, :__StackTrace).
-define(GET_STACKTRACE, __StackTrace).
-else.
-define(CAPTURE_STACKTRACE,).
-define(GET_STACKTRACE, erlang:get_stacktrace()).
-endif.

decode(Json, Options) ->
try
{ok, Value, Remainings} = try_decode(Json, Options),
check_decode_remainings(Remainings),
Value
catch
error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE ->
erlang:raise(error, Reason, [StackItem])
end.

Using efmt:

$ efmt baz.erl
-module(baz).

-ifdef('OTP_RELEASE').
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
-define(CAPTURE_STACKTRACE, :__StackTrace).
-define(GET_STACKTRACE, __StackTrace).
-else.
-define(CAPTURE_STACKTRACE, ).
-define(GET_STACKTRACE, erlang:get_stacktrace()).
-endif.

decode(Json, Options) ->
    try
        {ok, Value, Remainings} = try_decode(Json, Options),
        check_decode_remainings(Remainings),
        Value
    catch
        error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE->
            erlang:raise(error, Reason, [StackItem])
    end.

Using erlfmt:

$ erlfmt baz.erl
baz.erl:6:29: syntax error before: ':'
-module(baz).

-ifdef('OTP_RELEASE').
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
-define(CAPTURE_STACKTRACE, :__StackTrace).
-define(GET_STACKTRACE, __StackTrace).
-else.
-define(CAPTURE_STACKTRACE,).
-define(GET_STACKTRACE, erlang:get_stacktrace()).
-endif.

decode(Json, Options) ->
try
{ok, Value, Remainings} = try_decode(Json, Options),
check_decode_remainings(Remainings),
Value
catch
error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE ->
erlang:raise(error, Reason, [StackItem])
end.
baz.erl:19:50: syntax error before: '?'

Formatting speed

The following benchmark compares the time to format all "*.erl" files contained in the OTP-24 source distribution.

// OS and CPU spec.
$ uname -a
Linux TABLET-GC0A6KVD 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ cat /proc/cpuinfo | grep 'model name' | head -1
model name      : 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz

// Downloads OTP source code. There are 3,737 "*.erl" files.
$ wget https://erlang.org/download/otp_src_24.1.tar.gz
$ tar zxvf otp_src_24.1.tar.gz
$ cd otp_src_24.1/
$ find . -name '*.erl' | wc -l
3737

// Erlang version: Erlang/OTP 24 [erts-12.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

// erlfmt: 17.30s
$ time erlfmt (find . -name '*.erl') > /dev/null 2> /dev/null
________________________________________________________
Executed in   17.30 secs
   usr time   97.73 secs
   sys time   10.20 secs

// efmt (w/o include cache): 15.10s
$ time efmt --parallel $(find . -name '*.erl') > /dev/null 2> /dev/null
________________________________________________________
Executed in   15.10 secs
   usr time   98.83 secs
   sys time    9.67 secs

// efmt (w/ include cache): 5.84s
$ time efmt --parallel $(find . -name '*.erl') > /dev/null 2> /dev/null
________________________________________________________
Executed in    5.84 secs
   usr time   43.88 secs
   sys time    1.28 secs

Note that efmt needs to process --include and --include_lib to collect macro definitions in the included files. Once an include file is processed, efmt stores the result into a cache file under .efmt/cache/ dir. The efmt second execution in the above benchmark just reused the cached results instead of processing hole include files. So the execution time was much faster than the first execution.

Development phase

erlfmt has released the stable version (v1), but efmt hasn't. Perhaps some parts of the efmt style will change in future releases until it releases v1.

Limitations

There are some limitations that are not planned to be addressed in the future:

  • Only supports UTF-8 files
  • Doesn't process parse transforms
    • That is, if a parse transform has introduced custom syntaxes in your Erlang code, efmt could fail

Dependencies

~6–9MB
~187K SLoC