src/rules/unused_callbacks.erl

%% @doc A rule to detect unused callbacks.
%%      <p>This rule will check all callbacks defined in a module and find
%%      those that are not used anywhere in the module itself.</p>
%%      <p>It will emit a warning if it can't find the callback's atom name
%%      being used anywhere within a module. It will NOT emit a warning if an atom
%%      named as a callback is being used, no matter for what that atom is used.</p>
%%      <p>This limitation is due to the fact that there are many ways to call
%%      a function in Erlang (particularly when dynamic calls are involved).</p>
%%      <p>The assumption is that if you define a callback for a behavior,
%%      your generic module (where the callback is defined) should call
%%      that function at some point, using the implementation provided
%%      by the specific module (the one that implements the behavior).</p>
%%      <p>To avoid this warning, remove the unused callback definition.</p>
%%
%%      <h3>Note</h3>
%%      <blockquote>
%%      For this rule to apply, it's assumed that callbacks defined for a
%%      particular behavior are only used within the same module that defines it.
%%      If you define behaviors in your project and you use their callbacks from
%%      other modules, you can add an ignore rule in rebar.config
%%      for it.
%%      </blockquote>
%% @todo [#81 + #82] Correctly handle macros
-module(unused_callbacks).

-behaviour(hank_rule).

-export([analyze/2, ignored/2]).

%% @private
-spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()].
analyze(FilesAndASTs, _Context) ->
    [Result || {File, AST} <- FilesAndASTs, Result <- analyze_file(File, AST)].

analyze_file(File, AST) ->
    CbNodes = [Node || Node <- AST, hank_utils:node_has_attrs(Node, [callback])],
    Callbacks =
        lists:map(fun(CbNode) ->
                     [CbDataArgs | _] = erl_syntax:attribute_arguments(CbNode),
                     [CbDataTuple | _] = erl_syntax:tuple_elements(CbDataArgs),
                     [CbName, CBArit] = erl_syntax:tuple_elements(CbDataTuple),
                     {hank_utils:node_line(CbNode),
                      erl_syntax:atom_value(CbName),
                      erl_syntax:integer_value(CBArit)}
                  end,
                  CbNodes),
    analyze_callbacks(File, AST, Callbacks).

analyze_callbacks(_File, _AST, []) ->
    []; %% Skip files with no callback definitions
analyze_callbacks(File, AST, Callbacks) ->
    [set_result(File, Line, Callback, Arity)
     || {Line, Callback, Arity} <- Callbacks, not is_used_callback(Callback, AST)].

is_used_callback(Callback, Nodes) ->
    lists:any(fun(Node) -> hank_utils:node_has_atom(Node, Callback) end, Nodes).

set_result(File, Line, Callback, Arity) ->
    #{file => File,
      line => Line,
      text =>
          hank_utils:format_text("Callback ~tw/~B is not used anywhere in the module",
                                 [Callback, Arity]),
      pattern => {Callback, Arity}}.

%% @doc Rule ignore specifications. Example:
%%      <pre>
%%      -hank([{unused_callbacks,
%%              [all, %% Will ignore all versions of the all callback (i.e. any arity)
%%               {just, 1} %% Will ignore just(term()) but not just() nor just(_, _) callbacks
%%              ]},
%%      </pre>
-spec ignored(hank_rule:ignore_pattern(), term()) -> boolean().
ignored({Callback, Arity}, {Callback, Arity}) ->
    true;
ignored({Callback, _Arity}, Callback) ->
    true;
ignored(_Pattern, _IgnoreSpec) ->
    false.