6  Xref - 交叉引用工具

6 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,[...]}    

该查询要求限制外部调用(除未解析的调用外)到对那些被外部使用但既未导出也非内置函数的函数的调用(|| 运算符限制了使用的函数,而 | 运算符限制了调用函数)。- 运算符返回两个集合的差集,而将在下面使用的 + 运算符返回两个集合的并集。

预定义变量 XUXB 以及其他几个变量之间的关系值得详细说明。参考手册提到了两种表达所有函数集合的方法,一种侧重于它们是如何定义的:X + L + B + U,另一种侧重于它们是如何使用的:UU + LU + XU。参考手册还提到了关于这些变量的一些 事实

  • F 等于 L + X(定义的函数是局部函数和外部函数);
  • UXU 的子集(未知函数是外部使用函数的子集,因为编译器确保局部使用函数已定义);
  • BXU 的子集(对内置函数的调用在定义上始终是外部调用,并且会忽略未使用的内置函数);
  • LUF 的子集(局部使用函数是局部函数或导出函数,同样由编译器确保);
  • UU 等于 F - (XU + LU)(未使用函数是既未在外部使用也未在局部使用的定义函数);
  • UUF 的子集(未使用函数在分析的模块中定义)。

使用这些事实,下图中的两个小圆圈可以合并。

IMAGE MISSING

图 6.1:  函数的定义和使用

在这样的圆圈中标记查询的变量通常会使情况更清楚。下图说明了这一点,其中展示了一些预定义分析。请注意,仅由局部函数使用的局部函数未在 locals_not_used 圆圈中标记。

IMAGE MISSING

图 6.2:  一些预定义分析作为所有函数的子集

模块检查和预定义分析很有用,但受到限制。有时需要更大的灵活性,例如,可能不需要对所有调用应用图形分析,但某些子集也能达到同样的效果。这种灵活性由一个简单的语言提供。以下是一些语言的表达式,带有注释,重点介绍语言的元素,而不是提供有用的示例。假设要分析的系统是 OTP,因此为了运行这些查询,首先评估以下调用

    xref:start(s).
    xref:add_release(s, code:root_dir()).    
xref 模块的所有函数。
xref 模块的所有导出函数。交集运算符 * 的第一个操作数隐式地转换为第二个操作数的更特殊类型。
Tools 应用程序的所有模块。
所有名称以 xref_ 开头的模块。
从导出函数的调用数量。
对局部函数的所有外部调用。
同时具有外部版本和局部版本的所有调用。
上一个示例中进行局部调用的行。
倒数第二个示例中进行外部调用的行。
某些模块内的外部调用。
Kernel 应用程序中的所有调用。
Kernel 应用程序中的所有直接和间接调用。间接调用的调用函数和使用函数都在 kernel 应用程序的模块中定义,但间接调用可能使用 kernel 应用程序外部的某些函数。
toolbardebugger 的模块调用链(如果存在这样的链),否则为 false。调用链用模块列表表示,toolbar 是第一个元素,debugger 是最后一个元素。
toolbar 中的函数到 debugger 中的函数的所有(间)接调用。
xrefxref_base 的所有函数调用。
与上一个表达式相同的解释。
与上一个表达式相同的解释。
xreflists 的所有函数调用,以及从 xref_basedigraph 的所有函数调用。
xrefxref_baselistsdigraph 的所有函数调用。
相互调用图的所有强连通分量。每个分量都是一组相互(间接)调用的导出函数或未使用局部函数。
digraph 模块的所有导出函数,这些函数被 digraph 中的某些函数(间接)使用。
解释留作练习。

列表 图形表示 用于分析直接调用,而 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%(百分比)。这意味着使用模块图时,间接使用模块的数量大约是使用函数图的六倍。因此,上述问题的答案是,对于这种特定的分析,使用函数图绝对值得。