使用Xref删除Erlang死代码

如果你不去管死代码(就像那些不在任何地方使用的函数),它们就会堆积在大项目中。使用Xref最被低估的特性之一,你将能够检测和删除不再需要的死代码。

我们已经在这个博客里写了几篇关于我们如何大量使用Erlang/OTP来构建我们的实时竞价平台服务器的文章。

这些系统很大,到现在已经存在了很长时间。 就像任何大型旧系统一样,它们包含一些不再使用的代码片段。 需要明确的是:它们没有被破坏,它们甚至被测试所覆盖,但它们都没有在生产中使用。

在Erlang中,这些死代码表现为未使用的函数。 确切地说:它们是未使用的导出函数,因为在编译时会检测到未导出并且未使用的函数。

在一个大系统里找出未使用的导出函数是很困难的。幸运的是,Erlang/OTP已经给我们提供了一个工具来做这个事情,它就是:Xref

Xerf是一个交叉引用工具,可用于查找函数、模块、应用程序和发布之间的依赖关系。

如果你使用rebar3管理你的项目,那么你就可以运行如下简单的命令来使用Xref

1
$ rebar3 xref

如果你没有在 rebar.config 文件里为 Xref 进行任何配置的话,它将会检查你整个项目,并且进行所有可能的检查。这样的话,对于大项目来说,警告列表就会大量生成,因此,人们常常在 rebar.config 里进行如下配置:

1
2
3
4
5
{xref_checks, [
undefined_function_calls,
locals_not_used,
deprecated_function_calls
]}.

也就是说,这样的配置产生的报告就是如下三种:

  • 调用的函数不存在(undefined_function_calls)
  • 未使用未导出函数(locals_not_used)
  • 调用过期函数(deprecated_function_calls)

你可以在 里找到所有可检查的列表,但是,我想要你注意的是,后两项是编译器已经检查到的(如果你启用了正确的警告),而真正有效的检查是对 undefined_function_calls 的执行。这是一个很好的运行检查,但它不会帮助我们解决原始的死代码问题。

那么让我们来看看我们没有执行的检查。 通常,undefined_functions将报告与undefined_function_calls相同的结果,但是没有用实际的函数调用(不是很有用)。 deprecated_functionsdeprecated_function_calls也是如此。 但是,我们有exports_not_used,这正是我们正在寻找的检查。

exports_not_used 添加到我们的配置列表里,它将会报告那些我们导出了但是没有使用的函数。真是太棒了!

但是为什么没有人使用它呢?🤔

使用exports_not_used时有一些注意事项。 我现在列出它们,我会告诉你如何解决或至少让它们正常运作。

动态调用的函数

Xref将为在代码中找不到使用的每个导出函数报告一个警告。但是,Xref找不到它在哪里被使用,并不意味着实际使用函数的地方不存在。例如,Xref不能处理动态函数调用,但是它们是完全有效的使用方式。假设你有一个模块看起来像下面代码所示(别问我为什么):

1
2
3
4
5
6
7
8
-module(sample).
-exports([some_function/1, some_other_function/1]).
some_function(M) ->
M:some_other_function(an_argument).
some_other_function(X) ->
{called, X}.

而在另一些模块里,你可能这么调用:sample:some_function(sample)Xref没有足够聪明到探测到 sample:some_other_function/1 被真正使用了,因为它仅仅是通过动态评估来进行检查。上面的例子只是执行函数动态调用的其中一种方式。你可以通过如下示例看看其他方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% 经典的动态调用
Module:Function(Argument, Argument2),
% 使用 erlang:apply/3
erlang:apply(Module, Function, Arguments),
% 使用 spawn[_link]/3
erlang:spawn(Module, Function, Arguments),
% 使用 timer:tc/3
timer:tc(Module, Function, Arguments),
% 在监督者的规格说明里
{ChildName, {Module, Function, Arguments}, permanent, 5000, worker, dynamic},

注:如果添加{xref_warnings,true}. 到你的 rebar.config文件中,Xref至少会为这些无法解析的动态调用打印警告,如下所示:

1
sample: 1 unresolved call

无论如何,只要你的系统比原型稍微大一点,你就会开始在代码的各处出现导出而未使用的函数。但不要惊慌,实际上有一种方法可以避免这些警告,而且它还有一些额外的好处。这种方法就是使用ignore_xref

-ignore_xref是一个属性,你可以把它添加到你到模块中来阻止Xref对特定对函数发出警告。它的使用看起来如下所示:

1
2
3
4
5
6
7
-module(sample).
-exports([some_function/1, some_other_function/1]).
%% This function should be dynamically invoked through sample:some_function/1
-ignore_xref([{?MODULE, some_other_function, 1}]).
...

现在如果你查看OTP中Xref的文档,你不会找到有关这个属性的片言只语。这是因为它不是官方属性。ignore_xref 是 rebar3 xref(xref_run也一样) 的未公开文档属性。这个属性可以被添加到你的模块中,在当中列出那些你不想被Xref检查的函数。它的语法如下所示:

