diff options
Diffstat (limited to 'src')
-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 |
7 files changed, 368 insertions, 38 deletions
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. |