Current section
Files
Jump to
Current section
Files
src/rebar3_formatter.erl
%% @doc Automatic formatter for Erlang modules
-module(rebar3_formatter).
-export([format/2, remove_line_numbers/1]).
-type opts() :: #{files => [file:filename_all()],
output_dir => undefined | string(), encoding => none | epp:source_encoding(),
paper => pos_integer(), ribbon => pos_integer(), break_indent => pos_integer(),
sub_indent => pos_integer(), remove_tabs => boolean(),
remove_trailing_spaces => boolean(), inline_items => boolean(),
inline_expressions => boolean(), preserve_empty_lines => boolean(),
newline_after_attributes => boolean()}.
-export_type([opts/0]).
%% @doc Format a file.
%% Apply formatting rules to a file containing erlang code.
%% Use <code>Opts</code> to configure the formatter.
-spec format(file:filename_all(), opts()) -> ok.
format(File, Opts) ->
AST = get_ast(File),
QuickAST = get_quick_ast(File),
Comments = get_comments(File),
FileOpts = maps:merge(Opts, get_per_file_opts(File)),
NewFile = format(File, AST, Comments, FileOpts),
case get_quick_ast(NewFile) of
QuickAST -> ok;
_ -> erlang:error({modified_ast, File, NewFile})
end.
get_ast(File) ->
case ktn_dodger:parse_file(File) of
{ok, AST} ->
case [Error || {error, Error} <- AST] of
[] -> AST;
[Error | _] -> erlang:error({cant_parse, File, Error})
end;
{error, OpenError} -> erlang:error({cant_parse, File, OpenError})
end.
get_quick_ast(File) ->
case ktn_dodger:quick_parse_file(File) of
{ok, AST} -> remove_line_numbers(AST);
{error, OpenError} -> erlang:error({cant_parse, File, OpenError})
end.
%% @doc Removes line numbers from ASTs to allow for "semantic" comparison
remove_line_numbers(AST) when is_list(AST) ->
lists:map(fun remove_line_numbers/1, AST);
remove_line_numbers(AST) when is_tuple(AST) ->
[Type, _Line | Rest] = tuple_to_list(AST),
case Type of
error -> AST;
Type -> list_to_tuple([Type, no | remove_line_numbers(Rest)])
end;
remove_line_numbers(AST) -> AST.
get_comments(File) -> erl_comment_scan:file(File).
%% @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
get_per_file_opts(File) ->
{ok, AST} = epp_dodger:quick_parse_file(File),
maps:from_list([Opt || {attribute, _, format, Opts} <- AST, Opt <- Opts]).
format(File, AST, Comments, Opts) ->
Paper = maps:get(paper, Opts, 100),
Ribbon = maps:get(ribbon, Opts, 80),
Encoding = maps:get(encoding, Opts, utf8),
BreakIndent = maps:get(break_indent, Opts, 4),
SubIndent = maps:get(sub_indent, Opts, 2),
RemoveTabs = maps:get(remove_tabs, Opts, true),
RemoveTrailingSpaces = maps:get(remove_trailing_spaces, Opts, true),
InlineItems = maps:get(inline_items, Opts, true),
InlineExpressions = maps:get(inline_expressions, Opts, true),
PreserveEmptyLines = maps:get(preserve_empty_lines, Opts, false),
NewlineAfterAttrs = maps:get(newline_after_attributes, Opts, true),
FinalFile = case maps:get(output_dir, Opts) of
undefined -> File;
OutputDir -> filename:join(filename:absname(OutputDir), File)
end,
ok = filelib:ensure_dir(FinalFile),
FormatOpts = [{paper, Paper}, {ribbon, Ribbon}, {encoding, Encoding},
{break_indent, BreakIndent}, {sub_indent, SubIndent},
{inline_items, InlineItems}, {inline_expressions, InlineExpressions},
{newline_after_attributes, NewlineAfterAttrs}],
ExtendedAST = AST ++ [{eof, 0}],
WithComments = erl_recomment:recomment_forms(erl_syntax:form_list(ExtendedAST),
Comments),
PreFormatted = rebar3_prettypr:format(WithComments,
empty_lines(InlineExpressions, PreserveEmptyLines, File),
FormatOpts),
Formatted = maybe_remove_tabs(RemoveTabs,
unicode:characters_to_binary(PreFormatted, Encoding)),
Clean = maybe_remove_trailing_spaces(RemoveTrailingSpaces, Formatted),
ok = file:write_file(FinalFile, Clean),
FinalFile.
maybe_remove_tabs(false, Formatted) -> Formatted;
maybe_remove_tabs(true, Formatted) ->
binary:replace(Formatted, <<"\t">>, <<" ">>, [global]).
maybe_remove_trailing_spaces(false, Formatted) -> Formatted;
maybe_remove_trailing_spaces(true, Formatted) ->
re:replace(Formatted, <<" +\n">>, <<"\n">>, [global, {return, binary}]).
empty_lines(true, _, _) -> [];
empty_lines(false, false, _) -> [];
empty_lines(false, true, File) ->
{ok, Data} = file:read_file(File),
List = binary:split(Data, [<<"\n">>], [global, trim]),
{ok, NonEmptyLineRe} = re:compile("\\S"),
{Res, _} = lists:foldl(fun (Line, {EmptyLines, N}) ->
case re:run(Line, NonEmptyLineRe) of
{match, _} -> {EmptyLines, N + 1};
nomatch -> {[N | EmptyLines], N + 1}
end
end,
{[], 1}, List),
lists:reverse(Res).