查看源代码 Xref - 交叉引用工具
Xref 是一个交叉引用工具,可用于查找函数、模块、应用程序和发布版本之间的依赖关系。它通过分析定义的函数和函数调用来实现这一点。
为了使 Xref 易于使用,预定义了一些执行常见任务的分析。通常,可以检查模块或发布版本是否存在对未定义函数的调用。对于稍微高级的用户,有一个小型但灵活的语言,可用于选择分析系统的部分,并对选定的调用执行一些简单的图分析。
以下部分展示了 Xref 的一些功能,首先是模块检查和预定义分析。然后是一些可以在初次阅读时跳过的示例;并非所有使用的概念都已解释,并且假设您至少浏览过参考手册。
模块检查
假设我们要检查以下模块
-module(my_module).
-export([t/1]).
t(A) ->
my_module:t2(A).
t2(_) ->
true.
交叉引用数据从 BEAM 文件中读取,因此检查已编辑模块的第一步是编译它
1> c(my_module, debug_info).
./my_module.erl:10: Warning: function t2/1 is unused
{ok, my_module}
debug_info
选项确保 BEAM 文件包含调试信息,这使得查找未使用的局部函数成为可能。
现在可以检查该模块是否存在对已弃用函数的调用、对未定义函数的调用以及未使用的局部函数
2> xref:m(my_module)
[{deprecated,[]},
{undefined,[{{my_module,t,1},{my_module,t2,1}}]},
{unused,[{my_module,t2,1}]}]
m/1
也适用于检查即将加载到运行系统中的模块的 BEAM 文件是否调用任何未定义的函数。在任何一种情况下,代码服务器的代码路径(请参阅模块 code
)都用于查找导出外部调用函数但未被检查模块本身导出的模块,即所谓的库模块。
预定义分析
在最后一个示例中,要分析的模块作为 m/1
的参数给出,代码路径(隐式地)用作库路径。在此示例中,将使用 xref 服务器,这使得分析应用程序和发布版本成为可能,也可以显式选择库路径。
每个 Xref 服务器都由一个唯一的名称引用。该名称在创建服务器时给出
1> xref:start(s).
{ok,<0.27.0>}
接下来,要分析的系统被添加到 Xref 服务器。这里系统将是 OTP,因此不需要库路径。否则,当分析使用 OTP 的系统时,通常通过将库路径设置为默认 OTP 代码路径(或 code_path
,请参阅参考手册)来使 OTP 模块成为库模块。默认情况下,读取的 BEAM 文件的名称和警告在添加分析的模块时输出,但可以通过设置某些选项的默认值来避免这些消息
2> xref:set_default(s, [{verbose,false}, {warnings,false}]).
ok
3> xref:add_release(s, code:lib_dir(), {name, otp}).
{ok,otp}
add_release/3
假设由 code:lib_dir()
返回的库目录的所有子目录都包含应用程序;其效果是读取应用程序的所有 BEAM 文件。
现在很容易检查发布版本是否存在对未定义函数的调用
4> xref:analyze(s, undefined_function_calls).
{ok, [...]}
我们现在可以继续进行进一步的分析,或者我们可以删除 Xref 服务器
5> xref:stop(s).
检查对未定义函数的调用是预定义分析的一个示例,可能是最有用的一个。其他示例是查找未使用的局部函数或调用某些给定函数的分析。有关预定义分析的完整列表,请参阅 analyze/2,3 函数。
每个预定义分析都是一个查询的简写,这是一个小语言的句子,它将交叉引用数据作为预定义变量的值提供。因此,对未定义函数的调用的检查可以声明为一个查询
4> xref:q(s, "(XC - UC) || (XU - X - B)").
{ok,[...]}
该查询要求限制外部调用,除了对外部使用但既不是导出函数也不是内置函数的未解析调用(||
运算符限制使用的函数,而 |
运算符限制调用的函数)。-
运算符返回两个集合的差,而下面使用的 +
运算符返回两个集合的并集。
预定义变量 XU
、X
、B
和其他一些变量之间的关系值得详细说明。参考手册提到了两种表示所有函数集合的方法,一种侧重于如何定义它们:X + L + B + U
,另一种侧重于如何使用它们:UU + LU + XU
。参考手册还提到了一些关于变量的事实
F
等于L + X
(已定义的函数是局部函数和外部函数);U
是XU
的子集(未知函数是外部使用的函数的子集,因为编译器确保局部使用的函数已定义);B
是XU
的子集(根据定义,对内置函数的调用始终是外部的,并且未使用的内置函数将被忽略);LU
是F
的子集(局部使用的函数是局部函数或导出函数,同样由编译器确保);UU
等于F - (XU + LU)
(未使用的函数是已定义的函数,这些函数既不在外部使用,也不在局部使用);UU
是F
的子集(未使用的函数在分析的模块中定义)。
使用这些事实,可以将下图中的两个小圆圈组合起来。
通常,在这样的圆圈中标记查询的变量会更清晰。下图说明了一些预定义分析的这种情况。请注意,仅由局部函数使用的局部函数未在 locals_not_used
圆圈中标记。
表达式
模块检查和预定义分析很有用,但功能有限。有时需要更大的灵活性,例如,可能不需要对所有调用应用图分析,但某些子集也可以达到相同的效果。这种灵活性通过一种简单的语言提供。下面是一些带有注释的语言表达式,重点关注语言的元素,而不是提供有用的示例。假设分析的系统是 OTP,因此为了运行查询,首先评估这些调用
xref:start(s).
xref:add_release(s, code:root_dir()).
xref:q(s, "(Fun) xref : Mod").
-xref
模块的所有函数。xref:q(s, "xref : Mod * X").
-xref
模块的所有导出函数。交集运算符*
的第一个操作数隐式转换为第二个操作数的更特殊类型。xref:q(s, "(Mod) tools").
- Tools 应用程序的所有模块。xref:q(s, '"xref_.*" : Mod').
- 所有名称以xref_
开头的模块。xref:q(s, "# E | X ").
- 来自导出函数的调用次数。xref:q(s, "XC || L ").
- 所有对局部函数的外部调用。xref:q(s, "XC * LC").
- 所有既有外部版本又有局部版本的调用。xref:q(s, "(LLin) (LC * XC)").
- 上一个示例中发出局部调用的行。xref:q(s, "(XLin) (LC * XC)").
- 上一个示例中发出外部调用的行。xref:q(s, "XC * (ME - strict ME)").
- 某些模块中的外部调用。xref:q(s, "E ||| kernel").
- Kernel 应用程序中的所有调用。xref:q(s, "closure E | kernel || kernel").
- Kernel 应用程序中的所有直接和间接调用。间接调用的调用函数和使用的函数都在内核应用程序的模块中定义,但是间接调用可能会使用内核应用程序外部的某些函数。xref:q(s, "{toolbar,debugger}:Mod of ME").
- 从toolbar
到debugger
的模块调用链(如果存在这样的链),否则为false
。调用链由模块列表表示,toolbar
是第一个元素,debugger
是最后一个元素。xref:q(s, "closure E | toolbar:Mod || debugger:Mod").
- 从toolbar
中的函数到debugger
中的函数的所有(间接)调用。xref:q(s, "(Fun) xref -> xref_base").
- 从xref
到xref_base
的所有函数调用。xref:q(s, "E * xref -> xref_base").
- 与最后一个表达式相同的解释。xref:q(s, "E || xref_base | xref").
- 与最后一个表达式相同的解释。xref:q(s, "E * [xref -> lists, xref_base -> digraph]").
- 从xref
到lists
的所有函数调用,以及从xref_base
到digraph
的所有函数调用。xref:q(s, "E | [xref, xref_base] || [lists, digraph]").
- 从xref
和xref_base
到lists
和digraph
的所有函数调用。xref:q(s, "components EE").
- 跨函数调用图的所有强连通分量。每个分量是一组相互(直接或间接)调用的已导出或未使用的局部函数。xref:q(s, "X * digraph * range (closure (E | digraph) | (L * digraph))").
-digraph
模块中某些函数(直接或间接)使用的digraph
模块的所有已导出函数。xref:q(s, "L * yeccparser:Mod - range (closure (E |
yeccparser:Mod) | (X * yeccparser:Mod))").
- 其含义留给读者自行理解。
图分析
图的列表表示用于分析直接调用,而digraph
表示适合分析间接调用。限制运算符(|
、||
和 |||
)是唯一接受两种表示形式的运算符。这意味着为了使用限制来分析间接调用,必须显式应用closure
运算符(它创建图的digraph
表示)。
以下面的Erlang函数为例,分析间接调用,该函数尝试回答以下问题:如果我们想知道哪些模块被某些模块间接使用,那么使用函数图而不是模块图是否值得?回想一下,如果 M1 中有某个函数调用 M2 中的某个函数,则称模块 M1 调用模块 M2。如果我们能使用小得多的模块图,那就太好了,因为它也可以在 Xref 服务器的轻量级 modules
模式中使用。
t(S) ->
{ok, _} = xref:q(S, "Eplus := closure E"),
{ok, Ms} = xref:q(S, "AM"),
Fun = fun(M, N) ->
Q = io_lib:format("# (Mod) (Eplus | ~p : Mod)", [M]),
{ok, N0} = xref:q(S, lists:flatten(Q)),
N + N0
end,
Sum = lists:foldl(Fun, 0, Ms),
ok = xref:forget(S, 'Eplus'),
{ok, Tot} = xref:q(S, "# (closure ME | AM)"),
100 * ((Tot - Sum) / Tot).
代码注释
- 我们想要找到函数图的闭包到模块的缩减。直接的表达方式是
(Mod) (closure E | AM)
,但那样的话,我们必须在内存中表示 E 的所有传递闭包。相反,对于每个分析的模块,会找到间接使用的模块的数量,并计算所有模块的总和。 - 使用用户变量来保存函数图的
digraph
表示,以便在多个查询中使用。原因是效率。与=
运算符相反,:=
运算符会保存一个值以供后续分析使用。这里可能要注意的是,查询中相等的子表达式只会计算一次;=
不能用于加速。 Eplus | ~p : Mod
。|
运算符将第二个操作数转换为第一个操作数的类型。在这种情况下,模块将转换为模块的所有函数。必须为模块指定一个类型(: Mod
),否则像kernel
这样的模块将被转换为具有相同名称的应用程序的所有函数;在有歧义的情况下,会使用最通用的常量。- 由于我们只对一个比率感兴趣,因此使用了计算操作数元素的一元运算符
#
。 它不能应用于图的digraph
表示。 - 我们可以使用与函数图类似的循环来查找模块图闭包的大小,但是由于模块图要小得多,因此更直接的方法是可行的。
当 Erlang 函数 t/1
应用于加载了当前版本 OTP 的 Xref 服务器时,返回的值接近 84% 。这意味着使用模块图时,间接使用的模块的数量大约是六倍。因此,上述问题的答案是,对于此特定分析,使用函数图绝对值得。最后,请注意,在存在未解析的调用的情况下,图可能不完整,这意味着可能存在未显示的间接使用的模块。