src/hank_rule.erl

%%% @doc Behavior for defining a rule for Hank.
-module(hank_rule).

-type t() :: module().
-type asts() :: [{file:filename(), erl_syntax:forms()}].
-type result() ::
    #{file := file:filename(),
      line := non_neg_integer(),
      text := iodata(),
      rule => t(),
      pattern => ignore_pattern()}.
-type ignore_pattern() :: undefined | tuple().
-type ignore_spec() :: {file:filename(), t() | all} | {file:filename(), t(), term()}.

-export_type([t/0, asts/0, result/0, ignore_pattern/0, ignore_spec/0]).

-callback analyze(asts(), hank_context:t()) -> [result()].
-callback ignored(ignore_pattern(), term()) -> boolean().

-export([default_rules/0]).
-export([analyze/3]).
-export([is_ignored/3]).

%% @doc The list of default rules to apply
-spec default_rules() -> [].
default_rules() ->
    [Module
     || File
            <- filelib:wildcard(
                   filename:join([code:lib_dir(rebar3_hank), "**/*.beam"])),
        Module <- [list_to_atom(filename:basename(File, ".beam"))],
        {behaviour, Behaviours} <- Module:module_info(attributes),
        lists:member(?MODULE, Behaviours)].

%% @doc Analyze the given files with the rule.
-spec analyze(t(), asts(), hank_context:t()) -> [result()].
analyze(Rule, ASTs, Context) ->
    try
        [Result#{rule => Rule} || Result <- Rule:analyze(ASTs, Context)]
    catch
        _:Error:Stack ->
            logger:error("~p:analyze/3 failed with Error ~p \nStack: ~p", [Rule, Error, Stack]),
            erlang:error(analize_error)
    end.

%% @doc Check if given rule should be ignored from results
-spec is_ignored(t(), ignore_pattern(), all | term()) -> boolean().
is_ignored(Rule, Pattern, IgnoreSpec) ->
    IgnoreSpec =:= all orelse Rule:ignored(Pattern, IgnoreSpec).