mk/xrefr: Provide our own xrefr(1) escript

It is forked from xref_runner. Compared to upstream, it allows to
exclude entire modules: we need that for the *_compat modules.

Furthermore it's compatible with Erlang R16B03 because:
    - It's a plain text escript, not a precompiled one.
    - It doesn't use maps.

[#140125673]
This commit is contained in:
Jean-Sébastien Pédron 2017-06-30 11:29:37 +02:00
parent a6fc135a2a
commit c42e53a6e0
3 changed files with 216 additions and 21 deletions

View File

@ -3,7 +3,9 @@
# --------------------------------------------------------------------
ifneq ($(PROJECT),rabbit_common)
XREFR_ARGS := --config $(DEPS_DIR)/rabbit_common/xref.config
XREFR := $(DEPS_DIR)/rabbit_common/mk/xrefr
else
XREFR := mk/xrefr
endif
# --------------------------------------------------------------------

213
deps/rabbit_common/mk/xrefr vendored Executable file
View File

@ -0,0 +1,213 @@
#!/usr/bin/env escript
%% vim:ft=erlang:
%% The code is copied from xref_runner.
%% https://github.com/inaka/xref_runner
%%
%% The only change is the possibility to exclude entire modules because
%% we don't want to analyse the *_compat modules.
%%
%% It's also a plain text escript instead of a compiled one because we
%% want to support Erlang R16B03 and the version of xref_runner uses
%% maps and is built with something like Erlang 18.
%% This mode allows us to reference local function. For instance:
%% lists:map(fun generate_comment/1, Comments)
-mode(compile).
-define(DIRS, ["ebin", "test"]).
-define(EXCLUDED_MODULES, [rand_compat,
ssl_compat,
time_compat]).
-define(CHECKS, [undefined_function_calls,
undefined_functions,
locals_not_used]).
main(_) ->
Checks = ?CHECKS,
XrefWarnings = lists:append([check(Check) || Check <- Checks]),
warnings_prn(XrefWarnings),
case XrefWarnings of
[] -> ok;
_ -> halt(1)
end.
check(Check) ->
Dirs = ?DIRS,
lists:foreach(fun code:add_path/1, Dirs),
{ok, Xref} = xref:start([]),
try
ok = xref:set_library_path(Xref, code:get_path()),
lists:foreach(
fun(Dir) ->
{ok, _} = xref:add_directory(Xref, Dir)
end, Dirs),
ExcludedModules = ?EXCLUDED_MODULES,
xref:remove_module(Xref, ExcludedModules),
{ok, Results} = xref:analyze(Xref, Check),
FilteredResults = filter_xref_results(Check, Results),
[result_to_warning(Check, Result) || Result <- FilteredResults]
after
stopped = xref:stop(Xref)
end.
%% -------------------------------------------------------------------
%% Filtering results.
%% -------------------------------------------------------------------
filter_xref_results(Check, Results) ->
SourceModules =
lists:usort([source_module(Result) || Result <- Results]),
Ignores =
lists:flatmap(
fun(Module) -> get_ignorelist(Module, Check) end, SourceModules),
[Result || Result <- Results,
not lists:member(parse_xref_result(Result), Ignores)].
source_module({Mt, _Ft, _At}) -> Mt;
source_module({{Ms, _Fs, _As}, _Target}) -> Ms.
%%
%% Ignore behaviour functions, and explicitly marked functions
%%
%% Functions can be ignored by using
%% -ignore_xref([{F, A}, {M, F, A}...]).
get_ignorelist(Mod, Check) ->
%% Get ignore_xref attribute and combine them in one list
Attributes =
try
Mod:module_info(attributes)
catch
_Class:_Error -> []
end,
IgnoreXref =
[mfa(Mod, Value) || {ignore_xref, Values} <- Attributes, Value <- Values],
BehaviourCallbacks = get_behaviour_callbacks(Check, Mod, Attributes),
%% And create a flat {M, F, A} list
IgnoreXref ++ BehaviourCallbacks.
get_behaviour_callbacks(exports_not_used, Mod, Attributes) ->
Behaviours = [Value || {behaviour, Values} <- Attributes, Value <- Values],
[{Mod, {Mod, F, A}}
|| B <- Behaviours, {F, A} <- B:behaviour_info(callbacks)];
get_behaviour_callbacks(_Check, _Mod, _Attributes) ->
[].
mfa(M, {F, A}) -> {M, {M, F, A}};
mfa(M, MFA) -> {M, MFA}.
parse_xref_result({{SM, _, _}, MFAt}) -> {SM, MFAt};
parse_xref_result({TM, _, _} = MFAt) -> {TM, MFAt}.
%% -------------------------------------------------------------------
%% Preparing results.
%% -------------------------------------------------------------------
result_to_warning(Check, {MFASource, MFATarget}) ->
{Filename, Line} = get_source(MFASource),
[{filename, Filename},
{line, Line},
{source, MFASource},
{target, MFATarget},
{check, Check}];
result_to_warning(Check, MFA) ->
{Filename, Line} = get_source(MFA),
[{filename, Filename},
{line, Line},
{source, MFA},
{check, Check}].
%%
%% 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.
%%
get_source({M, F, A}) ->
case code:get_object_code(M) of
error -> {"", 0};
{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
[Source|_] = [S || {attribute, _, file, {S, _}} <- Code],
%% Extract the line number for a given function def
Fn = [E || E <- Code,
element(1, E) == function,
element(3, E) == F,
element(4, E) == A],
case Fn of
[{function, Line, F, _, _}] when is_integer(Line) ->
{Source, Line};
[{function, Line, F, _, _}] ->
{Source, erl_anno:line(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, 0}
end.
%% -------------------------------------------------------------------
%% Reporting results.
%% -------------------------------------------------------------------
warnings_prn([]) ->
ok;
warnings_prn(Comments) ->
Messages = lists:map(fun generate_comment/1, Comments),
lists:foreach(fun warning_prn/1, Messages).
warning_prn(Message) ->
FullMessage = Message ++ "~n",
io:format(FullMessage, []).
generate_comment(XrefWarning) ->
Filename = proplists:get_value(filename, XrefWarning),
Line = proplists:get_value(line, XrefWarning),
Source = proplists:get_value(source, XrefWarning),
Check = proplists:get_value(check, XrefWarning),
Target = proplists:get_value(target, XrefWarning),
Position = case {Filename, Line} of
{"", _} -> "";
{Filename, 0} -> [Filename, " "];
{Filename, Line} -> [Filename, ":",
integer_to_list(Line), " "]
end,
[Position, generate_comment_text(Check, Source, Target)].
generate_comment_text(Check, {SM, SF, SA}, TMFA) ->
SMFA = io_lib:format("`~p:~p/~p`", [SM, SF, SA]),
generate_comment_text(Check, SMFA, TMFA);
generate_comment_text(Check, SMFA, {TM, TF, TA}) ->
TMFA = io_lib:format("`~p:~p/~p`", [TM, TF, TA]),
generate_comment_text(Check, SMFA, TMFA);
generate_comment_text(undefined_function_calls, SMFA, TMFA) ->
io_lib:format("~s calls undefined function ~s", [SMFA, TMFA]);
generate_comment_text(undefined_functions, SMFA, _TMFA) ->
io_lib:format("~s is not defined as a function", [SMFA]);
generate_comment_text(locals_not_used, SMFA, _TMFA) ->
io_lib:format("~s is an unused local function", [SMFA]);
generate_comment_text(exports_not_used, SMFA, _TMFA) ->
io_lib:format("~s is an unused export", [SMFA]);
generate_comment_text(deprecated_function_calls, SMFA, TMFA) ->
io_lib:format("~s calls deprecated function ~s", [SMFA, TMFA]);
generate_comment_text(deprecated_functions, SMFA, _TMFA) ->
io_lib:format("~s is deprecated", [SMFA]).

View File

@ -1,20 +0,0 @@
% vim:ft=erlang:sw=2:et:
[
{xref, [
{checks, [undefined_function_calls,
undefined_functions,
locals_not_used
%exports_not_used,
%deprecated_function_calls,
%deprecated_functions
]},
{config, #{
dirs => ["ebin", "test"],
exclude_modules => [rand_compat,
ssl_compat,
time_compat]
}}
]
}
].