diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | THANKS | 3 | ||||
| -rw-r--r-- | rebar.config.sample | 2 | ||||
| -rw-r--r-- | src/rebar.app.src | 1 | ||||
| -rw-r--r-- | src/rebar_app_utils.erl | 1 | ||||
| -rw-r--r-- | src/rebar_digraph.erl | 18 | ||||
| -rw-r--r-- | src/rebar_fetch.erl | 1 | ||||
| -rw-r--r-- | src/rebar_prv_dialyzer.erl | 86 | ||||
| -rw-r--r-- | src/rebar_prv_install_deps.erl | 2 | ||||
| -rw-r--r-- | src/rebar_prv_xref.erl | 297 | ||||
| -rw-r--r-- | test/mock_pkg_resource.erl | 2 | ||||
| -rw-r--r-- | test/rebar_dialyzer_SUITE.erl | 158 | ||||
| -rw-r--r-- | test/rebar_xref_SUITE.erl | 190 | 
13 files changed, 722 insertions, 40 deletions
| @@ -43,6 +43,7 @@ limit scope.  | update     | Update package index |  | upgrade    | Fetch latest version of dep |  | version    | Print current version of Erlang/OTP and rebar | +| xref       | Run cross reference analysis on the project |  ### Commands still to do @@ -128,4 +128,5 @@ Alexander Verbitsky  Andras Horvath  Drew Varner  Omar Yasin -Tristan Sloughter
\ No newline at end of file +Tristan Sloughter +Kelly McLaughlin
\ No newline at end of file diff --git a/rebar.config.sample b/rebar.config.sample index d154ac1..05e7733 100644 --- a/rebar.config.sample +++ b/rebar.config.sample @@ -166,6 +166,8 @@           {git, "git://github.com/rebar/rebar.git", {branch, "master"}}},          {rebar, "1.0.0",           {git, "git://github.com/rebar/rebar.git", {tag, "1.0.0"}}}, +        {rebar, ".*", +         {git, "git://github.com/rebar/rebar.git", {ref, "7f73b8d3650b41ffd53a199f3eda20985eda84e3"}}},          %% Dependencies can be marked as 'raw'. Rebar does not require          %% such dependencies to have a standard Erlang/OTP layout          %% which assumes the presence of either diff --git a/src/rebar.app.src b/src/rebar.app.src index 73c1eff..1230436 100644 --- a/src/rebar.app.src +++ b/src/rebar.app.src @@ -43,6 +43,7 @@                       rebar_prv_release,                       rebar_prv_version,                       rebar_prv_common_test, +                     rebar_prv_xref,                       rebar_prv_help]}          ]}   ]}. diff --git a/src/rebar_app_utils.erl b/src/rebar_app_utils.erl index dac3f0f..f991e4e 100644 --- a/src/rebar_app_utils.erl +++ b/src/rebar_app_utils.erl @@ -127,6 +127,7 @@ app_vsn(Config, AppFile) ->  %% Internal functions  %% =================================================================== +load_app_file(_State, undefined) -> {error, missing_app_file};  load_app_file(State, Filename) ->      AppFile = {app_file, Filename},      case rebar_state:get(State, {appfile, AppFile}, undefined) of diff --git a/src/rebar_digraph.erl b/src/rebar_digraph.erl index 55d7272..3f942ef 100644 --- a/src/rebar_digraph.erl +++ b/src/rebar_digraph.erl @@ -40,15 +40,15 @@ add(Graph, {PkgName, Deps}) ->      end,      lists:foreach(fun(DepName) -> -                          case DepName of -                              {Name, _Vsn} -> -                                  Name; -                              Name -> -                                  Name -                          end, -                          V3 = case digraph:vertex(Graph, Name) of +                          Name1 = case DepName of +                                      {Name, _Vsn} -> +                                          ec_cnv:to_binary(Name); +                                      Name -> +                                          ec_cnv:to_binary(Name) +                                  end, +                          V3 = case digraph:vertex(Graph, Name1) of                                     false -> -                                       digraph:add_vertex(Graph, Name); +                                       digraph:add_vertex(Graph, Name1);                                     {V2, []} ->                                         V2                                 end, @@ -112,5 +112,5 @@ names_to_apps(Names, Apps) ->  -spec find_app_by_name(atom(), [rebar_app_info:t()]) -> {ok, rebar_app_info:t()} | error.  find_app_by_name(Name, Apps) ->      ec_lists:find(fun(App) -> -                          binary_to_atom(rebar_app_info:name(App), utf8) =:= binary_to_atom(Name, utf8) +                          rebar_app_info:name(App) =:= Name                    end, Apps). diff --git a/src/rebar_fetch.erl b/src/rebar_fetch.erl index eac2d02..088a49a 100644 --- a/src/rebar_fetch.erl +++ b/src/rebar_fetch.erl @@ -46,6 +46,7 @@ download_source(AppDir, Source) ->                  code:del_path(filename:absname(filename:join(AppDir1, "ebin"))),                  ec_file:remove(filename:absname(AppDir1), [recursive]),                  ok = ec_file:copy(FromDir, filename:absname(AppDir1), [recursive]), +                true = code:add_patha(filename:join(AppDir1, "ebin")),                  true          end      catch diff --git a/src/rebar_prv_dialyzer.erl b/src/rebar_prv_dialyzer.erl index 242890a..1707535 100644 --- a/src/rebar_prv_dialyzer.erl +++ b/src/rebar_prv_dialyzer.erl @@ -67,16 +67,19 @@ do(State) ->      Apps = rebar_state:project_apps(State),      try -        {ok, State1} = update_proj_plt(State, Plt, Apps), -        succ_typings(State1, Plt, Apps) +        do(State, Plt, Apps)      catch          throw:{dialyzer_error, Error} -> -            {error, {?MODULE, {error_processing_apps, Error, Apps}}} +            {error, {?MODULE, {error_processing_apps, Error}}}; +        throw:{dialyzer_warnings, Warnings} -> +            {error, {?MODULE, {dialyzer_warnings, Warnings}}}      end.  -spec format_error(any()) -> iolist(). -format_error({error_processing_apps, Error, _Apps}) -> +format_error({error_processing_apps, Error}) ->      io_lib:format("Error in dialyzing apps: ~s", [Error]); +format_error({dialyzer_warnings, Warnings}) -> +    io_lib:format("Warnings occured running dialyzer: ~b", [Warnings]);  format_error(Reason) ->      io_lib:format("~p", [Reason]). @@ -106,11 +109,22 @@ otp_version(Release) ->      [Vsn] = binary:split(Contents, [<<$\n>>], [global, trim]),      [_ | _] = unicode:characters_to_list(Vsn). + +do(State, Plt, Apps) -> +    {PltWarnings, State1} = update_proj_plt(State, Plt, Apps), +    {Warnings, State2} = succ_typings(State1, Plt, Apps), +    case PltWarnings + Warnings of +        0 -> +            {ok, State2}; +        TotalWarnings -> +            throw({dialyzer_warnings, TotalWarnings}) +    end. +  update_proj_plt(State, Plt, Apps) ->      {Args, _} = rebar_state:command_parsed_args(State),      case proplists:get_value(update_plt, Args) of          false -> -            {ok, State}; +            {0, State};          _ ->              do_update_proj_plt(State, Plt, Apps)      end. @@ -202,6 +216,8 @@ ebin_to_info(EbinDir, AppName) ->              Modules = proplists:get_value(modules, AppDetails, []),              Files = modules_to_files(Modules, EbinDir),              {IncApps ++ DepApps, Files}; +        {error, enoent} when AppName =:= erts -> +            {[], ebin_files(EbinDir)};          _ ->              Error = io_lib:format("Could not parse ~p", [AppFile]),              throw({dialyzer_error, Error}) @@ -222,6 +238,11 @@ module_to_file(Module, EbinDir, Ext) ->              false      end. +ebin_files(EbinDir) -> +    Wildcard = "*" ++ code:objfile_extension(), +    [filename:join(EbinDir, File) || +     File <- filelib:wildcard(Wildcard, EbinDir)]. +  read_plt(_State, Plt) ->      case dialyzer:plt_info(Plt) of          {ok, Info} -> @@ -238,26 +259,28 @@ check_plt(State, Plt, OldList, FilesList) ->      Old = sets:from_list(OldList),      Files = sets:from_list(FilesList),      Remove = sets:subtract(Old, Files), -    {ok, State1} = remove_plt(State, Plt, sets:to_list(Remove)), +    {RemWarnings, State1} = remove_plt(State, Plt, sets:to_list(Remove)),      Check = sets:intersection(Files, Old), -    {ok, State2} = check_plt(State1, Plt, sets:to_list(Check)), +    {CheckWarnings, State2} = check_plt(State1, Plt, sets:to_list(Check)),      Add = sets:subtract(Files, Old), -    add_plt(State2, Plt, sets:to_list(Add)). +    {AddWarnings, State3} = add_plt(State2, Plt, sets:to_list(Add)), +    ?DEBUG("~p", [[RemWarnings, CheckWarnings, AddWarnings]]), +    {RemWarnings + CheckWarnings + AddWarnings, State3}.  remove_plt(State, _Plt, []) -> -    {ok, State}; +    {0, State};  remove_plt(State, Plt, Files) ->      ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),      run_plt(State, Plt, plt_remove, Files).  check_plt(State, _Plt, []) -> -    {ok, State}; +    {0, State};  check_plt(State, Plt, Files) ->      ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),      run_plt(State, Plt, plt_check, Files).  add_plt(State, _Plt, []) -> -    {ok, State}; +    {0, State};  add_plt(State, Plt, Files) ->      ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),      run_plt(State, Plt, plt_add, Files). @@ -274,12 +297,13 @@ run_plt(State, Plt, Analysis, Files) ->  build_proj_plt(State, Plt, Files) ->      BasePlt = get_base_plt_location(State),      BaseFiles = get_base_plt_files(State), -    {ok, State1} = update_base_plt(State, BasePlt, BaseFiles), +    {BaseWarnings, State1} = update_base_plt(State, BasePlt, BaseFiles),      ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),      _ = filelib:ensure_dir(Plt),      case file:copy(BasePlt, Plt) of          {ok, _} -> -            check_plt(State1, Plt, BaseFiles, Files); +            {CheckWarnings, State2} = check_plt(State1, Plt, BaseFiles, Files), +            {BaseWarnings + CheckWarnings, State2};          {error, Reason} ->              Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",                                    [BasePlt, Plt, file:format_error(Reason)]), @@ -353,22 +377,28 @@ app_to_files(App) ->      Files.  run_dialyzer(State, Opts) -> -    Warnings = rebar_state:get(State, dialyzer_warnings, default_warnings()), -    Opts2 = [{warnings, Warnings} | Opts], -    _ = [?CONSOLE("~s", [format_warning(Warning)]) -         || Warning <- dialyzer:run(Opts2)], -    {ok, State}. - -format_warning(Warning) -> -    string:strip(dialyzer_format_warning(Warning), right, $\n). - -dialyzer_format_warning(Warning) -> -    case dialyzer:format_warning(Warning) of -        ":0: " ++ Warning2 -> -            Warning2; +    WarningsList = rebar_state:get(State, dialyzer_warnings, default_warnings()), +    Opts2 = [{warnings, WarningsList} | Opts], +    {Unknowns, Warnings} = format_warnings(dialyzer:run(Opts2)), +    _ = [?CONSOLE("~s", [Unknown]) || Unknown <- Unknowns], +    _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings], +    {length(Warnings), State}. + +format_warnings(Warnings) -> +    format_warnings(Warnings, [], []). + +format_warnings([Warning | Rest], Unknowns, Warnings) -> +    case dialyzer:format_warning(Warning, fullpath) of +        ":0: " ++ Unknown -> +            format_warnings(Rest, [strip(Unknown) | Unknowns], Warnings);          Warning2 -> -            Warning2 -    end. +            format_warnings(Rest, Unknowns, [strip(Warning2) | Warnings]) +    end; +format_warnings([], Unknowns, Warnings) -> +    {Unknowns, Warnings}. + +strip(Warning) -> +    string:strip(Warning, right, $\n).  default_warnings() ->      [error_handling, diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl index fc876bd..025d32a 100644 --- a/src/rebar_prv_install_deps.erl +++ b/src/rebar_prv_install_deps.erl @@ -226,7 +226,7 @@ package_to_app(DepsDir, Packages, {Name, Vsn}) ->          error ->              {error, missing_package};          {ok, P} -> -            PkgDeps = [{atom_to_binary(PkgName,utf8), list_to_binary(PkgVsn)} +            PkgDeps = [{PkgName, PkgVsn}                         || {PkgName,PkgVsn} <- proplists:get_value(<<"deps">>, P, [])],              Link = proplists:get_value(<<"link">>, P, ""),              {ok, AppInfo} = rebar_app_info:new(Name, Vsn), diff --git a/src/rebar_prv_xref.erl b/src/rebar_prv_xref.erl new file mode 100644 index 0000000..49ae70e --- /dev/null +++ b/src/rebar_prv_xref.erl @@ -0,0 +1,297 @@ +%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ts=4 sw=4 et + +-module(rebar_prv_xref). + +-behaviour(provider). + +-export([init/1, +         do/1, +         format_error/1]). + +-include("rebar.hrl"). + +-define(PROVIDER, xref). +-define(DEPS, [compile]). +-define(SUPPORTED_XREFS, [undefined_function_calls, undefined_functions, +                          locals_not_used, exports_not_used, +                          deprecated_function_calls, deprecated_functions]). + +%% =================================================================== +%% Public API +%% =================================================================== + +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> +    Provider = providers:create([{name, ?PROVIDER}, +                                 {module, ?MODULE}, +                                 {deps, ?DEPS}, +                                 {bare, false}, +                                 {example, "rebar3 xref"}, +                                 {short_desc, short_desc()}, +                                 {desc, desc()}]), +    State1 = rebar_state:add_provider(State, Provider), +    {ok, State1}. + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. +do(State) -> +    {OriginalPath, XrefChecks} = prepare(State), + +    %% Run xref checks +    ?INFO("Running cross reference analysis...", []), +    XrefResults = xref_checks(XrefChecks), + +    %% Run custom queries +    QueryChecks = rebar_state:get(State, xref_queries, []), +    QueryResults = lists:foldl(fun check_query/2, [], QueryChecks), + +    ok = cleanup(OriginalPath), + +    case XrefResults =:= [] andalso QueryResults =:= [] of +        true -> +            {ok, State}; +        false -> +            {error, {?MODULE, {xref_issues, XrefResults, QueryResults}}} +    end. + +-spec format_error(any()) -> iolist(). +format_error({xref_issues, XrefResults, QueryResults}) -> +    lists:flatten(display_results(XrefResults, QueryResults)); +format_error(Reason) -> +    io_lib:format("~p", [Reason]). + +%% =================================================================== +%% Internal functions +%% =================================================================== + +short_desc() -> +    "Run cross reference analysis". + +desc() -> +    io_lib:format( +      "~s~n" +      "~n" +      "Valid rebar.config options:~n" +      "  ~p~n" +      "  ~p~n" +      "  ~p~n" +      "  ~p~n", +      [short_desc(), +       {xref_warnings, false}, +       {xref_extra_paths,[]}, +       {xref_checks, [undefined_function_calls, undefined_functions, +                      locals_not_used, exports_not_used, +                      deprecated_function_calls, deprecated_functions]}, +       {xref_queries, +        [{"(xc - uc) || (xu - x - b" +          " - (\"mod\":\".*foo\"/\"4\"))",[]}]} +      ]). + +-spec prepare(rebar_state:t()) -> list(atom()). +prepare(State) -> +    {ok, _} = xref:start(xref), +    ok = xref:set_library_path(xref, code_path(State)), + +    xref:set_default(xref, [{warnings, +                             rebar_state:get(State, xref_warnings, false)}, +                            {verbose, rebar_log:is_verbose(State)}]), + +    [{ok, _} = xref:add_directory(xref, rebar_app_info:ebin_dir(App)) +     || App <- rebar_state:project_apps(State)], + +    %% Save the code path prior to doing any further code path +    %% manipulation +    OriginalPath = code:get_path(), +    true = code:add_path(rebar_dir:ebin_dir()), + +    %% Get list of xref checks we want to run +    ConfXrefChecks = rebar_state:get(State, xref_checks, +                                     [exports_not_used, +                                      undefined_function_calls]), + +    XrefChecks = sets:to_list(sets:intersection( +                                sets:from_list(?SUPPORTED_XREFS), +                                sets:from_list(ConfXrefChecks))), +    {OriginalPath, XrefChecks}. + +cleanup(Path) -> +    %% Restore the code path using the provided path +    true = rebar_utils:cleanup_code_path(Path), + +    %% Stop xref +    stopped = xref:stop(xref), +    ok. + +xref_checks(XrefChecks) -> +    lists:foldl(fun run_xref_check/2, [], XrefChecks). + +run_xref_check(XrefCheck, Acc) -> +    {ok, Results} = xref:analyze(xref, XrefCheck), +    case filter_xref_results(XrefCheck, Results) of +        [] -> +            Acc; +        FilterResult -> +            [{XrefCheck, FilterResult} | Acc] +    end. + +check_query({Query, Value}, Acc) -> +    {ok, Answer} = xref:q(xref, Query), +    case Answer =:= Value of +        false -> +            [{Query, Value, Answer} | Acc]; +        _     -> +            Acc +    end. + +code_path(State) -> +    [P || P <- code:get_path() ++ +              rebar_state:get(State, xref_extra_paths, []), +          filelib:is_dir(P)]. + +%% Ignore behaviour functions, and explicitly marked functions +%% +%% Functions can be ignored by using +%% -ignore_xref([{F, A}, {M, F, A}...]). +get_xref_ignorelist(Mod, XrefCheck) -> +    %% Get ignore_xref attribute and combine them in one list +    Attributes = +        try +            Mod:module_info(attributes) +        catch +            _Class:_Error -> [] +        end, + +    IgnoreXref = keyall(ignore_xref, Attributes), + +    BehaviourCallbacks = get_behaviour_callbacks(XrefCheck, Attributes), + +    %% And create a flat {M,F,A} list +    lists:foldl( +      fun({F, A}, Acc) -> [{Mod,F,A} | Acc]; +         ({M, F, A}, Acc) -> [{M,F,A} | Acc] +      end, [], lists:flatten([IgnoreXref, BehaviourCallbacks])). + +keyall(Key, List) -> +    lists:flatmap(fun({K, L}) when Key =:= K -> L; (_) -> [] end, List). + +get_behaviour_callbacks(exports_not_used, Attributes) -> +    [B:behaviour_info(callbacks) || B <- keyall(behaviour, Attributes)]; +get_behaviour_callbacks(_XrefCheck, _Attributes) -> +    []. + +parse_xref_result({_, MFAt}) -> MFAt; +parse_xref_result(MFAt) -> MFAt. + +filter_xref_results(XrefCheck, XrefResults) -> +    SearchModules = lists:usort( +                      lists:map( +                        fun({Mt,_Ft,_At}) -> Mt; +                           ({{Ms,_Fs,_As},{_Mt,_Ft,_At}}) -> Ms; +                           (_) -> undefined +                        end, XrefResults)), + +    Ignores = lists:flatmap(fun(Module) -> +                                    get_xref_ignorelist(Module, XrefCheck) +                            end, SearchModules), + +    [Result || Result <- XrefResults, +               not lists:member(parse_xref_result(Result), Ignores)]. + +display_results(XrefResults, QueryResults) -> +    [lists:map(fun display_xref_results_for_type/1, XrefResults), +     lists:map(fun display_query_result/1, QueryResults)]. + +display_query_result({Query, Answer, Value}) -> +    io_lib:format("Query ~s~n answer ~p~n did not match ~p~n", +                  [Query, Answer, Value]). + +display_xref_results_for_type({Type, XrefResults}) -> +    lists:map(display_xref_result_fun(Type), XrefResults). + +display_xref_result_fun(Type) -> +    fun(XrefResult) -> +            {Source, SMFA, TMFA} = +                case XrefResult of +                    {MFASource, MFATarget} -> +                        {format_mfa_source(MFASource), +                         format_mfa(MFASource), +                         format_mfa(MFATarget)}; +                    MFATarget -> +                        {format_mfa_source(MFATarget), +                         format_mfa(MFATarget), +                         undefined} +                end, +            case Type of +                undefined_function_calls -> +                    io_lib:format("~sWarning: ~s calls undefined function ~s (Xref)\n", +                                  [Source, SMFA, TMFA]); +                undefined_functions -> +                    io_lib:format("~sWarning: ~s is undefined function (Xref)\n", +                                  [Source, SMFA]); +                locals_not_used -> +                    io_lib:format("~sWarning: ~s is unused local function (Xref)\n", +                                  [Source, SMFA]); +                exports_not_used -> +                    io_lib:format("~sWarning: ~s is unused export (Xref)\n", +                                  [Source, SMFA]); +                deprecated_function_calls -> +                    io_lib:format("~sWarning: ~s calls deprecated function ~s (Xref)\n", +                                  [Source, SMFA, TMFA]); +                deprecated_functions -> +                    io_lib:format("~sWarning: ~s is deprecated function (Xref)\n", +                                  [Source, SMFA]); +                Other -> +                    io_lib:format("~sWarning: ~s - ~s xref check: ~s (Xref)\n", +                                  [Source, SMFA, TMFA, Other]) +            end +    end. + +format_mfa({M, F, A}) -> +    ?FMT("~s:~s/~w", [M, F, A]). + +format_mfa_source(MFA) -> +    case find_mfa_source(MFA) of +        {module_not_found, function_not_found} -> ""; +        {Source, function_not_found} -> ?FMT("~s: ", [Source]); +        {Source, Line} -> ?FMT("~s:~w: ", [Source, Line]) +    end. + +%% +%% Extract an element from a tuple, or undefined if N > tuple size +%% +safe_element(N, Tuple) -> +    case catch(element(N, Tuple)) of +        {'EXIT', {badarg, _}} -> +            undefined; +        Value -> +            Value +    end. + +%% +%% Given a MFA, find the file and LOC where it's defined. Note that +%% xref doesn't work if there is no abstract_code, so we can avoid +%% being too paranoid here. +%% +find_mfa_source({M, F, A}) -> +    case code:get_object_code(M) of +        error -> {module_not_found, function_not_found}; +        {M, Bin, _} -> find_function_source(M,F,A,Bin) +    end. + +find_function_source(M, F, A, Bin) -> +    AbstractCode = beam_lib:chunks(Bin, [abstract_code]), +    {ok, {M, [{abstract_code, {raw_abstract_v1, Code}}]}} = AbstractCode, +    %% Extract the original source filename from the abstract code +    [{attribute, 1, file, {Source, _}} | _] = Code, +    %% Extract the line number for a given function def +    Fn = [E || E <- Code, +               safe_element(1, E) == function, +               safe_element(3, E) == F, +               safe_element(4, E) == A], +    case Fn of +        [{function, Line, F, _, _}] -> {Source, Line}; +        %% do not crash if functions are exported, even though they +        %% are not in the source. +        %% parameterized modules add new/1 and instance/1 for example. +        [] -> {Source, function_not_found} +    end. diff --git a/test/mock_pkg_resource.erl b/test/mock_pkg_resource.erl index 502e184..ab2c0d3 100644 --- a/test/mock_pkg_resource.erl +++ b/test/mock_pkg_resource.erl @@ -78,7 +78,7 @@ mock_download(Opts) ->              App = binary_to_list(AppBin),              filelib:ensure_dir(Dir),              AppDeps = proplists:get_value({App,Vsn}, Deps, []), -            {ok, AppInfo} = rebar_test_utils:create_app( +            {ok, AppInfo} = rebar_test_utils:create_empty_app(                  Dir, App, Vsn,                  [element(1,D) || D  <- AppDeps]              ), diff --git a/test/rebar_dialyzer_SUITE.erl b/test/rebar_dialyzer_SUITE.erl new file mode 100644 index 0000000..724d8f0 --- /dev/null +++ b/test/rebar_dialyzer_SUITE.erl @@ -0,0 +1,158 @@ +-module(rebar_dialyzer_SUITE). + +-export([suite/0, +         init_per_suite/1, +         end_per_suite/1, +         init_per_testcase/2, +         all/0, +         update_base_plt/1, +         update_app_plt/1, +         build_release_plt/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("kernel/include/file.hrl"). + +suite() -> +    []. + +init_per_suite(Config) -> +    Config. + +end_per_suite(_Config) -> +    ok. + +init_per_testcase(Testcase, Config) -> +    PrivDir = ?config(priv_dir, Config), +    Prefix = ec_cnv:to_list(Testcase), +    Plt = filename:join(PrivDir,  Prefix ++ ".project.plt"), +    BasePlt = Prefix ++ "base.plt", +    RebarConfig = [{dialyzer_plt, Plt}, +                   {dialyzer_base_plt, BasePlt}, +                   {dialyzer_base_plt_dir, PrivDir}, +                   {dialyzer_base_plt_apps, [erts]}], +    [{plt, Plt}, +     {base_plt, filename:join(PrivDir, BasePlt)}, +     {rebar_config, RebarConfig} | +     rebar_test_utils:init_rebar_state(Config)]. + +all() -> +    [update_base_plt, update_app_plt, build_release_plt]. + +update_base_plt(Config) -> +    AppDir = ?config(apps, Config), +    RebarConfig = ?config(rebar_config, Config), +    BasePlt = ?config(base_plt, Config), +    Plt = ?config(plt, Config), + +    Name = rebar_test_utils:create_random_name("app1_"), +    Vsn = rebar_test_utils:create_random_vsn(), +    rebar_test_utils:create_app(AppDir, Name, Vsn, [erts]), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name}]}), + +    ErtsFiles = erts_files(), + +    {ok, BasePltFiles} = plt_files(BasePlt), +    ?assertEqual(ErtsFiles, BasePltFiles), + +    alter_plt(BasePlt), +    ok = file:delete(Plt), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name}]}), + +    {ok, BasePltFiles2} = plt_files(BasePlt), +    ?assertEqual(ErtsFiles, BasePltFiles2), + +    {ok, PltFiles} = plt_files(Plt), +    ?assertEqual(ErtsFiles, PltFiles). + + +update_app_plt(Config) -> +    AppDir = ?config(apps, Config), +    RebarConfig = ?config(rebar_config, Config), +    Plt = ?config(plt, Config), + +    Name = rebar_test_utils:create_random_name("app1_"), +    Vsn = rebar_test_utils:create_random_vsn(), +    rebar_test_utils:create_app(AppDir, Name, Vsn, [erts]), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name}]}), + +    ErtsFiles = erts_files(), + +    {ok, PltFiles} = plt_files(Plt), +    ?assertEqual(ErtsFiles, PltFiles), + +    alter_plt(Plt), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name}]}), + +    {ok, PltFiles2} = plt_files(Plt), +    ?assertEqual(ErtsFiles, PltFiles2), + +    ok = file:delete(Plt), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name}]}), + +    {ok, PltFiles3} = plt_files(Plt), +    ?assertEqual(ErtsFiles, PltFiles3). + +build_release_plt(Config) -> +    AppDir = ?config(apps, Config), +    RebarConfig = ?config(rebar_config, Config), +    BasePlt = ?config(base_plt, Config), +    Plt = ?config(plt, Config), + +    Name1 = rebar_test_utils:create_random_name("relapp1_"), +    Vsn1 = rebar_test_utils:create_random_vsn(), +    rebar_test_utils:create_app(filename:join([AppDir,Name1]), Name1, Vsn1, +                                [erts]), +    Name2 = rebar_test_utils:create_random_name("relapp2_"), +    Vsn2 = rebar_test_utils:create_random_vsn(), +    rebar_test_utils:create_app(filename:join([AppDir,Name2]), Name2, Vsn2, +                                [erts, ec_cnv:to_atom(Name1)]), + +    rebar_test_utils:run_and_check(Config, RebarConfig, ["dialyzer"], +                                   {ok, [{app, Name1}, {app, Name2}]}), + +    ErtsFiles = erts_files(), + +    {ok, BasePltFiles} = plt_files(BasePlt), +    ?assertEqual(ErtsFiles, BasePltFiles), + +    {ok, PltFiles} = plt_files(Plt), +    ?assertEqual(ErtsFiles, PltFiles). + +%% Helpers + +erts_files() -> +    ErtsDir = code:lib_dir(erts, ebin), +    ErtsBeams = filelib:wildcard("*.beam", ErtsDir), +    ErtsFiles = lists:map(fun(Beam) -> filename:join(ErtsDir, Beam) end, +                          ErtsBeams), +    lists:sort(ErtsFiles). + +plt_files(Plt) -> +    case dialyzer:plt_info(Plt) of +        {ok, Info} -> +            Files = proplists:get_value(files, Info), +            {ok, lists:sort(Files)}; +        Other -> +            Other +    end. + +alter_plt(Plt) -> +    {ok, Files} = plt_files(Plt), +    _ = dialyzer:run([{analysis_type, plt_remove}, +                      {init_plt, Plt}, +                      {files, [hd(Files)]}]), +    _ = dialyzer:run([{analysis_type, plt_add}, +                      {init_plt, Plt}, +                      {files, [code:which(dialyzer)]}]), +    ok. diff --git a/test/rebar_xref_SUITE.erl b/test/rebar_xref_SUITE.erl new file mode 100644 index 0000000..fde8c8f --- /dev/null +++ b/test/rebar_xref_SUITE.erl @@ -0,0 +1,190 @@ +%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ts=4 sw=4 et +-module(rebar_xref_SUITE). + +-export([suite/0, +         init_per_suite/1, +         end_per_suite/1, +         init_per_testcase/2, +         end_per_testcase/2, +         all/0, +         xref_test/1, +         xref_ignore_test/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("kernel/include/file.hrl"). + +%% =================================================================== +%% common_test callbacks +%% =================================================================== + +suite() -> +    []. + +init_per_suite(Config) -> +    Config. + +end_per_suite(_Config) -> +    ok. + +init_per_testcase(Case, Config) -> +    UpdConfig = rebar_test_utils:init_rebar_state(Config), +    AppDir = ?config(apps, UpdConfig), +    {ok, OrigDir} = file:get_cwd(), +    file:set_cwd(AppDir), +    Name = rebar_test_utils:create_random_name("xrefapp_"), +    Vsn = rebar_test_utils:create_random_vsn(), +    rebar_test_utils:create_empty_app(AppDir, Name, Vsn, [kernel, stdlib]), +    AppModules = [behaviour1, behaviour2, mymod, othermod], +    [write_src_file(AppDir, Name, Module, ignore_xref(Case)) || Module <- AppModules], +    RebarConfig = [{erl_opts, [debug_info]}, +                   {xref_checks, [deprecated_function_calls,deprecated_functions, +                                  undefined_function_calls,undefined_functions, +                                  exports_not_used,locals_not_used]}], +    [{app_name, Name}, +     {rebar_config, RebarConfig}, +     {orig_dir, OrigDir} | UpdConfig]. + +end_per_testcase(_, Config) -> +    ?debugMsg("End test case cleanup"), +    AppDir = ?config(apps, Config), +    OrigDir = ?config(orig_dir, Config), +    %% Code path cleanup because we set the CWD to the `AppDir' prior +    %% to launching rebar and these paths make it into the code path +    %% before the xref module executes so they don't get cleaned up +    %% automatically after the xref run. Only have to do this because +    %% we are about to remove the directory and there may be +    %% subsequent test cases that error out when the code path tries +    %% to include one of these soon-to-be nonexistent directories. +    true = code:del_path(AppDir ++ "/."), +    true = code:del_path(rebar_dir:ebin_dir()), +    file:set_cwd(OrigDir), +    ec_file:remove(AppDir, [recursive]), +    ok. + +all() -> +    [xref_test, xref_ignore_test]. + +%% =================================================================== +%% Test cases +%% =================================================================== + +xref_test(Config) -> +    AppDir = ?config(apps, Config), +    State = ?config(state, Config), +    Name = ?config(app_name, Config), +    RebarConfig = ?config(rebar_config, Config), +    Result = rebar3:run(rebar_state:new(State, RebarConfig, AppDir), ["xref"]), +    verify_results(xref_test, Name, Result). + +xref_ignore_test(Config) -> +    AppDir = ?config(apps, Config), +    State = ?config(state, Config), +    Name = ?config(app_name, Config), +    RebarConfig = ?config(rebar_config, Config), +    Result = rebar3:run(rebar_state:new(State, RebarConfig, AppDir), ["xref"]), +    verify_results(xref_ignore_test, Name, Result). + +%% =================================================================== +%% Helper functions +%% =================================================================== + +ignore_xref(xref_ignore_test) -> +    true; +ignore_xref(_) -> +    false. + +verify_results(TestCase, AppName, Results) -> +    {error, {rebar_prv_xref, +             {xref_issues, XrefResults, QueryResults}}} = Results, +    verify_test_results(TestCase, AppName, XrefResults, QueryResults). + +verify_test_results(xref_test, AppName, XrefResults, _QueryResults) -> +    AppModules = ["behaviour1", "behaviour2", "mymod", "othermod", "somemod"], +    [Behaviour1Mod, Behaviour2Mod, MyMod, OtherMod, SomeMod] = +        [list_to_atom(AppName ++ "_" ++ Mod) || Mod <- AppModules], +    UndefFuns = proplists:get_value(undefined_functions, XrefResults), +    UndefFunCalls = proplists:get_value(undefined_function_calls, XrefResults), +    LocalsNotUsed = proplists:get_value(locals_not_used, XrefResults), +    ExportsNotUsed = proplists:get_value(exports_not_used, XrefResults), +    DeprecatedFuns = proplists:get_value(deprecated_functions, XrefResults), +    DeprecatedFunCalls = proplists:get_value(deprecated_function_calls, XrefResults), +    ?assert(lists:member({SomeMod, notavailable, 1}, UndefFuns)), +    ?assert(lists:member({{OtherMod, somefunc, 0}, {SomeMod, notavailable, 1}}, +                         UndefFunCalls)), +    ?assert(lists:member({MyMod, fdeprecated, 0}, DeprecatedFuns)), +    ?assert(lists:member({{OtherMod, somefunc, 0}, {MyMod, fdeprecated, 0}}, +                         DeprecatedFunCalls)), +    ?assert(lists:member({MyMod, localfunc2, 0}, LocalsNotUsed)), +    ?assert(lists:member({Behaviour1Mod, behaviour_info, 1}, ExportsNotUsed)), +    ?assert(lists:member({Behaviour2Mod, behaviour_info, 1}, ExportsNotUsed)), +    ?assert(lists:member({MyMod, other2, 1}, ExportsNotUsed)), +    ?assert(lists:member({OtherMod, somefunc, 0}, ExportsNotUsed)), +    ?assertNot(lists:member({MyMod, bh1_a, 1}, ExportsNotUsed)), +    ?assertNot(lists:member({MyMod, bh1_b, 1}, ExportsNotUsed)), +    ?assertNot(lists:member({MyMod, bh2_a, 1}, ExportsNotUsed)), +    ?assertNot(lists:member({MyMod, bh2_b, 1}, ExportsNotUsed)), +    ok; +verify_test_results(xref_ignore_test, AppName, XrefResults, _QueryResults) -> +    AppModules = ["behaviour1", "behaviour2", "mymod", "othermod", "somemod"], +    [Behaviour1Mod, Behaviour2Mod, MyMod, OtherMod, SomeMod] = +        [list_to_atom(AppName ++ "_" ++ Mod) || Mod <- AppModules], +    UndefFuns = proplists:get_value(undefined_functions, XrefResults), +    ?assertNot(lists:keymember(undefined_function_calls, 1, XrefResults)), +    ?assertNot(lists:keymember(locals_not_used, 1, XrefResults)), +    ?assertNot(lists:keymember(exports_not_used, 1, XrefResults)), +    ?assertNot(lists:keymember(deprecated_functions, 1, XrefResults)), +    ?assertNot(lists:keymember(deprecated_function_calls, 1, XrefResults)), +    ?assert(lists:member({SomeMod, notavailable, 1}, UndefFuns)), +    ok. + +write_src_file(Dir, AppName, Module, IgnoreXref) -> +    Erl = filename:join([Dir, "src", module_name(AppName, Module)]), +    ok = filelib:ensure_dir(Erl), +    ok = ec_file:write(Erl, get_module_body(Module, AppName, IgnoreXref)). + +module_name(AppName, Module) -> +    lists:flatten([AppName, "_", atom_to_list(Module), ".erl"]). + +get_module_body(behaviour1, AppName, IgnoreXref) -> +    ["-module(", AppName, "_behaviour1).\n", +     "-export([behaviour_info/1]).\n", +     ["-ignore_xref({behaviour_info,1}).\n" || X <- [IgnoreXref], X =:= true], +     "behaviour_info(callbacks) -> [{bh1_a,1},{bh1_b,1}];\n", +     "behaviour_info(_Other) -> undefined.\n"]; +get_module_body(behaviour2, AppName, IgnoreXref) -> +    ["-module(", AppName, "_behaviour2).\n", +     "-export([behaviour_info/1]).\n", +     ["-ignore_xref({behaviour_info,1}).\n" || X <- [IgnoreXref], X =:= true], +     "behaviour_info(callbacks) -> [{bh2_a,1},{bh2_b,1}];\n", +     "behaviour_info(_Other) -> undefined.\n"]; +get_module_body(mymod, AppName, IgnoreXref) -> +    ["-module(", AppName, "_mymod).\n", +     "-export([bh1_a/1,bh1_b/1,bh2_a/1,bh2_b/1," +     "other1/1,other2/1,fdeprecated/0]).\n", +     ["-ignore_xref([{other2,1},{localfunc2,0},{fdeprecated,0}]).\n" +      || X <- [IgnoreXref], X =:= true], +     "-behaviour(", AppName, "_behaviour1).\n",     % 2 behaviours +     "-behaviour(", AppName, "_behaviour2).\n", +     "-deprecated({fdeprecated,0}).\n",      % deprecated function +     "bh1_a(A) -> localfunc1(bh1_a, A).\n", % behaviour functions +     "bh1_b(A) -> localfunc1(bh1_b, A).\n", +     "bh2_a(A) -> localfunc1(bh2_a, A).\n", +     "bh2_b(A) -> localfunc1(bh2_b, A).\n", +     "other1(A) -> localfunc1(other1, A).\n", % regular exported functions +     "other2(A) -> localfunc1(other2, A).\n", +     "localfunc1(A, B) -> {A, B}.\n",       % used local +     "localfunc2() -> ok.\n",               % unused local +     "fdeprecated() -> ok.\n"              % deprecated function +    ]; +get_module_body(othermod, AppName, IgnoreXref) -> +    ["-module(", AppName, "_othermod).\n", +     "-export([somefunc/0]).\n", +     [["-ignore_xref([{", AppName, "_somemod,notavailable,1},{somefunc,0}]).\n", +       "-ignore_xref({", AppName, "_mymod,fdeprecated,0}).\n"] +      || X <- [IgnoreXref], X =:= true], +     "somefunc() ->\n", +     "   ", AppName, "_mymod:other1(arg),\n", +     "   ", AppName, "_somemod:notavailable(arg),\n", +     "   ", AppName, "_mymod:fdeprecated().\n"]. | 
