File 4061-xref-Add-native-support-for-ignore_xref.patch of Package erlang

From 6f398fb896fc26817a0d96d4b3dc186dcaf9d300 Mon Sep 17 00:00:00 2001
From: Richard Carlsson <carlsson.richard@gmail.com>
Date: Sun, 25 Jan 2026 17:15:16 +0100
Subject: [PATCH] xref: Add native support for ignore_xref

Moves the result filtering from rebar3 into xref itself.
---
 lib/tools/src/xref.erl                        | 102 ++++++++++++++++--
 lib/tools/src/xref.hrl                        |   2 +
 lib/tools/src/xref_base.erl                   |  38 +++++--
 lib/tools/src/xref_reader.erl                 |   6 +-
 lib/tools/test/xref_SUITE.erl                 |   5 +-
 lib/tools/test/xref_SUITE_data/lib_test/t.erl |  19 +++-
 6 files changed, 145 insertions(+), 27 deletions(-)

diff --git a/lib/tools/src/xref.erl b/lib/tools/src/xref.erl
index 21de672a40..e67d9a3c6d 100644
--- a/lib/tools/src/xref.erl
+++ b/lib/tools/src/xref.erl
@@ -103,6 +103,24 @@ module. All unknown functions are also undefined functions; there is a
 [figure](xref_chapter.md#venn2) in the User's Guide that illustrates
 this relationship.
 
+The module attribute `ignore_xref` can be used to suppress warnings for
+certain modules or functions in your modules. The following forms are
+understood by Xref:
+
+```erlang
+-ignore_xref(module).
+-ignore_xref([module1, module2, module3]).
+
+-ignore_xref(function/0).
+-ignore_xref([function1/0, function2/1, function3/2]).
+-ignore_xref({function,0}).
+-ignore_xref([{function1,0}, {function2,1}, {function3,2}]).
+
+%% ignoring calls/references to other modules from the current
+-ignore_xref({other_mod1, func1, 0}).
+-ignore_xref([{other_mod1, func1, 0}, {other_mod2, func2, 1}]).
+```
+
 The module attribute tag `deprecated` can be used to inform
 Xref about _deprecated functions_{: #deprecated_function } and optionally when
 functions are planned to be removed. A few examples show the idea:
@@ -595,6 +613,8 @@ called, all user variables are forgotten.
 
 -import(sofs, [to_external/1, is_sofs_set/1]).
 
+-include("xref.hrl").
+
 %%%----------------------------------------------------------------------
 %%% API
 %%%----------------------------------------------------------------------
@@ -1965,16 +1985,16 @@ handle_call({variables, Options}, _From, State) ->
     {reply, Reply, NewState};
 handle_call({analyze, What}, _From, State) ->
     {Reply, NewState} = xref_base:analyze(State, What),
-    {reply, unsetify(Reply), NewState};
+    {reply, finalize(Reply, NewState), NewState};
 handle_call({analyze, What, Options}, _From, State) ->
     {Reply, NewState} = xref_base:analyze(State, What, Options),
-    {reply, unsetify(Reply), NewState};
+    {reply, finalize(Reply, NewState), NewState};
 handle_call({qry, Q}, _From, State) ->
     {Reply, NewState} = xref_base:q(State, Q),
-    {reply, unsetify(Reply), NewState};
+    {reply, finalize(Reply, NewState), NewState};
 handle_call({qry, Q, Options}, _From, State) ->
     {Reply, NewState} = xref_base:q(State, Q, Options),
-    {reply, unsetify(Reply), NewState};
+    {reply, finalize(Reply, NewState), NewState};
 handle_call(get_default, _From, State) ->
     Reply = xref_base:get_default(State),
     {reply, Reply, State};
@@ -2075,10 +2095,72 @@ do_analysis(State, Analysis) ->
 	    throw(Error)
     end.
 
-unsetify(Reply={ok, X}) ->
+finalize({ok, X}, #xref{ignores = Ignores}) ->
+    {ok, filter_xref_results(Ignores, unsetify(X))};
+finalize(Error, _State) ->
+    Error.
+
+unsetify(X) ->
     case is_sofs_set(X) of
-	true -> {ok, to_external(X)};
-	false -> Reply
-    end;
-unsetify(Reply) ->
-    Reply.
+        true -> to_external(X);
+        false -> X
+    end.
+
+%% Filters out behaviour functions and explicitly marked functions
+%% For example: `-ignore_xref([{F, A}, {M, F, A}, M, ...]).`
+filter_xref_results(_Ignores, Results) when not is_list(Results) ->
+    Results;
+filter_xref_results(Ignores, Results) ->
+    SearchModules = lists:usort(
+                      lists:map(
+                        fun({Mt,_Ft,_At}) -> Mt;
+                           ({{Ms,_Fs,_As},{_Mt,_Ft,_At}}) -> Ms;
+                           (_) -> undefined
+                        end, Results)),
+
+    Ignores1 = Ignores ++ lists:flatmap(fun(Module) ->
+                                    get_xref_ignorelist(Module)
+                            end, SearchModules),
+
+    lists:filter( fun(Result) -> pred_xref_result(Result, Ignores1) end, Results).
+
+pred_xref_result({Src, Dest}, Ignores) ->
+    pred_xref_result1(Src, Ignores)
+        andalso pred_xref_result1(Dest, Ignores);
+pred_xref_result(Vertex, Ignores) ->
+    pred_xref_result1(Vertex, Ignores).
+
+pred_xref_result1(Vertex, Ignores) ->
+    Mod = case Vertex of
+              {Module, _Func, _Arity} -> Module;
+              _ -> Vertex
+          end,
+    not lists:member(Vertex, Ignores) andalso not lists:member(Mod, Ignores).
+
+get_xref_ignorelist(Mod) ->
+    %% Get ignore_xref attribute and combine them in one list
+    Attributes = get_module_attrs(Mod),
+    IgnoreXref = keyall(ignore_xref, Attributes),
+    lists:foldl(
+      fun({F, A}, Acc) -> [{Mod,F,A} | Acc];
+         ({M, F, A}, Acc) -> [{M,F,A} | Acc];
+         (M, Acc) when is_atom(M) -> [M | Acc]
+      end, [], lists:flatten([IgnoreXref])).
+
+keyall(Key, List) ->
+    lists:flatmap(fun({K, L}) when Key =:= K -> L; (_) -> [] end, List).
+
+get_module_attrs(Mod) ->
+    case erlang:module_loaded(Mod) of
+        true ->
+            erlang:get_module_info(Mod, attributes);
+        false ->
+            case code:which(Mod) of
+                non_existing -> [];
+                Path ->
+                    case beam_lib:chunks(Path, [attributes]) of
+                        {ok, {Mod, [{attributes,As}]}} -> As;
+                        _ -> []
+                    end
+            end
+    end.
diff --git a/lib/tools/src/xref.hrl b/lib/tools/src/xref.hrl
index f8c223237e..ddd7cb0b8f 100644
--- a/lib/tools/src/xref.hrl
+++ b/lib/tools/src/xref.hrl
@@ -40,6 +40,7 @@
 	  modules = dict:new(),         % dict-of(xref_mod())
 	  applications = dict:new(),    % dict-of(xref_app())
 	  releases = dict:new(),        % dict-of(xref_rel())
+          ignores = [],
 
 	  library_path = [],         % [string()] | code_path
 	  libraries = dict:new(),    % dict-of(xref_lib())
@@ -58,6 +59,7 @@
 	  builtins,          % whether calls to built-in functions are included
 	  info,              % number of exports, locals etc.
 	  no_unresolved = 0, % number of unresolved calls
+          attributes = [],   % module attributes
 	  data               
 	  %% Data has been read from the BEAM file, and is represented here
           %% as a list of sets.
diff --git a/lib/tools/src/xref_base.erl b/lib/tools/src/xref_base.erl
index 044349446b..99859aa90a 100644
--- a/lib/tools/src/xref_base.erl
+++ b/lib/tools/src/xref_base.erl
@@ -743,13 +743,13 @@ read_module(SplitName, AppName, Builtins, Verbose, Warnings, State) ->
 read_a_module({Dir, BaseName}, AppName, Builtins, Verbose, Warnings, Mode) ->
     File = filename:join(Dir, BaseName),
     case abst(File, Builtins, Mode) of
-	{ok, _M, no_abstract_code} when Verbose ->
+        {ok, _M, _Attrs, no_abstract_code} when Verbose ->
 	    message(Verbose, no_debug_info, [File]),
 	    no;
-	{ok, _M, no_abstract_code} when not Verbose ->
+        {ok, _M, _Attrs, no_abstract_code} when not Verbose ->
 	    message(Warnings, no_debug_info, [File]),
 	    no;
-	{ok, M, Data, UnresCalls0}  ->
+        {ok, M, Attrs, Data, UnresCalls0}  ->
 	    message(Verbose, done_file, [File]),
             %% Remove duplicates. Identical unresolved calls on the
             %% same line are counted as _one_ unresolved call.
@@ -764,7 +764,7 @@ read_a_module({Dir, BaseName}, AppName, Builtins, Verbose, Warnings, Mode) ->
                 {ok, {_, _, _, Time}} ->
                     XMod = #xref_mod{name = M, app_name = AppName,
                                      dir = Dir, mtime = Time,
-                                     builtins = Builtins,
+                                     builtins = Builtins, attributes = Attrs,
                                      no_unresolved = NoUnresCalls},
                     {ok, PrepMod, Bad} =
                         prepare_module(Mode, XMod, UnresCalls, Data),
@@ -796,7 +796,7 @@ process_module(State) ->
 abst(File, Builtins, _Mode = functions) ->
     case beam_lib:chunks(File, [abstract_code, exports, attributes]) of
 	{ok, {M,[{abstract_code,NoA},_X,_A]}} when NoA =:= no_abstract_code ->
-	    {ok, M, NoA};
+            {ok, M, [], NoA};
 	{ok, {M, [{abstract_code, {raw_abstract_v1, Code}},
                   {exports,X0}, {attributes,A}]}} ->
 	    %% R9C-
@@ -805,7 +805,8 @@ abst(File, Builtins, _Mode = functions) ->
 	    Forms = erl_internal:add_predefined_functions(Forms1),
 	    X = mfa_exports(X0, A, M),
             D = deprecated(A, X, M),
-	    xref_reader:module(M, Forms, Builtins, X, D);
+            {Data, U} = xref_reader:module(M, Forms, Builtins, X, D),
+            {ok, M, A, Data, U};
 	Error when element(1, Error) =:= error ->
 	    Error
     end;
@@ -824,7 +825,7 @@ abst(File, Builtins, _Mode = modules) ->
 			      end,
 			filter(Fun, I0)
 		end,
-	    {ok, Mod, {X, I, D}, []};
+            {ok, Mod, At, {X, I, D}, []};
 	Error when element(1, Error) =:= error ->
 	    Error
     end.
@@ -914,7 +915,7 @@ do_add_module(S, XMod, Unres, Data) ->
     {ok, Ms, Bad, NS}.
 
 prepare_module(_Mode = functions, XMod, Unres0, Data) ->
-    {DefAt0, LPreCAt0, XPreCAt0, LC0, XC0, X0, _, Depr, OL0} = Data,
+    {DefAt0, LPreCAt0, XPreCAt0, LC0, XC0, X0, Depr, OL0} = Data,
     FT = [tspec(func)],
     FET = [tspec(fun_edge)],
     PCA = [tspec(pre_call_at)],
@@ -969,7 +970,8 @@ finish_module({functions, XMod, List, NoCalls, Unres}, S) ->
     Info = no_info(X2, L2, LC2, XC2, EE2, Unres, NoCalls, NoUnres),
 
     XMod1 = XMod#xref_mod{data = T, info = Info},
-    S1 = S#xref{modules = dict:store(M, XMod1, S#xref.modules)},
+    S1 = S#xref{modules = dict:store(M, XMod1, S#xref.modules),
+                ignores = ignores(M, XMod#xref_mod.attributes) ++ S#xref.ignores},
     {ok, [M], take_down(S1)};
 finish_module({modules, XMod, List}, S) ->
     ok = check_module(XMod, S),
@@ -982,6 +984,24 @@ finish_module({modules, XMod, List}, S) ->
     S1 = S#xref{modules = dict:store(M, XMod1, S#xref.modules)},
     {ok, [M], take_down(S1)}.
 
+ignores(M, Attrs) ->
+    case lists:keyfind(ignore_xref, 1, Attrs) of
+        {ignore_xref, Xs} when is_list(Xs) -> normalize_ignores(M, Xs);
+        {ignore_xref, X} -> normalize_ignores(M, [X]);
+        _ -> []
+    end.
+
+normalize_ignores(M, [{F, A} | Xs]) ->
+    [{M, F, A} | normalize_ignores(M, Xs)];
+normalize_ignores(M, [{_M, _F, _A}=X | Xs]) ->
+    [X | normalize_ignores(M, Xs)];
+normalize_ignores(M, [X | Xs]) when is_atom(X) ->
+    [X | normalize_ignores(M, Xs)];
+normalize_ignores(M, [_ | Xs]) ->
+    normalize_ignores(M, Xs);
+normalize_ignores(_M, []) ->
+    [].
+
 check_module(XMod, State) ->
     M = XMod#xref_mod.name,
     case dict:find(M, State#xref.modules) of
diff --git a/lib/tools/src/xref_reader.erl b/lib/tools/src/xref_reader.erl
index 82dcda0ce8..8dc8b8416f 100644
--- a/lib/tools/src/xref_reader.erl
+++ b/lib/tools/src/xref_reader.erl
@@ -47,9 +47,6 @@
 
 -include("xref.hrl").
 
-%% -> {ok, Module, {DefAt, LCallAt, XCallAt, LC, XC, X, Attrs, Depr, OL},
-%%         Unresolved}} | EXIT
-%% Attrs = {[], [], []} (no longer used)
 module(Module, Forms, CollectBuiltins, X, DF) ->
     Attrs = [{Attr,V} || {attribute,_Anno,Attr,V} <- Forms],
     IsAbstract = xref_utils:is_abstract_module(Attrs),
@@ -70,8 +67,7 @@ forms([], S) ->
              F ->
                  [{M, F, 0}]
          end,
-    Attrs = {[], [], []},
-    {ok, M, {DefAt, LCallAt, XCallAt, LC, XC, X, Attrs, Depr, OL}, U}.
+    {{DefAt, LCallAt, XCallAt, LC, XC, X, Depr, OL}, U}.
 
 form({attribute, _, on_load, {F, 0}}, S) ->
     S#xrefr{on_load = F};
diff --git a/lib/tools/test/xref_SUITE.erl b/lib/tools/test/xref_SUITE.erl
index 0e7ee1a811..a569dcb7fd 100644
--- a/lib/tools/test/xref_SUITE.erl
+++ b/lib/tools/test/xref_SUITE.erl
@@ -904,6 +904,8 @@ lib(Conf) when is_list(Conf) ->
           {{lib2,unknown,0},0}, {{lib3,f,0},0}]} = xref:q(s,"(Lin)LM"),
     {ok,[lib1,lib2,lib3,t,unknown]} = xref:q(s,"M"),
     {ok,[{lib2,f,0},{lib3,f,0},{t,t,0}]} = xref:q(s,"X * M"),
+    {ok, ExportsNotUsed} = xref:analyze(s, exports_not_used),
+    [{t,t,0}] = ExportsNotUsed,
     check_state(s),
 
     copy_file(fname(Dir, "lib1.erl"), fname(Dir,"lib1.beam")),
@@ -2508,12 +2510,11 @@ eval(Query, S) ->
     unsetify(Answer).
 
 add_module(S, XMod, DefAt, X, LCallAt, XCallAt, XC, LC) ->
-    Attr = {[], [], []},
     Depr0 = {[], [], [], []},
     DBad = [],
     Depr = {Depr0,DBad},
     OL = [],
-    Data = {DefAt, LCallAt, XCallAt, LC, XC, X, Attr, Depr, OL},
+    Data = {DefAt, LCallAt, XCallAt, LC, XC, X, Depr, OL},
     Unres = [],
     {ok, _Module, _Bad, State} =
     xref_base:do_add_module(S, XMod, Unres, Data),
diff --git a/lib/tools/test/xref_SUITE_data/lib_test/t.erl b/lib/tools/test/xref_SUITE_data/lib_test/t.erl
index d2bd81110e..117392d5e8 100644
--- a/lib/tools/test/xref_SUITE_data/lib_test/t.erl
+++ b/lib/tools/test/xref_SUITE_data/lib_test/t.erl
@@ -1,14 +1,31 @@
+%% %CopyrightBegin%
+%%
+%% SPDX-License-Identifier: Apache-2.0
+%%
+%% Copyright Ericsson AB 2025. All Rights Reserved.
+%%
+%% %CopyrightEnd%
 -module(t).
 
--export([t/0]).
+-export([t/0, u/0]).
+
+-ignore_xref(lib0).
+-ignore_xref(u/0).
+-ignore_xref([{lib2,unknown_ignored,0}]).
 
 t() ->
+    %% lib0: nonexisting, ignored
     %% lib1: only unknown functions used
     %% lib2: one known used, one unknown function used, one local used
     %% lib3: one known function used
+    lib0:unknown(),
     lib1:unknown(),
     lib2:f(), %% known, g/0 not used
+    lib2:unknown_ignored(),
     lib2:unknown(),
     lib2:local(),
     lib3:f(),
     unknown:unknown().
+
+u() ->
+    true.
-- 
2.51.0

openSUSE Build Service is sponsored by