src/rules/single_use_hrls.erl

%% @doc A rule to detect header files used in just one module.
%%      <p>To avoid this warning, include the content of the header file into
%%      the module.</p>
%%
%%      <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 some of
%%      your header files, you can add an ignore rule in rebar.config for it.
%%      </blockquote>
-module(single_use_hrls).

-behaviour(hank_rule).

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

%% @private
-spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()].
analyze(FilesAndASTs, _Context) ->
    [set_result(HeaderFile, IncludedAtFile)
     || {HeaderFile, [IncludedAtFile]} <- build_include_list(FilesAndASTs)].

set_result(HeaderFile, IncludedAtFile) ->
    #{file => HeaderFile,
      line => 0,
      text =>
          hank_utils:format_text("This header file is only included at: ~ts", [IncludedAtFile]),
      pattern => undefined}.

build_include_list(FilesAndASTs) ->
    {Files, _ASTs} = lists:unzip(FilesAndASTs),
    lists:foldl(fun({File, AST}, Acc) ->
                   lists:foldl(fun(IncludedFile, AccInner) ->
                                  AtFiles =
                                      case lists:keyfind(IncludedFile, 1, AccInner) of
                                          false ->
                                              [];
                                          {IncludedFile, IncludedAtFiles} ->
                                              IncludedAtFiles
                                      end,
                                  NewTuple = {IncludedFile, [File | AtFiles]},
                                  lists:keystore(IncludedFile, 1, AccInner, NewTuple)
                               end,
                               Acc,
                               included_files(Files, AST))
                end,
                [],
                FilesAndASTs).

included_files(Files, AST) ->
    [included_file_path(Files, IncludedFile)
     || IncludedFile <- hank_utils:attr_args_concrete(AST, include),
        is_file_included(Files, IncludedFile) =/= false].

included_file_path(Files, IncludedFile) ->
    case is_file_included(Files, IncludedFile) of
        false ->
            IncludedFile;
        IncludedFileWithPath ->
            IncludedFileWithPath
    end.

is_file_included(Files, IncludedFile) ->
    MatchFunc = fun(File) -> hank_utils:paths_match(IncludedFile, File) end,
    case lists:search(MatchFunc, Files) of
        {value, IncludedFileWithPath} ->
            IncludedFileWithPath;
        _ ->
            false
    end.

%% @doc It doesn't make sense to provide individual ignore spec support here.
%%      The rule's basic unit is already a file.
-spec ignored(hank_rule:ignore_pattern(), term()) -> false.
ignored(undefined, _IgnoreSpec) ->
    false.