A rebar plugin for code formatting.
$ rebar3 compile
As #314 and #315 show, if you use OTP25 or newer, you need to enable all features in your Erlang VM when you're running the formatter. This is because
rebar3_format
useskatana_code
and, to be able to parse code containing new features,katana_code
is compiled with those features enabled. So, even if you don't use the new features yourself, you need them enabled when running the formatter on your code. To achieve that, you can set this variable on your development/CI environment:ERL_AFLAGS="-enable-feature all"
Add the plugin to your rebar config:
{project_plugins, [rebar3_format]}
Then just call your plugin directly in an existing application:
$ rebar3 format
By default, this will format every Erlang file (including hrl
s, erl
s and app.src
s) under src
, include
and test
, together with every .config
file, in your root directory and any nested Erlang apps' directories within your project. 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' --files 'include/*.hrl'
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()
):
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()
):
80
.ribbon
(pos_integer()
):
56
.default_formatter
: Our own default formatter, defining our personal criteria for how to stylize Erlang code. It admits all the options listed below.default_formatter
.options
(#{atom() => term()}
):
formatter
. The available keys are:
encoding
(none | epp:source_encoding()
):
none
.paper
(pos_integer()
):
100
.ribbon
(pos_integer()
):
90
.break_indent
(pos_integer()
):
4
.sub_indent
(pos_integer()
):
paper
clause head.when
break_indent
.unquote_atoms
(boolean()
):
'this_one'
) or not.true
, i.e. the formatter won't preserve your quotes if they're not needed, unless you explicitely ask for.truncate_strings
(boolean()
):
otp_formatter
does, turning a long string into multiple same-length strings one per row to respect paper
and ribbon
.false
, i.e. the formatter will keep the strings as it finds them.inline_attributes
(inlining()
):
-export
, -export_type
and -optional_callbacks
.inlining()
type definition below for a list of options.all
, i.e. always put as many functions/types on each row as possible.inline_fields
(inlining()
):
inlining()
type definition below for a list of options.{when_under, 3}
, i.e. always place each field in its own line, unless there is only 1 or 2 of them.inline_items
(inlining()
):
inlining()
type definition below for a list of options.{when_over, 25}
to properly accommodate large binaries or lists.inline_simple_funs
(boolean()
):
paper
and ribbon
allows it or if these simple funs should be indented as all the others.true
.inline_qualified_function_composition
(boolean()
):
module1:function1(module2:function2(...
) should stay in the same line if they fit or if the formatter should always put the internal function call in the next line.prettypr
is built (which is the tool we're using to finally print the formatted code) we can't indent these function calls only if it doesn't fit in a line, at least not without adding an extra space to the right of (
for all function applications. That's why this switch is all-or-none. You can a more detailed explanation of this behaviour here.d(f:f(g:g(h(…))))
the formatter will always write g:g(...)
in the next row, but not h(...)
nor f:f(...)
will be moved to a new row.false
.inline_clause_bodies
(boolean()
):
case
, function
, etc. statements) should be placed in the same line as the clause heads if paper
and ribbon
allows it or if all bodies should be placed in the next line after their clause heads.false
.inline_expressions
(boolean()
):
paper
and ribbon
allows it or if each expression should be placed in its own line.false
.parenthesize_infix_operations
(boolean()
):
false
.spaces_around_arguments
(boolean()
):
a_function:call("with", "arguments")
should be formatted as a_function:call( "with", "argments" )
.inline_qualified_function_composition
, we strongly recommend you to use inline_qualified_function_composition => true
if you use spaces_within_parentheses => true
.false
.spaces_around_fields
(boolean()
):
#{a => map, "with" => "fields"}
should be formatted as #{ a => map, "with" => "fields" }
.false
.preserve_empty_lines
(boolean()
):
inline_expressions
is false
.true
, one empty line will preserved for each group of empty lines that are placed between expressions in a clause.true
.parse_macro_definitions
(boolean()
):
ktn_dodger
(the module we use to parse the code) doesn't parse macro definitions by default. That's to prevent removing parentheses where they're actually meaningful in the context where the macro is used, but not in the context where it's defined.true
, the formatter will instruct ktn_dodger
to actually parse the macros.true
.false
only for the module that contain macros that would be broken otherwise.sort_arity_qualifiers
(true
| false
)
-export
-export_type
-optional_callback
false
.files
([file:filename_all()]
):
--files
on command line).["src/**/*.?rl"]
ignore
([file:filename_all()]
):
files
option or using --files
in the command line if they match one of the given wildcards.-format(ignore).
in it or the comment % @format ignore.
inlining() :: all | none | {when_over, pos_integer()} | {when_under, pos_integer()}
:
all
, the formatter will try to fit as many items in each line as permitted by paper
and ribbon
.none
, the formatter will place each item in its own line.{when_over, N}
the formatter will work as none
for lists with up to N
elements, and it will inline longer lists.{when_under, N}
the formatter will work as none
for lists with more than N
elements, and it will inline shorter lists.You can tweak any of the formatter options for a particular file, using the format
attribute in it, like this:
-format(#{paper => 80}).
You can also achieve the same effect using @format
in a comment, like this:
% @format #{paper => 80}.
We're very strict with the parsing of comments, tho. You can use multiple %
signs if you want, but you have to place the @
sign exactly one space after the last %
and you have to place the whole map with options in a single line (although you can have multiple @format
comments per file) that has to end in period (.
).
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_simple_funs => false, % and she's adamant about it
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 main
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:
{project_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:
{project_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.
{options, #{print_width => 100, ignore_pragma => true}} %% ...or no options at all.
]}.
erlfmt_formatter
is compatible with version v0.7.0
and v0.8.0
of erlfmt
, which are currently available at hex.pm.
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.
In the scripts
folder you'll find two scripts that work really well as pre and post commit git hooks, in case you want to slowly format your huge repos with a myriad of modules :)
To contribute to rebar3_format, please refer to CONTRIBUTING.