# Copyright (c) 2022, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
.PHONY: xref
# Configuration.
# We do not use locals_not_used or deprecated_function_calls
# because the compiler will error out by default in those
# cases with Erlang.mk. Deprecated functions may make sense
# in some cases but few libraries define them. We do not
# use exports_not_used by default because it hinders more
# than it helps library projects such as Cowboy. Finally,
# undefined_functions provides little that undefined_function_calls
# doesn't already provide, so it's not enabled by default.
XREF_CHECKS ?= [undefined_function_calls]
# Instead of predefined checks a query can be evaluated
# using the Xref DSL. The $q variable is used in that case.
# The scope is a list of keywords that correspond to
# application directories, being essentially an easy way
# to configure which applications to analyze. With:
#
# - app: .
# - apps: $(ALL_APPS_DIRS)
# - deps: $(ALL_DEPS_DIRS)
# - otp: Built-in Erlang/OTP applications.
#
# The default is conservative (app) and will not be
# appropriate for all types of queries (for example
# application_call requires adding all applications
# that might be called or they will not be found).
XREF_SCOPE ?= app # apps deps otp
# If the above is not enough, additional application
# directories can be configured.
XREF_EXTRA_APP_DIRS ?=
# As well as additional non-application directories.
XREF_EXTRA_DIRS ?=
# Erlang.mk supports -ignore_xref([...]) with forms
# {M, F, A} | {F, A} | M, the latter ignoring whole
# modules. Ignores can also be provided project-wide.
XREF_IGNORE ?= []
# All callbacks may be ignored. Erlang.mk will ignore
# them automatically for exports_not_used (unless it
# is explicitly disabled by the user).
XREF_IGNORE_CALLBACKS ?=
# Core targets.
help::
$(verbose) printf '%s\n' '' \
'Xref targets:' \
' xref Analyze the project using Xref' \
' xref q=QUERY Evaluate an Xref query'
# Plugin-specific targets.
define xref.erl
{ok, Xref} = xref:start([]),
Scope = [$(call comma_list,$(XREF_SCOPE))],
AppDirs0 = [$(call comma_list,$(foreach d,$(XREF_EXTRA_APP_DIRS),"$d"))],
AppDirs1 = case lists:member(otp, Scope) of
false -> AppDirs0;
true ->
RootDir = code:root_dir(),
AppDirs0 ++ [filename:dirname(P) || P <- code:get_path(), lists:prefix(RootDir, P)]
end,
AppDirs2 = case lists:member(deps, Scope) of
false -> AppDirs1;
true -> [$(call comma_list,$(foreach d,$(ALL_DEPS_DIRS),"$d"))] ++ AppDirs1
end,
AppDirs3 = case lists:member(apps, Scope) of
false -> AppDirs2;
true -> [$(call comma_list,$(foreach d,$(ALL_APPS_DIRS),"$d"))] ++ AppDirs2
end,
AppDirs = case lists:member(app, Scope) of
false -> AppDirs3;
true -> ["../$(notdir $(CURDIR))"|AppDirs3]
end,
[{ok, _} = xref:add_application(Xref, AppDir, [{builtins, true}]) || AppDir <- AppDirs],
ExtraDirs = [$(call comma_list,$(foreach d,$(XREF_EXTRA_DIRS),"$d"))],
[{ok, _} = xref:add_directory(Xref, ExtraDir, [{builtins, true}]) || ExtraDir <- ExtraDirs],
ok = xref:set_library_path(Xref, code:get_path() -- (["ebin", "."] ++ AppDirs ++ ExtraDirs)),
Checks = case {$1, is_list($2)} of
{check, true} -> $2;
{check, false} -> [$2];
{query, _} -> [$2]
end,
FinalRes = [begin
IsInformational = case $1 of
query -> true;
check ->
is_tuple(Check) andalso
lists:member(element(1, Check),
[call, use, module_call, module_use, application_call, application_use])
end,
{ok, Res0} = case $1 of
check -> xref:analyze(Xref, Check);
query -> xref:q(Xref, Check)
end,
Res = case IsInformational of
true -> Res0;
false ->
lists:filter(fun(R) ->
{Mod, InMFA, MFA} = case R of
{InMFA0 = {M, _, _}, MFA0} -> {M, InMFA0, MFA0};
{M, _, _} -> {M, R, R}
end,
Attrs = try
Mod:module_info(attributes)
catch error:undef ->
[]
end,
InlineIgnores = lists:flatten([
[case V of
M when is_atom(M) -> {M, '_', '_'};
{F, A} -> {Mod, F, A};
_ -> V
end || V <- Values]
|| {ignore_xref, Values} <- Attrs]),
BuiltinIgnores = [
{eunit_test, wrapper_test_exported_, 0}
],
DoCallbackIgnores = case {Check, "$(strip $(XREF_IGNORE_CALLBACKS))"} of
{exports_not_used, ""} -> true;
{_, "0"} -> false;
_ -> true
end,
CallbackIgnores = case DoCallbackIgnores of
false -> [];
true ->
Behaviors = lists:flatten([
[BL || {behavior, BL} <- Attrs],
[BL || {behaviour, BL} <- Attrs]
]),
[{Mod, CF, CA} || B <- Behaviors, {CF, CA} <- B:behaviour_info(callbacks)]
end,
WideIgnores = if
is_list($(XREF_IGNORE)) ->
[if is_atom(I) -> {I, '_', '_'}; true -> I end
|| I <- $(XREF_IGNORE)];
true -> [$(XREF_IGNORE)]
end,
Ignores = InlineIgnores ++ BuiltinIgnores ++ CallbackIgnores ++ WideIgnores,
not (lists:member(InMFA, Ignores)
orelse lists:member(MFA, Ignores)
orelse lists:member({Mod, '_', '_'}, Ignores))
end, Res0)
end,
case Res of
[] -> ok;
_ when IsInformational ->
case Check of
{call, {CM, CF, CA}} ->
io:format("Functions that ~s:~s/~b calls:~n", [CM, CF, CA]);
{use, {CM, CF, CA}} ->
io:format("Function ~s:~s/~b is called by:~n", [CM, CF, CA]);
{module_call, CMod} ->
io:format("Modules that ~s calls:~n", [CMod]);
{module_use, CMod} ->
io:format("Module ~s is used by:~n", [CMod]);
{application_call, CApp} ->
io:format("Applications that ~s calls:~n", [CApp]);
{application_use, CApp} ->
io:format("Application ~s is used by:~n", [CApp]);
_ when $1 =:= query ->
io:format("Query ~s returned:~n", [Check])
end,
[case R of
{{InM, InF, InA}, {M, F, A}} ->
io:format("- ~s:~s/~b called by ~s:~s/~b~n",
[M, F, A, InM, InF, InA]);
{M, F, A} ->
io:format("- ~s:~s/~b~n", [M, F, A]);
ModOrApp ->
io:format("- ~s~n", [ModOrApp])
end || R <- Res],
ok;
_ ->
[case {Check, R} of
{undefined_function_calls, {{InM, InF, InA}, {M, F, A}}} ->
io:format("Undefined function ~s:~s/~b called by ~s:~s/~b~n",
[M, F, A, InM, InF, InA]);
{undefined_functions, {M, F, A}} ->
io:format("Undefined function ~s:~s/~b~n", [M, F, A]);
{locals_not_used, {M, F, A}} ->
io:format("Unused local function ~s:~s/~b~n", [M, F, A]);
{exports_not_used, {M, F, A}} ->
io:format("Unused exported function ~s:~s/~b~n", [M, F, A]);
{deprecated_function_calls, {{InM, InF, InA}, {M, F, A}}} ->
io:format("Deprecated function ~s:~s/~b called by ~s:~s/~b~n",
[M, F, A, InM, InF, InA]);
{deprecated_functions, {M, F, A}} ->
io:format("Deprecated function ~s:~s/~b~n", [M, F, A]);
_ ->
io:format("~p: ~p~n", [Check, R])
end || R <- Res],
error
end
end || Check <- Checks],
stopped = xref:stop(Xref),
case lists:usort(FinalRes) of
[ok] -> halt(0);
_ -> halt(1)
end
endef
xref: deps app
ifdef q
$(verbose) $(call erlang,$(call xref.erl,query,"$q"),-pa ebin/)
else
$(verbose) $(call erlang,$(call xref.erl,check,$(XREF_CHECKS)),-pa ebin/)
endif