%%% @doc Plugin provider for rebar3 hank
-module(rebar3_hank_prv).
-export([init/1, do/1, format_error/1]).
-define(FILES_PATTERN, "**/*.{erl,hrl,config,app.src,app.src.script}").
%% @private
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
HankProvider =
providers:create([{name, hank},
{module, rebar3_hank_prv},
{bare, true},
{deps, [app_discovery]},
{example, "rebar3 hank"},
{opts, opts()},
{short_desc, "A rebar plugin for dead code cleaning"},
{desc, ""}]),
KiwFProvider =
providers:create([{name, kiwf},
{module, rebar3_hank_prv},
{bare, true},
{deps, [app_discovery]},
{example, "rebar3 kiwf"},
{opts, opts()},
{short_desc, "An alias for rebar3 hank"},
{desc, ""}]),
{ok,
rebar_state:add_provider(
rebar_state:add_provider(State, HankProvider), KiwFProvider)}.
opts() ->
[{unused_ignores,
$u,
"unused_ignores",
boolean,
"Warn on unused ignores (default: true)."}].
%% @private
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, iodata()}.
do(State) ->
rebar_api:info("Looking for code to kill with fire...", []),
Rules = get_rules(State),
rebar_api:debug("Hank rules: ~p", [Rules]),
Context = hank_context:from_rebar_state(State),
rebar_api:debug("Hank Context: ~p", [Context]),
%% All files except those under _build or _checkouts
Files = [F || F <- filelib:wildcard(?FILES_PATTERN), not is_hidden(F)],
rebar_api:debug("Hank will use ~p files for analysis: ~p", [length(Files), Files]),
IgnoreSpecsFromState =
case proplists:get_value(ignore, rebar_state:get(State, hank, []), none) of
none ->
[];
IgnoreRules ->
[{F, Rule, Options}
|| {Wildcard, Rule, Options} <- normalize(IgnoreRules),
F <- filelib:wildcard(Wildcard)]
end,
ParsingStyle =
proplists:get_value(parsing_style, rebar_state:get(State, hank, []), parallel),
try hank:analyze(Files, IgnoreSpecsFromState, Rules, ParsingStyle, Context) of
#{results := [],
unused_ignores := UnusedIgnores,
stats := Stats} ->
instrument(Stats, UnusedIgnores, State),
{ok, State};
#{results := Results,
unused_ignores := UnusedIgnores,
stats := Stats} ->
instrument(Stats, UnusedIgnores, State),
{error, format_results(Results)}
catch
Kind:Error:Stack ->
rebar_api:warn("~p analyzing files: ~p\nStack: ~p", [Kind, Error, Stack]),
{error, format_error(Error)}
end.
instrument(#{ignored := Ignored,
parsing := Parsing,
analyzing := Analyzing,
total := Total},
UnusedIgnores,
State) ->
rebar_api:debug("Hank ignored ~p warnings", [Ignored]),
rebar_api:debug("Hank spent ~pms parsing and ~pms analyzing the system (~pms total time)",
[Parsing, Analyzing, Total]),
{Args, _} = rebar_state:command_parsed_args(State),
Verbose =
case lists:keyfind(unused_ignores, 1, Args) of
{unused_ignores, Value} ->
Value;
false ->
true % The default is to print out warnings
end,
case {Verbose, UnusedIgnores} of
{false, _} ->
ok;
{true, []} ->
ok;
{true, UnusedIgnores} ->
Msg = "The following ignore specs are no longer needed and can be removed:\n"
++ lists:flatmap(fun format_unused_ignore/1, UnusedIgnores),
rebar_api:warn(Msg, [])
end.
format_unused_ignore({File, Rule, all}) ->
io_lib:format("* ~ts: ~p~n", [File, Rule]);
format_unused_ignore({File, Rule, Specs}) ->
io_lib:format("* ~ts: ~p: ~p~n", [File, Rule, Specs]).
-spec format_results([hank_rule:result()]) -> string().
format_results(Results) ->
lists:foldr(fun(Result, Acc) -> [Acc, format_result(Result), $\n] end,
"The following pieces of code are dead and should be removed:\n",
Results).
format_result(#{file := File,
line := Line,
text := Msg}) ->
hank_utils:format_text("~ts:~tp: ~ts", [File, Line, Msg]).
%% @private
%% @doc Determines files that should be fully hidden to Hank.
is_hidden(Filename) ->
lists:any(fun is_hidden_name/1, filename:split(Filename)).
is_hidden_name(".") ->
false;
is_hidden_name("..") ->
false;
is_hidden_name("." ++ _) ->
true;
is_hidden_name("_" ++ _) ->
true;
is_hidden_name(_) ->
false.
%% @private
-spec format_error(any()) -> binary().
format_error(Reason) ->
hank_utils:format_text("~tp", [Reason]).
-spec get_rules(rebar_state:t()) -> [hank_rule:t()].
get_rules(State) ->
case proplists:get_value(rules, rebar_state:get(State, hank, []), all) of
all ->
hank_rule:default_rules();
Rules ->
Rules
end.
normalize(IgnoreRules) ->
lists:foldl(fun (WildcardRuleMaybeOpts, Acc) when is_tuple(WildcardRuleMaybeOpts) ->
normalize_rules(WildcardRuleMaybeOpts, Acc);
(Wildcard, Acc) ->
[{Wildcard, all, all} | Acc]
end,
[],
IgnoreRules).
normalize_rules({Wildcard, Rules, Options}, Acc) when is_list(Rules) ->
[{Wildcard, Rule, Options} || Rule <- Rules] ++ Acc;
normalize_rules({Wildcard, Rule, Options}, Acc) ->
normalize_rules({Wildcard, [Rule], Options}, Acc);
normalize_rules({Wildcard, Rules}, Acc) when is_list(Rules) ->
[{Wildcard, Rule, all} || Rule <- Rules] ++ Acc;
normalize_rules({Wildcard, Rule}, Acc) ->
normalize_rules({Wildcard, [Rule]}, Acc).