Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/sile/efmt
Erlang code formatter
https://github.com/sile/efmt
erlang formatter rust
Last synced: 5 days ago
JSON representation
Erlang code formatter
- Host: GitHub
- URL: https://github.com/sile/efmt
- Owner: sile
- License: apache-2.0
- Created: 2017-06-21T18:01:53.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2024-09-25T11:38:22.000Z (3 months ago)
- Last Synced: 2024-12-13T13:06:10.932Z (12 days ago)
- Topics: erlang, formatter, rust
- Language: Rust
- Homepage:
- Size: 3.27 MB
- Stars: 69
- Watchers: 6
- Forks: 4
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE-APACHE
Awesome Lists containing this project
README
efmt
====[![efmt](https://img.shields.io/crates/v/efmt.svg)](https://crates.io/crates/efmt)
[![hex.pm version](https://img.shields.io/hexpm/v/rebar3_efmt.svg)](https://hex.pm/packages/rebar3_efmt)
[![vscode version](https://img.shields.io/vscode-marketplace/v/sile.efmt.svg?label=vscode)](https://marketplace.visualstudio.com/items?itemName=sile.efmt)
[![Documentation](https://docs.rs/efmt/badge.svg)](https://docs.rs/efmt)
[![Actions Status](https://github.com/sile/efmt/workflows/CI/badge.svg)](https://github.com/sile/efmt/actions)
![License](https://img.shields.io/crates/l/efmt)An Erlang code formatter.
[Online demo](https://sile.github.io/efmt/examples/efmt.html).
Features
--------- An opinionated formatter
- No configuration options
- If items (e.g., `case` blocks, lists, records) contain newlines in the original code, those are processed in multi-line mode
- [Emacs Erlang Mode](https://www.erlang.org/doc/apps/tools/erlang_mode_chapter.html) friendly indentation with some exceptions
- 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](https://hex.pm/packages/rebar3_efmt)
- Thorough macro support ([MACRO_AND_DIRECTIVE.md](MACRO_AND_DIRECTIVE.md))An Formatting Example
---------------------### Before
```erlang
-module(example).
-export(
[fac/1]
).fac(1)->
1;fac(N )
-> N*fac(
N-1).
```### After
```erlang
-module(example).
-export([fac/1]).fac(1) ->
1;
fac(N) ->
N * fac(N - 1).
```Installation
------------### With [Rebar3](https://github.com/erlang/rebar3)
Just add the following line to your `rebar.config`.
```erlang
{project_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.
```erlang
{efmt, [{exclude_file, "rebar.config"}]}.
```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](https://github.com/sile/efmt/releases).
```console
// An example to download the binary for Linux.
$ VERSION=0.18.3
$ 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](https://doc.rust-lang.org/cargo/)
If you have installed `cargo` (the package manager for Rust), you can install `efmt` with the following command:
```console
$ cargo install efmt
$ efmt
```Usage
-----Formats an Erlang file (assuming `example.erl` in the above example is located in the current directory):
```console
$ 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:
```console
$ efmt -c example.erl # or `rebar3 efmt -c example.erl`
--- a/example.erl
+++ b/example.erl
@@ -1,9 +1,8 @@
-module(example).
--export(
- [fac/1]
-).
+-export([fac/1]).-fac(1)->
-1;fac(N )
--> N*fac(
-N-1).
+
+fac(1) ->
+ 1;
+fac(N) ->
+ 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:
```console
$ 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:
```console
// Short doc.
$ efmt -h # or `rebar3 efmt -h`// Long doc.
$ efmt --help # or `rebar3 efmt --help`
```### How to keep some areas from being formatted
If you want to keep the style of some areas in your input text,
please use `@efmt:off` and `@efmt:on` comments as follows:```erlang
foo() ->
%% @efmt:off
LargeList =
[1,2,3,...,
998,999,1000],
%% @efmt:onbar(LargeList).
```Editor Integrations
-------------------- Emacs: [emacs-format-all-the-code](https://github.com/lassik/emacs-format-all-the-code)
- VSCode: [extension](https://marketplace.visualstudio.com/items?itemName=sile.efmt)
- Sublime Text: [Formatter](https://packagecontrol.io/packages/Formatter)Differences with other Erlang formatters
-----------------------------------------Since I'm not familiar with other Erlang formatters, and [the README.md of `erlfmt`](https://github.com/WhatsApp/erlfmt/blob/main/README.md) 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.11.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 when you decide which one you should 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 all difference points here.
So I just give you some formatted code examples and hope they give you a sense.#### Original code
```erlang
-module(foo).-spec hello(term(), integer()) ->
{ok, integer()} | {error, Reason :: term()} |
timeout.
hello({_, _, A, _,
[B, _, C]}, D) -> {ok,
A + B +
C + D};
hello(Error, X) when not is_integer(X);
is_atom(X) ->
{error, Error};
hello(#record{foo=[_,_],
bar=#{qux := 10}}, World) ->
World.
```Let's see how `erlfmt` and `efmt` format the above code.
#### `erlfmt` formatted code
`$ erlfmt foo.erl`
```erlang
-module(foo).-spec hello(term(), integer()) ->
{ok, integer()}
| {error, Reason :: term()}
| timeout.
hello({_, _, A, _, [B, _, C]}, D) ->
{ok,
A + B +
C + D};
hello(Error, X) when
not is_integer(X);
is_atom(X)
->
{error, Error};
hello(
#record{
foo = [_, _],
bar = #{qux := 10}
},
World
) ->
World.
```#### `efmt` formatted code
`$ efmt foo.erl`
```erlang
-module(foo).-spec hello(term(), integer()) ->
{ok, integer()} |
{error, Reason :: term()} |
timeout.
hello({_,
_,
A,
_,
[B, _, C]},
D) ->
{ok, A + B +
C + D};
hello(Error, X)
when not is_integer(X);
is_atom(X) ->
{error, Error};
hello(#record{
foo = [_, _],
bar = #{qux := 10}
},
World) ->
World.
```### No line width limit
Unlike `erlfmt`, `efmt` doesn't provide a feature to ensure each line of the formatted code is within a specified line width (columns).
### 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.
```erlang
-module(bar).invalid_fun() ->
: foo,
ok.valid_fun
()->
ok.
```Using `erlfmt`:
```console
$ 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`:
```console
$ efmt bar.erl
[2021-11-28T11:30:06Z ERROR efmt] Failed to format "bar.erl"
Parse failed:
--> bar.erl:4:5
4 | : foo,
| ^ unexpected tokenError: 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](https://github.com/sile/jsone/blob/master/src/jsone.erl):
```erlang
-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`:
```console
$ 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`:
```console
$ 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.
```console
// 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: 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
```### 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
- Doesn't process `-include().` and `-include_lib().` directives
- Macros defined in those include files are expanded to (dummy) atoms.