Current section
Files
Jump to
Current section
Files
src/hank.erl
%%% @doc The Erlang Dead Code Cleaner
-module(hank).
%% It's dynamically called through rpc:pmap/3
-ignore_xref([get_ast/1]).
-export([analyze/5]).
-export([get_ast/1]).
-type ms() :: non_neg_integer().
-type stats() ::
#{ignored := non_neg_integer(),
parsing := ms(),
analyzing => ms(),
total => ms()}.
-type parsing_style() :: parallel | sequential.
-export_type([stats/0, parsing_style/0]).
%% @doc Runs a list of rules over a list of files and returns all the
%% dead code pieces it can find.
-spec analyze([file:filename()],
[hank_rule:ignore_spec()],
[hank_rule:t()],
parsing_style(),
hank_context:t()) ->
#{results => [hank_rule:result()], stats => stats()}.
analyze(Files, IgnoredSpecsFromState, Rules, ParsingStyle, Context) ->
StartMs = erlang:monotonic_time(millisecond),
{ParsingNanos, ASTs} = timer:tc(fun() -> get_asts(Files, ParsingStyle) end),
FilesAndASTs = lists:zip(Files, ASTs),
IgnoredRulesFromAST =
[{File, IgnoredRule, IgnoredSpecs}
|| {File, AST} <- FilesAndASTs,
not lists:member({File, all, []}, IgnoredSpecsFromState),
{IgnoredRule, IgnoredSpecs} <- ignored_rules(AST, Rules)],
IgnoredRulesFromConfig =
[{File, Rule, Options}
|| {File, IgnoredRule, Options} <- IgnoredSpecsFromState,
Rule <- Rules,
IgnoredRule == all orelse IgnoredRule == Rule],
IgnoredRules = IgnoredRulesFromAST ++ IgnoredRulesFromConfig,
erlang:yield(),
{AnalyzingNanos, AllResults} =
timer:tc(fun() -> analyze(Rules, FilesAndASTs, Context) end),
{Results, Ignored} =
lists:partition(fun(#{file := File,
rule := Rule,
pattern := Pattern}) ->
IgnoredSpecs = ignored_specs(File, Rule, IgnoredRules),
not hank_rule:is_ignored(Rule, Pattern, IgnoredSpecs)
end,
AllResults),
TotalMs = erlang:monotonic_time(millisecond) - StartMs,
#{results => Results,
stats =>
#{ignored => length(Ignored),
parsing => ParsingNanos div 1000,
analyzing => AnalyzingNanos div 1000,
total => TotalMs}}.
-spec get_asts([file:filename()], parsing_style()) -> [erl_syntax:forms()].
get_asts(Files, parallel) ->
rpc:pmap({?MODULE, get_ast}, [], Files);
get_asts(Files, sequential) ->
lists:map(fun get_ast/1, Files).
%% @hidden Only used through rpc:pmap/3
-spec get_ast(file:filename()) -> erl_syntax:forms().
get_ast(File) ->
case ktn_dodger:parse_file(File, [no_fail, parse_macro_definitions]) of
{ok, AST} ->
AST;
{error, OpenError} ->
erlang:error({cant_parse, File, OpenError})
end.
ignored_rules(AST, Rules) ->
FoldFun =
fun(Node, Acc) ->
case erl_syntax:type(Node) of
attribute ->
try erl_syntax_lib:analyze_attribute(Node) of
{hank, {hank, ignore}} ->
normalize_ignored_rules(Rules) ++ Acc;
{hank, {hank, RulesToIgnore}} ->
normalize_ignored_rules(RulesToIgnore) ++ Acc;
_ ->
Acc
catch
_:_ ->
Acc
end;
_ ->
Acc
end
end,
erl_syntax_lib:fold(FoldFun, [], erl_syntax:form_list(AST)).
normalize_ignored_rules(RulesToIgnore) ->
lists:map(fun normalize_ignored_rule/1, RulesToIgnore).
normalize_ignored_rule(Rule) when is_atom(Rule) ->
{Rule, all};
normalize_ignored_rule({Rule, Specs}) when is_list(Specs) ->
{Rule, Specs};
normalize_ignored_rule({Rule, Spec}) ->
{Rule, [Spec]}.
ignored_specs(File, Rule, IgnoredRules) ->
Fun = fun ({File0, Rule0, Specs}, IgnoredSpecs)
when File =:= File0 andalso Rule =:= Rule0 ->
case is_list(Specs) of
true ->
Specs ++ IgnoredSpecs;
false ->
[Specs | IgnoredSpecs]
end;
({File0, Rule0, all}, IgnoredSpecs) when File =:= File0 andalso Rule =:= Rule0 ->
[all | IgnoredSpecs];
(_, IgnoredSpecs) ->
IgnoredSpecs
end,
lists:foldl(Fun, [], IgnoredRules).
analyze(Rules, ASTs, Context) ->
[Result || Rule <- Rules, Result <- hank_rule:analyze(Rule, ASTs, Context)].