6. Xref - The Cross Reference Tool
6 Xref-交叉参考工具
Xref是一个交叉引用工具,可用于查找函数、模块、应用程序和发行版之间的依赖关系。它通过分析定义的函数和函数调用来实现这一点。
为了使Xref易于使用,有一些预定义的分析可以执行一些常见的任务。通常,可以检查模块或发行版对未定义函数的调用。对于稍微高级一些的用户,有一种较小但相当灵活的语言,可用于选择分析系统的部分以及对选定的调用进行一些简单的图形分析。
以下部分展示了Xref的一些特性,首先是模块检查和预定义分析。然后,遵循可以在一读时跳过的示例;并不是所有使用的概念都被解释,并且假设reference manual
至少已经被浏览过了。
6.1模块检查
假设我们想检查以下模块:
-module(my_module).
-export([t/1]).
t(A) ->
my_module:t2(A).
t2(_) ->
true.
交叉参考数据是从束文件中读取的,所以检查编辑模块的第一步是编译它:
1> c(my_module, debug_info).
./my_module.erl:10: Warning: function t2/1 is unused
{ok, my_module}
大debug_info
选项确保BEAM文件包含调试信息,这使得查找未使用的本地函数成为可能。
现在可以检查模块是否有调用到deprecated functions
,呼叫undefined functions
,对于未使用的本地功能:
2> xref:m(my_module)
[{deprecated,[]},
{undefined,[{{my_module,t,1},{my_module,t2,1}}]},
{unused,[{my_module,t2,1}]}]
m/1
也适用于检查将要加载到正在运行的系统中的模块的BEAM文件没有调用任何未定义的函数。在任何一种情况下,代码服务器的代码路径(请参阅模块code
)都用于查找导出外部调用的函数的模块,这些函数不是由检查的模块本身导出的,如此调用library modules
。
6.2预定义分析
在最后一个示例中,要分析的模块作为m/1
,代码路径为%28隐式%29用于library path
.在本例中,xref server
将被使用,这使得分析应用程序和发行版成为可能,也可以显式地选择库路径。
每个Xref服务器都有一个唯一的名称。在创建服务器时给出了名称:
1> xref:start(s).
{ok,<0.27.0>}
接下来将要分析的系统添加到Xref服务器。这里的系统将是OTP,所以不需要库路径。否则,在分析使用OTP的系统时,通常将OTP模块设置为库模块,方法是将库路径设置为默认的OTP代码路径(或code_path
参见reference manual
)。默认情况下,添加分析模块时会输出读取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()
包含应用程序;其效果是读取所有应用程序%27波束文件。
现在很容易检查对未定义函数的调用的发布:
4> xref:analyze(s, undefined_function_calls).
{ok, [...]}
我们现在可以继续进行进一步的分析,或者删除Xref服务器:
5> xref:stop(s).
对未定义函数调用的检查是预定义分析的一个示例,可能是最有用的分析。其他示例是找到未使用的本地函数或调用某些给定函数的函数的分析。见analyze/2,3
函数用于完整的预定义分析列表。
每个预定义的分析都是query
,一种微小语言的句子,提供交叉引用数据作为predefined variables
因此,对未定义函数的调用的检查可以表示为查询:
4> xref:q(s, "(XC - UC) || (XU - X - B)").
{ok,[...]}
查询请求外部调用的限制,但对外部使用但既未导出也未内置函数%28的函数的未解析调用除外。||
运算符限制使用的函数,而|
运算符限制调用函数%29。大-
运算符返回两个集合的差,并且+
下面使用的运算符返回两个集合的合并。
预定义变量之间的关系XU
,,,X
,,,B
还有几个值得详细阐述。参考手册提到了表达所有功能集的两种方法,一种是侧重于如何定义它们:X + L + B + U
,并且关注如何使用它们:UU + LU + XU
.参考资料还提到了一些facts
关于变量:
F
等于L + X
%28定义函数为本地函数,外部函数为%29;
U
的子集XU
%28未知函数是外部使用函数的子集,因为编译器确保本地使用的函数定义为%29;
B
的子集XU
对内置函数的%28调用按定义总是外部调用,未使用的内建函数被忽略%29;
LU
的子集F
%28本地使用的函数要么是本地函数,要么是导出函数,同样由编译器%29确保;
UU
等于F - (XU + LU)
%28未使用的函数是定义的函数,既不是外部使用的,也不是本地使用的%29;
UU
的子集F
%28未使用的函数在分析模块%29中定义。
利用这些事实,可以将下图中的两个小圆圈结合起来。
图6.1:函数的定义和使用
在这样一个圆中标记查询的变量通常是明确的。下面的图片说明了这一点,用于一些预定义的分析。注意,仅由本地函数使用的本地函数不会在locals_not_used
绕圈。
图6.2:一些预定义分析作为所有函数的子集
6.3表达式
模块检查和预定义的分析是有用的,但有限的。有时需要更多的灵活性,例如,可能不需要对所有调用应用图形分析,但某些子集也会做得同样好。这种灵活性提供了一种简单的语言。下面是语言的一些表达和评论,重点放在语言的元素上,而不是提供有用的例子。被分析的系统假定为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").工具应用程序的所有模块。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").内核应用程序中的所有调用。xref:q(s, "closure E | kernel || kernel").内核应用程序中的所有直接和间接调用。间接调用的调用和使用函数都是在内核应用程序的模块中定义的,但内核应用程序之外的一些函数可能是通过间接调用来实现的。xref:q(s, "{toolbar,debugger}:Mod of ME").从toolbar到debugger,如果存在这样的链,则为false调用链由模块列表表示,toolbar是第一个元素debugger最后一个元素。xref:q(s, "closure E | toolbar:Mod || debugger:Mod").中函数的所有%28 in%29直接调用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").调用图的所有强连通分量。每个组件都是一组导出或未使用的本地函数,它们直接调用彼此的%28 in%29。xref:q(s, "X * digraph * range (closure (E | digraph) | (L * digraph))").的所有导出函数digraph中的某些函数直接使用%28 in%29的模块。digraph...xref:q(s, "L * yeccparser:Mod - range (closure (E |yeccparser:Mod) | (X * yeccparser:Mod))").这种解释只是一种练习。
6.4图分析
名单representation of graphs
用于分析直接调用,而digraph
表示适用于分析间接呼叫。限制运算符%28|
,,,||
和|||
%29是唯一接受这两种表示的操作符。这意味着,为了分析使用限制的间接调用,closure
运算符%28创建digraph
图%29的表示必须显式应用。
作为分析间接调用的示例,下面的Erlang函数试图回答以下问题:如果我们想知道某些模块%28s%29间接使用了哪些模块,那么使用function graph
而不是模块图?回想一下,如果M1中有一些函数调用M2中的某个函数,则称为模块M1。如果我们可以使用这个小得多的模块图,那就太好了,因为它也可以在重量轻的情况下使用。modules
mode
Xref服务器。
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的外部参照服务器时,返回值接近84(百分比)。这意味着在使用模块图时,间接使用的模块数量大约增加六倍。所以上述问题的答案是,对于这种特定的分析使用函数图是绝对值得的。最后,请注意,如果存在未解决的调用,图表可能不完整,这意味着可能会有间接使用的模块不显示。