1
-ignore_xref([{module(), function(), arity()} | {module(), function()}]).

使用这种方式,你可以有效地移除掉有关那些被导出的只被动态调用的函数的警告。另外,如上例所示,你可以在这个属性的上方添加注释,这些函数将在哪里被使用。

动态生成的代码

你可能不是一个动态生成代码的发烧友,但是有时候你还真不可避免地要遇到动态生成的代码。

例如,我们在系统的几个地方使用了protobuf,导致我们使用了gpb和它的rebar3插件。当用gpb写模块的时候,它是不知道这个模块是如何被使用的,因此它就无法判断函数是否不需要被导出。这就意味着,当我们使用Xref的时候,会得到由gpb生成的所有被导出而未被使用的函数的警告。

如何避免这些警告呢?我们不能使用 -ignore_xref,因为代码不是我们手写的,而是gpb自动生成的。事实证明,还有另一种方式。 我们可以在rebar.config中使用读起来怪怪的的xref_ignores属性。 它基本上允许你拥有一个可以在任何地方被忽略的全局的函数列表。 它看起来像这样:

1
2
3
4
5
6
{xref_ignores, [
{my_gpb_generated_module, some_function, 1},
{my_gpb_generated_module, some_other_function, 0},
{my_gpb_generated_module, a_function_with_various_arities},
...
]}.

目前还没有办法忽略一个模块里的所有函数,不过我已经给这个项目提了 issue。或许你也可以把这个问题作为一个 hacktoberfest 项目来攻克。

导出函数被系统外部使用

如果你有一些函数仅仅因为当你远程登录到生产中的服务器时,会在shell中使用它们而被导出,那会怎么样?如果它们被外部脚本在你的节点执行RPC调用的时候被使用或诸如此类的情况,那会怎么样?

在这种情况下,我建议你使用 -ignore_xref,并在那里添加一个适当的注释,说明如何/何时/在哪里使用这些函数。我保证,这么做将来会有回报的。

导出函数仅用于测试

我有时候会遇到另一种不同的场景(特别是有关遗留代码的时候),就是被导出只是为了它们可以被测试。我们的想法是模拟它们或访问一些内部逻辑,否则这些内部逻辑应该隐藏在生产系统中。

首先,如果你使用 ,你就不需要导出那些函数。你可以在测试中使用非导出的函数。

现在,如果你使用通用测试或其他需要在被测模块外部编写测试的框架,那就是另一回事了。 我认为重要的是要考虑导出函数只是为了在测试中使用它们一般是不可取的,因为……

  • 如果你的函数未导出和未使用,则编译器会检测它们,如前所述,这允许你更早地发现错误。
  • 如果要添加在生产中不可用的函数并且/或者做不应该在生产中完成的功能,那么你的测试不会准确模拟真实场景,这可能会导致测试通过代码仍然无法如预期一样正常工作。

尽管如此,有时候还是没有办法解决它:你需要模拟一些外部世界看不到的东西,你需要验证一些只以非常复杂的格式暴露的数据或者检测真正难以捕获的副作用。 在这些场景中,ignore_xref以及一个简洁的注释对于未来的开发人员来说是避免意外和挫折的好工具,使得他们可以发现一个未使用的函数,决定删除它。

库接口

最后,还有另外一种情况,你确实需要导出应用程序中没有实际使用的函数:当你的应用程序是一个库时(例如,当你正在构建一个应用程序以便在其他系统中用作依赖项时)。在这种情况下,一些函数构成了应用程序的接口,它们不会被应用程序使用。它们是公开的,所以你的用户可以在他们的应用程序中调用它们。

这些函数都将被报告为未使用的导出函数,并且必须为所有这些函数编写ignore_xref / xref_ignore,这样做不是好的办法。但是真正好的办法是在测试中覆盖它们。如果你这样做了,你就有了避免警告的方法并且实际上只为导出、未使用和未测试的函数生成警告。你可以像如下方式运行Xref

1
$ rebar3 as test xref

使用test profilerebar3将把所有测试模块包含到分析中,并且由于你的接口函数将在这里使用,所以它不会警告您。

总结

虽然Xref是一个强大的工具,但是它需要一些调整来挖掘它的全部潜力。

首先,你必须使用正确的检查。我们的推荐检查列表是:

1
2
3
4
{xref_checks, [
undefined_function_calls,
exports_not_used
]}.

然后,你必须适当地使用-ignore_xref属性和xref_ignores配置参数来标识有意导出和未使用的所有函数。 如果你正在编写库,则还应该使用rebar3 as test xref来考虑分析中的测试。

有了这些,你应该会得到0个警告的报告,因此你可以确定项目中没有任何死代码。

嗯,实际上,你没有任何死函数(未使用的导出)。 但你仍然会有未使用的函数子句,未使用的case子句等形式的死代码,这些代码还不少,Xref将不会检测到这些问题。

为此,你需要一个更强大的工具:dialyzer。 我们不会在本文中介绍它,但请继续关注后续的文章。

原文链接: http://tech.adroll.com/blog/dev/2018/10/09/remove-erlang-dead-code-xref.html