%%% @doc Plugin provider for rebar3 -module(rebar3_format_prv). -export([init/1, do/1, format_error/1]). %% @private -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([{name, format}, {module, rebar3_format_prv}, {bare, true}, {deps, [app_discovery]}, {example, "rebar3 format"}, {opts, [{files, $f, "files", string, "List of files and directories to be formatted"}, {verify, $v, "verify", boolean, "Just verify, don't format"}, {output, $o, "output", string, "Output directory for the formatted files"}]}, {short_desc, "A rebar plugin for code formatting"}, {desc, ""}]), {ok, rebar_state:add_provider(State, Provider)}. %% @private -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, iodata()}. do(State) -> {Args, _} = rebar_state:command_parsed_args(State), Apps = case rebar_state:current_app(State) of undefined -> rebar_state:project_apps(State); AppInfo -> [AppInfo] end, Cwd = rebar_state:dir(State), Dirs = [dir_for_app(AppInfo, Cwd) || AppInfo <- Apps], Action = get_action(Args), OutputDirOpt = get_output_dir(Action, Args), Opts = maps:put(action, Action, maps:put(output_dir, OutputDirOpt, get_opts(State))), rebar_api:debug("Formatter options: ~p", [Opts]), Formatter = get_formatter(State, Opts), IgnoredFiles = get_ignored_files(State), Files = get_files(Args, State, Dirs) -- IgnoredFiles, rebar_api:debug("Found ~p files: ~p", [length(Files), Files]), case format_files(Files, Formatter) of ok -> ignore(IgnoredFiles, Formatter), {ok, State}; {error, Error} -> {error, format_error(Error)} end. %% @private -spec dir_for_app(rebar_app_info:t(), file:filename_all()) -> file:filename_all() | []. dir_for_app(AppInfo, Cwd) -> {ok, Dir} = rebar_file_utils:path_from_ancestor( rebar_app_info:dir(AppInfo), Cwd), Dir. %% @private -spec format_error(any()) -> string(). format_error({unformatted_files, Files}) -> lists:foldr(fun(File, Acc) -> [Acc, File, $\n] end, "The following files are not properly formatted:\n", Files); format_error(Reason) -> io_lib:format("Unknown Formatting Error: ~p", [Reason]). -spec get_action(proplists:proplist()) -> format | verify. get_action(Args) -> case lists:keyfind(verify, 1, Args) of {verify, true} -> verify; _ -> format end. -spec get_files(proplists:proplist(), rebar_state:t(), [file:filename_all() | []]) -> [file:filename_all()]. get_files(Args, State, Dirs) -> FilesFromArgs = [Value || {files, Value} <- Args], {Patterns, Dirs1} = case FilesFromArgs of [] -> FormatConfig = rebar_state:get(State, format, []), case proplists:get_value(files, FormatConfig, undefined) of undefined -> {["include/**/*.[he]rl", "include/**/*.app.src", "src/**/*.[he]rl", "src/**/*.app.src", "test/**/*.[he]rl", "test/**/*.app.src", "{rebar,elvis,sys}.config"], Dirs}; Wildcards -> {Wildcards, []} end; Files -> {Files, []} end, %% Special handling needed for "" (current directory) %% so that ignore-lists work in an expected way. [File || Pattern <- Patterns, File <- filelib:wildcard(Pattern)] ++ [filename:join(Dir, File) || Dir <- Dirs1, Dir =/= "", Pattern <- Patterns, File <- filelib:wildcard(Pattern, Dir)]. -spec get_ignored_files(rebar_state:t()) -> [file:filename_all()]. get_ignored_files(State) -> FormatConfig = rebar_state:get(State, format, []), Patterns = proplists:get_value(ignore, FormatConfig, []), [IgnoredFile || Pat <- Patterns, IgnoredFile <- filelib:wildcard(Pat)]. -spec get_output_dir(format | verify, proplists:proplist()) -> none | current | file:filename_all(). get_output_dir(Action, Args) -> case {lists:keyfind(output, 1, Args), Action} of {{output, OutputDir}, _} -> OutputDir; {false, format} -> current; {false, verify} -> none end. -spec get_formatter(rebar_state:t(), rebar3_formatter:opts()) -> rebar3_formatter:t(). get_formatter(State, Opts) -> Module = proplists:get_value(formatter, rebar_state:get(State, format, []), default_formatter), rebar3_formatter:new(Module, Opts, State). -spec get_opts(rebar_state:t()) -> rebar3_formatter:opts(). get_opts(State) -> proplists:get_value(options, rebar_state:get(State, format, []), #{}). -spec format_files([file:filename_all()], rebar3_formatter:t()) -> ok | {error, term()}. format_files(Files, Formatter) -> try lists:filter(fun(File) -> rebar_api:debug("Formatting ~p with ~p", [File, Formatter]), changed == rebar3_formatter:format_file(File, Formatter) end, Files) of [] -> ok; ChangedFiles -> case rebar3_formatter:action(Formatter) of format -> ok; verify -> {error, {unformatted_files, ChangedFiles}} end catch _:Error:Stack -> rebar_api:warn("Error parsing files: ~p~nStack: ~p", [Error, Stack]), {error, Error} end. %% @doc Process ignored files ignore(IgnoredFiles, Formatter) -> lists:foreach(fun(File) -> ok = rebar3_formatter:ignore(File, Formatter) end, IgnoredFiles).