32 个版本 (18 个重大更改)

0.18.1 2024 年 8 月 1 日
0.18.0 2024 年 7 月 31 日
0.17.0 2024 年 7 月 19 日
0.15.0 2024 年 2 月 18 日
0.1.1 2021 年 11 月 29 日

#133 in 开发工具

Download history 2/week @ 2024-06-03 223/week @ 2024-06-17 16/week @ 2024-06-24 13/week @ 2024-07-01 3/week @ 2024-07-08 101/week @ 2024-07-15 50/week @ 2024-07-22 302/week @ 2024-07-29 6/week @ 2024-08-05

每月 459 次下载

MIT/Apache

265KB
7.5K SLoC

efmt

efmt hex.pm version vscode version Documentation Actions Status License

Erlang 代码格式化工具。

在线演示.

功能

  • 有偏见的格式化工具
    • 没有配置选项
    • 如果项目(例如,case 块、列表、记录)在原始代码中包含换行符,则将在多行模式下处理这些换行符
  • Emacs Erlang 模式 友好的缩进,有一些例外
  • 保留原始文本的非空白标记不变
    • 确保格式化后的代码保持相同的语义意义
  • 提供 rebar3 插件: rebar3_efmt
  • 彻底支持宏(MACRO_AND_DIRECTIVE.md

格式化示例

之前

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

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

之后

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


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

安装

使用 Rebar3

只需将以下行添加到您的 rebar.config 文件中。

{project_plugins, [rebar3_efmt]}.

然后,您可以通过运行以下命令来运行 $ rebar3 efmt

如果您想通过 rebar.config 提供默认选项,请指定一个以 efmt 为键,以 efmt 的选项为值的条目。

{efmt, [{exclude_file, "rebar.config"}]}.

请注意,rebar3_efmt 尝试自动为您的工作环境下载预构建的二进制文件(请参阅下一节)。但是,如果没有合适的版本,您需要自己构建 efmt 二进制文件。

预构建的二进制文件

Linux 和 MacOS 的预构建二进制文件可在 发布页面 上找到。

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

使用 Cargo

如果您已安装 cargo(Rust 的包管理器),可以使用以下命令安装 efmt

$ cargo install efmt
$ efmt

用法

格式化Erlang文件(例如,在上述示例中,example.erl位于当前目录)

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

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

检查原始文本与格式化文本之间的差异

$ 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

用格式化后的文件覆盖原始文件

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

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

有关其他命令行选项,请参阅帮助文档

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

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

如何避免某些区域被格式化

如果您想在输入文本中保留某些区域的外观,请使用以下注释:@efmt:off@efmt:on

foo() ->
    %% @efmt:off
    LargeList =
      [1,2,3,...,
       998,999,1000],
    %% @efmt:on

    bar(LargeList).

编辑器集成

与其他Erlang格式化器的差异

由于我对其他Erlang格式化器不太熟悉,并且erlfmt的README.md已经提供了各种格式化器之间的良好比较表,因此我只在这里描述efmterlfmt之间的差异。

请注意,在以下示例中,我使用了efmt-v0.11.0erlfmt-v1.0.0

格式化风格

我认为efmt的格式化风格与erlfmt有很大不同。在我看来,这是您决定选择哪一个时的一个重要点。如果您喜欢erlfmt的风格,那就没问题。我建议使用erlfmt。但是,如果您喜欢efmt的风格,那就欢迎使用efmt

在这里挑选所有不同点是一项艰巨的任务。所以我只给你一些格式化代码示例,希望它们能给你一个感觉。

原始代码

-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.

让我们看看erlfmtefmt是如何格式化上述代码的。

erlfmt格式化代码

$erlfmt foo.erl

-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格式化代码

$efmt foo.erl

-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.

无行宽限制

erlfmt不同,efmt不提供确保格式化代码的每一行都位于指定行宽(列)内的功能。

错误处理

erlfmt似乎会在检测到语法错误后尝试格式化代码的剩余部分。相比之下,efmt一旦检测到错误就会中断。

例如,让我们格式化以下代码。

-module(bar).

invalid_fun() ->
    : foo,
ok.

valid_fun
()->
ok.

使用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)

使用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)

宏处理

efmt尽可能地像Erlang预处理器一样处理宏。

因此,它可以涵盖广泛的复杂情况。让我们格式化以下基于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.

使用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.

使用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: '?'

格式化速度

以下基准测试比较了格式化OTP-24源分布中所有".erl"文件所需的时间。

// 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.ac.cn/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

开发阶段

erlfmt已经发布了稳定版本(v1),但efmt还没有。也许在发布v1之前,efmt的风格的一些部分可能会发生变化。

限制

有一些限制,未来不计划解决

  • 仅支持UTF-8文件
  • 不处理解析转换
    • 也就是说,如果解析转换在您的Erlang代码中引入了自定义语法,efmt可能会失败
  • 不处理-include().-include_lib().指令
    • 这些包含文件中定义的宏被展开为(虚拟的)原子。

依赖项

~7–20MB
~217K SLoC