-
Notifications
You must be signed in to change notification settings - Fork 21
/
rebar3_formatter.erl
104 lines (91 loc) · 3.33 KB
/
rebar3_formatter.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
%% @doc Automatic formatter for Erlang modules
-module(rebar3_formatter).
-export([new/3, format_file/2, ignore/2, action/1]).
-type opts() ::
#{output_dir => none | current | file:filename_all(),
encoding => none | epp:source_encoding(),
action => verify | format,
_ => _}.
-type result() :: changed | unchanged.
-type state() :: term().
-opaque t() ::
#{module := module(),
opts := opts(),
state := state()}.
-export_type([opts/0, result/0, t/0]).
%% Initialize the formatter and generate a state that will be passed in when
%% calling other callbacks
-callback init(opts(), undefined | rebar_state:t()) -> state().
%% Format a file.
%% Note that opts() are not the same as the global ones passed in on init/2.
%% These opts include per-file options specified with the -format attribute.
-callback format_file(file:filename_all(), state(), opts()) -> result().
%% @doc Build a formatter.
-spec new(module(), opts(), undefined | rebar_state:t()) -> t().
new(Module, Opts, RebarState) ->
#{module => Module,
opts => Opts,
state => Module:init(Opts, RebarState)}.
%% @doc Format a file.
%% Apply formatting rules to a file containing erlang code.
-spec format_file(file:filename_all(), t()) -> result().
format_file(File,
#{opts := Opts,
module := Module,
state := State} =
Formatter) ->
case apply_per_file_opts(File, Opts) of
ignore ->
ignore(File, Formatter),
unchanged;
FileOpts ->
Module:format_file(File, State, FileOpts)
end.
%% @doc Process an ignored file.
%% If output dir is not the current one we need to copy the files that we
%% are not formatting to it
-spec ignore(file:filename_all(), t()) -> ok.
ignore(File, #{opts := #{output_dir := OutputDir}}) when not is_atom(OutputDir) ->
OutFile =
filename:join(
filename:absname(OutputDir), File),
ok = filelib:ensure_dir(OutFile),
{ok, _} = file:copy(File, OutFile),
ok;
ignore(_, _) ->
ok.
%% @doc The action that the formatter will perform.
-spec action(t()) -> verify | format.
action(#{opts := Opts}) ->
maps:get(action, Opts, format).
%% @doc We need to use quick_parse_file/1 here because the returned format
%% is much more manageable than the one returned by parse_file/1
apply_per_file_opts(File, Opts) ->
FileOpts = attribute_opts(File) ++ comment_opts(File),
case lists:member(ignore, FileOpts) of
true ->
ignore;
false ->
MergeF = fun(Map, Acc) -> maps:merge(Acc, Map) end,
lists:foldl(MergeF, Opts, FileOpts)
end.
attribute_opts(File) ->
{ok, AST} = epp_dodger:quick_parse_file(File),
[Opt || {attribute, _, format, Opt} <- AST].
comment_opts(File) ->
AllComments =
[string:trim(Comment, leading, [$%])
|| {_, _, _, Comments} <- erl_comment_scan:file(File), Comment <- Comments],
lists:filtermap(fun parse_comment/1, AllComments).
parse_comment(" @format " ++ FormatOptsAsString) ->
try
{ok, Scanned, _} = erl_scan:string(FormatOptsAsString),
{ok, Parsed} = erl_parse:parse_exprs(Scanned),
{value, Evaluated, []} = erl_eval:exprs(Parsed, orddict:new()),
{true, Evaluated}
catch
error:_ ->
false
end;
parse_comment(_) ->
false.