src/rebar_prv_gradualizer.erl

%% @doc
%% The Rebar3 plugin for Gradualizer.
%%
%% To run Gradualizer from Rebar3, add it as a plugin in your `rebar.config':
%%
%% ```
%% {plugins, [
%%   {gradualizer, {git, "git@github.com:josefs/Gradualizer.git", {branch, "master"}}}
%% ]}.
%% '''
%%
%% @end
-module(rebar_prv_gradualizer).

-export([init/1, do/1, format_error/1]).

-define(PROVIDER, gradualizer).
-define(DEPS, [compile]).

-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
    Provider = providers:create([
            {name, ?PROVIDER},
            {module, ?MODULE},
            {bare, true},
            {deps, ?DEPS},
            {example, "rebar gradualizer"},
            {opts, [{use_beams, $b, "use_beams", boolean, "use beam files as input"}]},
            {short_desc, "typecheck the project with gradualizer"},
            {desc, ""}
    ]),
    {ok, rebar_state:add_provider(State, Provider)}.

-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
    {ok, _} = application:ensure_all_started(gradualizer),
    {Opts, _} = rebar_state:command_parsed_args(State),
    UseBeams = proplists:get_value(use_beams, Opts, false),
    code:add_pathsa(rebar_state:code_paths(State, all_deps)),
    CheckedApps = lists:map(
        fun (App) -> gradualizer_check_app(App, UseBeams) end,
        rebar_state:project_apps(State)),
    HasNok = lists:member(nok, CheckedApps),
    if
        HasNok -> {error, {?MODULE, undefined}};
        true -> {ok, State}
    end.

-spec gradualizer_check_app(rebar_app_info:t(), boolean()) -> ok | nok.
gradualizer_check_app(App, UseBeams) ->
    GOpts = rebar_app_info:get(App, gradualizer_opts, []),
    case UseBeams of
        false ->
            Files = files_to_check(App),
            gradualizer:type_check_files(Files, GOpts);
        true ->
            EBinDir = rebar_app_info:ebin_dir(App),
            gradualizer:type_check_dir(EBinDir, GOpts)
    end.

-spec files_to_check(rebar_app_info:t()) -> [file:name()].
files_to_check(App) ->
    Opts = rebar_app_info:opts(App),
    GOpts = rebar_app_info:get(App, gradualizer_opts, []),
    Include = proplists:get_value(include, GOpts, undefined),
    Exclude = proplists:get_value(exclude, GOpts, []),
    Cwd = rebar_app_info:dir(App),

    Patterns = case Include of
        undefined ->
            {SrcDirs, ExtraDirs} = resolve_src_dirs(Opts),
            lists:map(fun(File) ->
                filename:join(filename:absname(File, Cwd), "*.erl")
            end, SrcDirs ++ ExtraDirs);
        _ -> Include
    end,
    Files = lists:flatmap(fun (Pattern) ->
            filelib:wildcard(filename:absname(Pattern, Cwd))
        end, Patterns),
    ExpandedFiles = lists:flatmap(fun (Dir) ->
            case filelib:is_dir(Dir) of
                true ->
                    filelib:wildcard(filename:join(Dir, "*.{erl,beam}"));
                false ->
                    [Dir]
            end
        end, Files),
    ExpandedExclude = lists:flatmap(fun (Pattern) ->
                filelib:wildcard(filename:absname(Pattern, Cwd))
            end, Exclude),
    lists:filter(
        fun (File) ->
            not lists:member(File, ExpandedExclude)
        end, ExpandedFiles).

-spec format_error(any()) -> string().
format_error(_) ->
    "Gradualizer found errors.".

-spec resolve_src_dirs(dict:dict()) -> {[file:name()], [file:name()]}.
resolve_src_dirs(Opts) ->
    SrcDirs = rebar_dir:src_dirs(Opts, ["src"]),
    ExtraDirs = rebar_dir:extra_src_dirs(Opts, []),
    normalize_src_dirs(SrcDirs, ExtraDirs).

%% remove duplicates and make sure no directories that exist
%% in src_dirs also exist in extra_src_dirs
-spec normalize_src_dirs([file:name()], [file:name()]) -> {[file:name()], [file:name()]}.
normalize_src_dirs(SrcDirs, ExtraDirs) ->
    S = lists:usort(SrcDirs),
    E = lists:subtract(lists:usort(ExtraDirs), S),
    {S, E}.