Current section

Files

Jump to
erlfmt src erlfmt.erl
Raw

src/erlfmt.erl

%% Copyright (c) Facebook, Inc. and its affiliates.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(erlfmt).
%% API exports
-export([
main/1,
init/1,
format_file/2,
read_forms/1,
read_forms_string/2,
format_error/1,
format_error_info/1
]).
-export_type([error_info/0, out/0]).
-type error_info() :: {file:name_all(), erl_anno:location(), module(), Reason :: any()}.
-type out() :: standard_out | {path, file:name_all()} | replace.
-define(PAGE_WIDTH, 92).
-define(SCAN_START, {1, 1}).
-define(SCAN_OPTS, [text, return_comments]).
-define(COMMAND_NAME, "erlfmt").
%% escript entry point
-spec main([string()]) -> no_return().
main(Argv) ->
application:ensure_all_started(erlfmt),
Opts = erlfmt_cli:opts(),
case getopt:parse(Opts, Argv) of
{ok, {ArgOpts, _Extra}} ->
erlfmt_cli:do(ArgOpts, "erlfmt");
{error, Error} ->
io:put_chars(standard_error, [getopt:format_error(Opts, Error), "\n\n"]),
getopt:usage(Opts, "erlfmt")
end.
%% rebar3 plugin entry point
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
rebar3_fmt_prv:init(State).
%% API entry point
-spec format_file(file:name_all(), out()) -> {ok, [error_info()]} | {error, error_info()}.
format_file(FileName, Out) ->
try
{ok, Forms, Warnings} = file_read_forms(FileName),
[$\n | Formatted] = format_forms(Forms),
verify_forms(FileName, Forms, Formatted),
write_forms(FileName, Formatted, Out),
{ok, Warnings}
catch
{error, Error} -> {error, Error}
end.
%% API entry point
-spec read_forms(file:name_all()) ->
{ok, [erlfmt_parse:abstract_form()], [error_info()]} | {error, error_info()}.
read_forms(FileName) ->
try file_read_forms(FileName)
catch
{error, Error} -> {error, Error}
end.
file_read_forms(FileName) ->
case file:open(FileName, [read, {encoding, utf8}]) of
{ok, File} ->
try read_forms(erlfmt_scan:io_form(File), FileName, [], [])
after file:close(File)
end;
{error, Reason} ->
throw({error, {FileName, 0, file, Reason}})
end.
%% API entry point
-spec read_forms_string(file:name_all(), string()) ->
{ok, [erlfmt_parse:abstract_form()], [error_info()]} | {error, error_info()}.
read_forms_string(FileName, String) ->
try read_forms(erlfmt_scan:string_form(String), FileName, [], [])
catch
{error, Error} -> {error, Error}
end.
read_forms({ok, Tokens, Comments, Cont}, FileName, Acc, Warnings0) ->
{Form, Warnings} = parse_form(Tokens, Comments, FileName, Cont, Warnings0),
read_forms(erlfmt_scan:continue(Cont), FileName, [Form | Acc], Warnings);
read_forms({eof, _Loc}, _FileName, Acc, Warnings) ->
{ok, lists:reverse(Acc), lists:reverse(Warnings)};
read_forms({error, {ErrLoc, Mod, Reason}, _Loc}, FileName, _Acc, _Warnings) ->
throw({error, {FileName, ErrLoc, Mod, Reason}}).
parse_form([], _Comments, _FileName, Cont, Warnings) ->
{form_string(Cont), Warnings};
parse_form(Tokens, Comments, FileName, Cont, Warnings) ->
case erlfmt_parse:parse_form(Tokens) of
{ok, Form0} ->
{erlfmt_recomment:recomment(Form0, Comments), Warnings};
{error, {ErrLoc, Mod, Reason}} ->
Warning = {FileName, ErrLoc, Mod, Reason},
{form_string(Cont), [Warning | Warnings]}
end.
form_string(Cont) ->
{raw_string, string:trim(erlfmt_scan:last_form_string(Cont), both, "\n")}.
format_forms([{attribute, _, {atom, _, spec}, _} = Attr, {function, _, _} = Fun | Rest]) ->
[$\n, format_form(Attr), format_form(Fun) | format_forms(Rest)];
format_forms([Form | Rest]) ->
[$\n, format_form(Form) | format_forms(Rest)];
format_forms([]) ->
[].
format_form({raw_string, String}) ->
[String, $\n];
format_form(Form) ->
Doc = erlfmt_format:form_to_algebra(Form),
[erlfmt_algebra:document_render(Doc, [{page_width, ?PAGE_WIDTH}]), $\n].
verify_forms(FileName, Forms, Formatted) ->
case read_forms_string(FileName, unicode:characters_to_list(Formatted)) of
{ok, Forms2, _} ->
try equivalent_list(Forms, Forms2)
catch
{not_equivalent, Left, Right} ->
Location = try_location(Left, Right),
throw(
{error,
{FileName, Location, ?MODULE, {not_equivalent, Left, Right}}}
)
end;
{error, _} ->
throw({error, {FileName, 0, ?MODULE, could_not_reparse}})
end.
equivalent(Element, Element) ->
true;
equivalent({raw_string, RawL}, {raw_string, RawR}) ->
string:equal(RawL, RawR) orelse throw({not_equivalent, RawL, RawR});
equivalent({Type, _}, {Type, _}) ->
true;
equivalent({string, _, String} = L, {concat, _, Values} = R) ->
string_concat_equivalent(String, Values) orelse throw({not_equivalent, L, R});
equivalent({Type, _, L}, {Type, _, R}) ->
equivalent(L, R);
equivalent({Type, _, L1, L2}, {Type, _, R1, R2}) ->
equivalent(L1, R1) andalso equivalent(L2, R2);
equivalent({Type, _, L1, L2, L3}, {Type, _, R1, R2, R3}) ->
equivalent(L1, R1) andalso equivalent(L2, R2) andalso equivalent(L3, R3);
equivalent({Type, _, L1, L2, L3, L4}, {Type, _, R1, R2, R3, R4}) ->
equivalent(L1, R1) andalso
equivalent(L2, R2) andalso equivalent(L3, R3) andalso equivalent(L4, R4);
equivalent(Ls, Rs) when is_list(Ls), is_list(Rs) ->
equivalent_list(Ls, Rs);
equivalent(L, R) ->
throw({not_equivalent, L, R}).
string_concat_equivalent(String, Values) ->
string:equal(String, [Value || {string, _, Value} <- Values]).
equivalent_list([L | Ls], [R | Rs]) ->
equivalent(L, R) andalso equivalent_list(Ls, Rs);
equivalent_list([], []) ->
true;
equivalent_list(Ls, Rs) ->
throw({not_equivalent, Ls, Rs}).
try_location(Node, _) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location([Node | _], _) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, Node) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, [Node | _]) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, _) -> 0.
write_forms(_FileName, Formatted, standard_out) ->
io:put_chars(Formatted);
write_forms(FileName, Formatted, Out) ->
OutFileName = out_file(FileName, Out),
case filelib:ensure_dir(OutFileName) of
ok -> ok;
{error, Reason1} -> throw({error, {OutFileName, 0, file, Reason1}})
end,
case file:write_file(OutFileName, unicode:characters_to_binary(Formatted)) of
ok -> ok;
{error, Reason2} -> throw({error, {OutFileName, 0, file, Reason2}})
end.
out_file(FileName, replace) ->
FileName;
out_file(FileName, {path, Path}) ->
filename:join(Path, filename:basename(FileName)).
-spec format_error_info(error_info()) -> unicode:chardata().
format_error_info({FileName, Anno, Mod, Reason}) ->
io_lib:format("~ts~s: ~ts", [FileName, format_loc(Anno), Mod:format_error(Reason)]).
format_loc(0) -> "";
format_loc({Line, Col}) -> io_lib:format(":~B:~B", [Line, Col]);
format_loc(#{location := {Line, Col}}) -> io_lib:format(":~B:~B", [Line, Col]);
format_loc(Line) when is_integer(Line) -> io_lib:format(":~B", [Line]).
format_error({not_equivalent, Node1, Node2}) ->
io_lib:format(
"formatter result not equivalent. Please report this bug.~n~n~p~n~n~p",
[Node1, Node2]
);
format_error(could_not_reparse) ->
"formatter result invalid, could not reparse".