2017-06-30 17:29:37 +08:00
|
|
|
#!/usr/bin/env escript
|
|
|
|
%% vim:ft=erlang:
|
|
|
|
|
|
|
|
%% The code is copied from xref_runner.
|
|
|
|
%% https://github.com/inaka/xref_runner
|
|
|
|
%%
|
2017-06-30 19:46:54 +08:00
|
|
|
%% The only change is the support of our erlang_version_support
|
|
|
|
%% attribute: we don't want any warnings about functions which will be
|
|
|
|
%% dropped at load time.
|
2017-06-30 17:29:37 +08:00
|
|
|
%%
|
|
|
|
%% 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(CHECKS, [undefined_function_calls,
|
|
|
|
undefined_functions,
|
|
|
|
locals_not_used]).
|
|
|
|
|
|
|
|
main(_) ->
|
|
|
|
Checks = ?CHECKS,
|
2017-07-07 19:23:16 +08:00
|
|
|
ElixirDeps = get_elixir_deps_paths(),
|
|
|
|
[true = code:add_path(P) || P <- ElixirDeps],
|
2017-06-30 17:29:37 +08:00
|
|
|
XrefWarnings = lists:append([check(Check) || Check <- Checks]),
|
|
|
|
warnings_prn(XrefWarnings),
|
|
|
|
case XrefWarnings of
|
|
|
|
[] -> ok;
|
|
|
|
_ -> halt(1)
|
|
|
|
end.
|
|
|
|
|
2017-07-07 19:23:16 +08:00
|
|
|
get_elixir_deps_paths() ->
|
|
|
|
case os:getenv("ERLANG_MK_RECURSIVE_DEPS_LIST") of
|
|
|
|
false ->
|
|
|
|
[];
|
|
|
|
Filename ->
|
|
|
|
{ok, Fd} = file:open(Filename, [read]),
|
|
|
|
get_elixir_deps_paths1(Fd, [])
|
|
|
|
end.
|
|
|
|
|
|
|
|
get_elixir_deps_paths1(Fd, Paths) ->
|
|
|
|
case file:read_line(Fd) of
|
2019-12-03 01:59:38 +08:00
|
|
|
{ok, Line0} ->
|
|
|
|
Line = Line0 -- [$\r, $\n],
|
|
|
|
RootPath = case os:type() of
|
|
|
|
{unix, _} ->
|
|
|
|
Line;
|
|
|
|
{win32, _} ->
|
|
|
|
case os:find_executable("cygpath.exe") of
|
|
|
|
false ->
|
|
|
|
Line;
|
|
|
|
Cygpath ->
|
|
|
|
os:cmd(
|
|
|
|
io_lib:format("~s --windows \"~s\"",
|
|
|
|
[Cygpath, Line]))
|
|
|
|
-- [$\r, $\n]
|
|
|
|
end
|
|
|
|
end,
|
2017-07-07 19:23:16 +08:00
|
|
|
Glob = filename:join([RootPath, "_build", "dev", "lib", "*", "ebin"]),
|
|
|
|
NewPaths = filelib:wildcard(Glob),
|
|
|
|
get_elixir_deps_paths1(Fd, Paths ++ NewPaths);
|
|
|
|
eof ->
|
2019-12-03 01:59:38 +08:00
|
|
|
add_elixir_stdlib_path(Paths)
|
2017-07-07 19:23:16 +08:00
|
|
|
end.
|
|
|
|
|
2019-12-03 01:59:38 +08:00
|
|
|
add_elixir_stdlib_path(Paths) ->
|
|
|
|
case find_elixir_home() of
|
|
|
|
false -> Paths;
|
|
|
|
ElixirLibDir -> [ElixirLibDir | Paths]
|
|
|
|
end.
|
|
|
|
|
|
|
|
find_elixir_home() ->
|
|
|
|
ElixirExe = case os:type() of
|
|
|
|
{unix, _} -> "elixir";
|
|
|
|
{win32, _} -> "elixir.bat"
|
|
|
|
end,
|
|
|
|
case os:find_executable(ElixirExe) of
|
|
|
|
false -> false;
|
|
|
|
ExePath -> resolve_symlink(ExePath)
|
|
|
|
end.
|
|
|
|
|
|
|
|
resolve_symlink(ExePath) ->
|
|
|
|
case file:read_link_all(ExePath) of
|
|
|
|
{error, einval} ->
|
|
|
|
determine_elixir_home(ExePath);
|
|
|
|
{ok, ResolvedLink} ->
|
|
|
|
ExePath1 = filename:absname(ResolvedLink,
|
|
|
|
filename:dirname(ExePath)),
|
|
|
|
resolve_symlink(ExePath1);
|
|
|
|
{error, _} ->
|
|
|
|
false
|
|
|
|
end.
|
|
|
|
|
|
|
|
determine_elixir_home(ExePath) ->
|
|
|
|
LibPath = filename:join([filename:dirname(filename:dirname(ExePath)),
|
|
|
|
"lib",
|
|
|
|
"elixir",
|
|
|
|
"ebin"]),
|
|
|
|
case filelib:is_dir(LibPath) of
|
|
|
|
true -> LibPath;
|
|
|
|
false -> {skip, "Failed to locate Elixir lib dir"}
|
|
|
|
end.
|
2017-06-30 17:29:37 +08:00
|
|
|
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) ->
|
2017-06-30 22:30:16 +08:00
|
|
|
case filelib:is_dir(Dir) of
|
|
|
|
true -> {ok, _} = xref:add_directory(Xref, Dir);
|
|
|
|
false -> ok
|
|
|
|
end
|
2017-06-30 17:29:37 +08:00
|
|
|
end, Dirs),
|
|
|
|
|
|
|
|
{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]),
|
|
|
|
|
2017-06-30 19:46:54 +08:00
|
|
|
Ignores = lists:flatmap(
|
|
|
|
fun(Module) -> get_ignorelist(Module, Check) end, SourceModules),
|
|
|
|
|
|
|
|
UnusedFunctions = lists:flatmap(
|
|
|
|
fun(Mod) -> get_unused_compat_functions(Mod) end,
|
|
|
|
SourceModules),
|
|
|
|
|
|
|
|
ToIgnore = case get(results_to_ignore) of
|
|
|
|
undefined -> [];
|
|
|
|
RTI -> RTI
|
|
|
|
end,
|
|
|
|
NewToIgnore = [parse_xref_target(Result)
|
|
|
|
|| Result <- Results,
|
|
|
|
lists:member(parse_xref_source(Result), UnusedFunctions)],
|
|
|
|
AllToIgnore = ToIgnore ++ NewToIgnore ++ [mfa(M, {F, A})
|
|
|
|
|| {_, {M, F, A}} <- Ignores],
|
|
|
|
put(results_to_ignore, AllToIgnore),
|
2017-06-30 17:29:37 +08:00
|
|
|
|
|
|
|
[Result || Result <- Results,
|
2017-06-30 19:46:54 +08:00
|
|
|
not lists:member(parse_xref_result(Result), Ignores) andalso
|
|
|
|
not lists:member(parse_xref_result(Result), AllToIgnore) andalso
|
|
|
|
not lists:member(parse_xref_source(Result), UnusedFunctions)].
|
2017-06-30 17:29:37 +08:00
|
|
|
|
|
|
|
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) ->
|
|
|
|
[].
|
|
|
|
|
2017-06-30 19:46:54 +08:00
|
|
|
get_unused_compat_functions(Module) ->
|
|
|
|
OTPVersion = code_version:get_otp_version(),
|
|
|
|
Attributes = try
|
|
|
|
Module:module_info(attributes)
|
|
|
|
catch
|
|
|
|
_Class:_Error -> []
|
|
|
|
end,
|
|
|
|
CompatTuples = [Tuple
|
|
|
|
|| {erlang_version_support, Tuples} <- Attributes,
|
|
|
|
Tuple <- Tuples],
|
|
|
|
get_unused_compat_functions(Module, OTPVersion, CompatTuples, []).
|
|
|
|
|
|
|
|
get_unused_compat_functions(_, _, [], Result) ->
|
|
|
|
Result;
|
|
|
|
get_unused_compat_functions(Module,
|
|
|
|
OTPVersion,
|
|
|
|
[{MinOTPVersion, Choices} | Rest],
|
|
|
|
Result) ->
|
|
|
|
Functions = lists:map(
|
|
|
|
fun({_, Arity, Pre, Post}) ->
|
|
|
|
if
|
|
|
|
OTPVersion >= MinOTPVersion ->
|
|
|
|
%% We ignore the "pre" function.
|
|
|
|
mfa(Module, {Pre, Arity});
|
|
|
|
true ->
|
|
|
|
%% We ignore the "post" function.
|
|
|
|
mfa(Module, {Post, Arity})
|
|
|
|
end
|
|
|
|
end, Choices),
|
|
|
|
get_unused_compat_functions(Module, OTPVersion, Rest,
|
|
|
|
Result ++ Functions).
|
|
|
|
|
2017-06-30 17:29:37 +08:00
|
|
|
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}.
|
|
|
|
|
2017-06-30 19:46:54 +08:00
|
|
|
parse_xref_source({{SM, _, _} = MFAt, _}) -> {SM, MFAt};
|
|
|
|
parse_xref_source({TM, _, _} = MFAt) -> {TM, MFAt}.
|
|
|
|
|
|
|
|
parse_xref_target({_, {TM, _, _} = MFAt}) -> {TM, MFAt};
|
|
|
|
parse_xref_target({TM, _, _} = MFAt) -> {TM, MFAt}.
|
|
|
|
|
2017-06-30 17:29:37 +08:00
|
|
|
%% -------------------------------------------------------------------
|
|
|
|
%% 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]).
|