查看源代码 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 的子集(未使用的函数在分析的模块中定义)。

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

Definition and use of functions

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

Some predefined analyses as subsets of all functions

表达式

模块检查和预定义分析很有用,但功能有限。有时需要更大的灵活性,例如,可能不需要对所有调用应用图分析,但某些子集也可以达到相同的效果。这种灵活性通过一种简单的语言提供。下面是一些带有注释的语言表达式,重点关注语言的元素,而不是提供有用的示例。假设分析的系统是 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"). - 从 toolbardebugger 的模块调用链(如果存在这样的链),否则为 false。调用链由模块列表表示,toolbar 是第一个元素,debugger 是最后一个元素。

  • xref:q(s, "closure E | toolbar:Mod || debugger:Mod"). - 从 toolbar 中的函数到 debugger 中的函数的所有(间接)调用。

  • xref:q(s, "(Fun) xref -> xref_base"). - 从 xrefxref_base 的所有函数调用。

  • xref:q(s, "E * xref -> xref_base"). - 与最后一个表达式相同的解释。

  • xref:q(s, "E || xref_base | xref"). - 与最后一个表达式相同的解释。

  • xref:q(s, "E * [xref -> lists, xref_base -> digraph]"). - 从 xreflists 的所有函数调用,以及从 xref_basedigraph 的所有函数调用。

  • xref:q(s, "E | [xref, xref_base] || [lists, digraph]"). - 从 xrefxref_baselistsdigraph 的所有函数调用。

  • 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% 。这意味着使用模块图时,间接使用的模块的数量大约是六倍。因此,上述问题的答案是,对于此特定分析,使用函数图绝对值得。最后,请注意,在存在未解析的调用的情况下,图可能不完整,这意味着可能存在未显示的间接使用的模块。