%% @doc A rule to detect unused macros.
%% <p>To avoid this warning, remove the unused macros.</p>
%% Note that for header files, this rule will fail to detect some unused
%% macros. Particularly, in the case where you have an unused macro defined
%% in a header file and another macro with the same name and arity defined
%% somewhere else that is used.
%% Since determining precisely what files are included in each -include
%% attribute is not trivial, Hank will act conservatively and not make any
%% effort to verify where each macro that's used is defined.
%% So, if you have a project with multiple definitions of the same macro
%% with the same arity... well... as long as one of them is used, none of
%% them will be reported as unused.
%%
%% <h3>Note</h3>
%% <blockquote>
%% This rule assumes that hrl files will not be used outside your project.
%% If you are writing a library that requires your clients to use a macro
%% defined in some of your header files, you can add an ignore rule in
%% rebar.config for it.
%% </blockquote>
%% @todo Detect unparsable macros [https://github.com/AdRoll/rebar3_hank/issues/37]
-module(unused_macros).
-behaviour(hank_rule).
-export([analyze/2, ignored/2]).
%% @private
-spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()].
analyze(FilesAndASTs, _Context) ->
MacrosInFiles = lists:map(fun macro_usage/1, FilesAndASTs),
AllUsedMacros = [UsedMacro || #{used := Used} <- MacrosInFiles, UsedMacro <- Used],
[Result
|| #{file := File,
defined := DefinedMacros,
used := UsedMacros}
<- MacrosInFiles,
Result <- analyze(File, DefinedMacros, UsedMacros, AllUsedMacros)].
macro_usage({File, AST}) ->
FoldFun =
fun(Node, {Definitions, Usage}) ->
case erl_syntax:type(Node) of
attribute ->
case hank_utils:attr_name(Node) of
define ->
{[Node | Definitions], Usage};
ControlFlowAttr
when ControlFlowAttr == ifdef;
ControlFlowAttr == ifndef;
ControlFlowAttr == undef ->
{Definitions, [hank_utils:macro_from_control_flow_attr(Node) | Usage]};
_ ->
{Definitions, Usage}
end;
macro ->
{Definitions, [Node | Usage]};
_ ->
{Definitions, Usage}
end
end,
{MacroDefinitions, MacroUsage} =
erl_syntax_lib:fold(FoldFun, {[], []}, erl_syntax:form_list(AST)),
DefinedMacros = lists:map(fun macro_definition_name_and_line/1, MacroDefinitions),
UsedMacros = lists:map(fun macro_application_name/1, MacroUsage),
#{file => File,
defined => DefinedMacros,
used => UsedMacros}.
analyze(File, DefinedMacros, UsedMacros, AllUsedMacros) ->
case filename:extension(File) of
".erl" ->
analyze(File, DefinedMacros, UsedMacros);
".hrl" ->
analyze(File, DefinedMacros, AllUsedMacros);
_ ->
[]
end.
analyze(File, DefinedMacros, UsedMacros) ->
[result(File, MacroName, MacroArity, MacroLine)
|| {MacroName, MacroArity, MacroLine} <- DefinedMacros,
not is_member({MacroName, MacroArity}, UsedMacros)].
macro_definition_name_and_line(Node) ->
{MacroName, MacroArity} = hank_utils:macro_definition_name(Node),
Line = hank_utils:node_line(Node),
{MacroName, MacroArity, Line}.
macro_application_name(Node) ->
{hank_utils:macro_name(Node), hank_utils:macro_arity(Node)}.
result(File, Name, Arity, Line) ->
Text =
case Arity of
none ->
hank_utils:format_text("?~ts is unused", [Name]);
Arity ->
hank_utils:format_text("?~ts/~p is unused", [Name, Arity])
end,
#{file => File,
line => Line,
text => Text,
pattern => {Name, Arity}}.
%% @doc Rule ignore specifications. Example:
%% <pre>
%% -hank([{unused_macros,
%% ["ALL", %% Will ignore ?ALL, ?ALL() and ?ALL(X)
%% {"ZERO", 0}, %% Will ignore ?ZERO() but not ?ZERO(X) nor ?ZERO
%% {"ONE", 1}, %% Will ignore ?ONE(X) but not ?ONE() nor ?ONE
%% {"NONE", none} %% Will ignore ?NONE but not ?NONE(X) nor ?NONE()
%% ]},
%% </pre>
-spec ignored(hank_rule:ignore_pattern(), term()) -> boolean().
ignored({Name, Arity}, {Name, Arity}) ->
true;
ignored({Name, _Arity}, Name) ->
true;
ignored(_Pattern, _IgnoreSpec) ->
false.
is_member({MacroName, none}, UsedMacros) ->
lists:keymember(MacroName, 1, UsedMacros);
is_member(Macro, UsedMacros) ->
lists:member(Macro, UsedMacros).