A rebar plugin for code formatting
$ rebar3 compile
Add the plugin to your rebar config:
{plugins, [rebar3_format]}
Then just call your plugin directly in an existing application:
$ rebar3 format
This will format every Erlang file under /src
by default. You can specify the directory/file to format as following:
$ rebar3 format --files src/my_subdir/*.erl
$ rebar3 format --files src/other_subdir/my_file.erl
$ rebar3 format --files test/**/*.erl
To save the formatted files in a different directory you have to pass it as a parameter:
$ rebar3 format --output formatted/
The plugin supports the following configuration options in the format
section of rebar.config
:
formatter
(module()
):- This is the module that will dictate the style in which all the code will be formatted. It must implement the
rebar3_formatter
behavior. This project itself provides 2 formatters:otp_formatter
: Based on the default formatter that comes with Erlang/OTP (erl_prettypr
), we only fixed some bugs but then respected the original format it produced in its entirety. This formatter only recognizes 2 options:paper
(pos_integer()
):- Specifies the preferred maximum number of characters on any line, including indentation.
- The default value is
80
.
ribbon
(pos_integer()
):- Specifies the preferred maximum number of characters on any line, not counting indentation.
- The default value is
56
.
default_formatter
: Our own default formatter, defining our personal criteria for how to stylize Erlang code. It admits all the options listed below.
- The default value is
default_formatter
.
- This is the module that will dictate the style in which all the code will be formatted. It must implement the
options
(#{atom() => term()}
):- A map with a list of options that should be interpreted by the chosen
formatter
. The available keys are:encoding
(none | epp:source_encoding()
):- Encoding to use when writing files.
- The default value is
none
.
paper
(pos_integer()
):- Specifies the preferred maximum number of characters on any line, including indentation.
- The default value is
100
.
ribbon
(pos_integer()
):- Specifies the preferred maximum number of characters on any line, not counting indentation.
- The default value is
90
.
break_indent
(pos_integer()
):- Specifies the preferred number of characters to use to indent a line that "breaks" from the previous one (for instance, a clause body after a clause head).
- The default value is
4
.
unquote_atoms
(boolean()
):- Specifies whether the formatter should remove quotes from atoms that don't need them (e.g.
'this_one'
) or not. - The default value is
true
, i.e. the formatter won't preserve your quotes if they're not needed, unless you explicitely ask for.
- Specifies whether the formatter should remove quotes from atoms that don't need them (e.g.
inline_attributes
(all | none | {when_over, pos_integer()}
):- Specifies the desired behavior for inlining attributes with lists, like
-export
,-export_type
and-optional_callbacks
. - When this option is
all
, the formatter will try to fit as many items in each line as permitted bypaper
andribbon
. - When the flag is
none
, the formatter will place each item in its own line. - When the flag is
{when_over, N}
the formatter will work asnone
for lists with up toN
elements, and it will inline longer lists. - The default value is
all
, i.e. always put as many functions/types on each row as possible.
- Specifies the desired behavior for inlining attributes with lists, like
inline_items
(all | none | {when_over, pos_integer()}
):- Specifies the desired behavior when the formatter needs to use multiple lines for a multi-item structure (i.e. tuple, list, map, etc.).
- NOTE: If the formatter can put all items in the same row, it will do it, regardless of this configuration.
- When this option is
all
, the formatter will try to fit as many items in each line as permitted bypaper
andribbon
. - When the flag is
none
, the formatter will place each item in its own line. - When the flag is
{when_over, N}
the formatter will work asnone
for lists with up toN
elements, and it will inline longer lists. - The default value is
{when_over, 25}
to properly accommodate large binaries or lists.
inline_clause_bodies
(boolean()
):- Specifies if clause bodies (for
case
,function
, etc. statements) should be placed in the same line as the clause heads ifpaper
andribbon
allows it or if all bodies should be placed in the next line after their clause heads. - The default value is
false
.
- Specifies if clause bodies (for
inline_expressions
(boolean()
):- Specifies if sequential expressions in a clause should be placed in the same line if
paper
andribbon
allows it or if each expression should be placed in its own line. - The default value is
false
.
- Specifies if sequential expressions in a clause should be placed in the same line if
preserve_empty_lines
(boolean()
):- Specifies if blank lines between statements should be preserved when formatting.
- Keep in mind that blank lines between clauses, between items in tuples, lists, etc, between attributes, and so on will not be affected by this configuration and therefore they'll be unconditionally removed.
- This option is only used when
inline_expressions
isfalse
. - If this option is
true
, one empty line will preserved for each group of empty lines that are placed between expressions in a clause. - The default value is
true
.
- A map with a list of options that should be interpreted by the chosen
files
([file:filename_all()]
):- List of wildcard patterns representing the files that will be formatted by default (i.e. when not using
--files
on command line). - The default value is
["src/**/*.?rl"]
- List of wildcard patterns representing the files that will be formatted by default (i.e. when not using
ignore
([file:filename_all()]
):- List of wildcard patterns representing the files that the formatter will ignore when formatting.
- Note that it will ignore the files set for formatting either with the
files
option or using--files
in the command line if they match one of the given wildcards. - You can also ignore a specific file adding the attribute
-format(ignore)
in it.
You can tweak any of the formatter options for a particular file, using the format
attribute in it, like this:
-format(#{paper => 80}).
To test the plugin just run rebar3 test
.
It will essentially run rebar3 format
inside test_app
.
Add modules with any "tricky" formatting you want to test_app/src
, and push them to github including the after
results.
The after
results can be tought as the expected output behaviour.
When we created this tool, we envisioned a workflow for teams where each member can use their preferred style for code formatting.
The idea is to take advantage of rebar3
profiles and write the following on your rebar.config
file:
%% The canonical format used when pushing code to the central repository
{format, [
{files, ["src/*.erl", "include/*.hrl", "test/*.erl"]},
{formatter, default_formatter},
{options, #{paper => 100}}
]}.
{profiles, [
{brujo, [
{format, [
{files, ["src/*.erl", "include/*.hrl", "test/*.erl"]},
{formatter, rok_formatter}, % I prefer comma-first formatting
{options, #{paper => 100}}
]}
]},
{miriam, [
{format, [
{files, ["src/*.erl", "include/*.hrl", "test/*.erl"]},
{formatter, default_formatter},
{options, #{
inline_clause_bodies => false, % she doesn't like one-liners
inline_items => all % but she does like long lists of items
}}
]}
]}
]}
Then whenever you're about to work on something, follow this ritual:
git checkout master
git checkout -b my-branch
rebar3 as brujo format
# Work on your code...
rebar3 format # This can be a git hook for commits
git commit -am "Apply my changes"
git push origin my-branch --set-upstream
Other developers do the same but using as $THEIR_NAME
instead of as brujo
.
That way each developer can read code in the way they understand it better, write code exactly how they like to write it, etc. Then push it to the central repository in a consistent way that matches the style of the rest of the project.
Through rebar3 format
, you can use other formatters that are not included in this repository. That way you can follow our proposed workflow and allow each developer to format the code with their favorite formatter using rebar3 plugins while still maintaining an unique canonical formatter when pushing to your central git repository.
You also get -format
attribute compliance (including -format ignore.
) for free, since they're respected when using any formatter.
If you want to use @old-reliable's steamroller, you just need to add the following things to your rebar.config
file:
{plugins, [rebar3_format, steamroller]}.
{format, [
{files, ["src/*.erl", "include/*.hrl"]},
{ignore, ["src/*_ignore.erl", "src/ignored_file_config.erl"]},
{formatter, sr_formatter}, %% The steamroller formatter.
{options, #{line_length => 80}}
]}.
If you want to use @whatsapp's erlfmt, you just need to add the following things to your rebar.config
file:
{plugins, [rebar3_format, erlfmt]}.
{format, [
{files, ["src/*.erl", "include/*.hrl"]},
{ignore, ["src/*_ignore.erl", "src/ignored_file_config.erl"]},
{formatter, erlfmt_formatter} %% The erlfmt formatter interface.
]}.
erlfmt_formatter
is compatible with version v0.1.0
of erlfmt
, which is the only one that's currently available at hex.pm. When a new version is published, we'll need to adjust our code since the API for erlfmt
will change (it already changed in their master
branch).
To create a new formatter, you need to implement the rebar3_formatter
behaviour. It defines just one callback:
-callback format(file:filename_all(), opts()) -> result().
That means you need to write a function that receives a filename and a map with options (some of them are specified in the rebar3_formatter
module, but you can add as many others as you want) and returns a result (either changed
or unchanged
). It's expected for your formatter to honor the predefined options as described below:
output_dir
:none
: Don't produce any output.current
: Replace files when formatting.file:filename_all()
: Drop files in this folder, preserving their current names.
encoding
:none
: Preserve/guess original encoding of files.epp:source_encoding()
: Use this encoding when parsing files.
action
:verify
: Only return the result without actually modifying any files.format
: Do format the files.
It's a good practice, although not enforced by the formatter itself to respect -format
attributes in files as the formatters provided in this repo do.
To remove the need for parsing and writing files, you can use the rebar3_ast_formatter
module/behaviour as default_formatter
and otp_formatter
do.
You can use rebar3_format from Visual Studio Code with the Erlang Formatter extension.
To contribute to rebar3_format, please refer to CONTRIBUTING.