为什么Dialyzer没有发现此代码错误?

时间:2012-08-07 18:08:15

标签: erlang static-analysis dialyzer

我已根据this教程创建了以下代码段。最后两行(feed_squid(FeederRP)feed_red_panda(FeederSquid))显然违反了定义的约束,但Dialyzer发现它们没问题。这是非常令人失望的,因为这正是我想要用执行静态分析的工具捕获的错误类型。

教程中提供了解释:

  

在使用错误的馈线调用功能之前,它们是   首先用正确的方式打电话。截至R15B01,Dialyzer不会   使用此代码查找错误。观察到的行为就是那么快   当一个给定函数的调用在函数体内成功时,   Dialyzer将忽略同一代码单元中的后续错误。

这种行为的理由是什么?我理解成功打字背后的哲学是“从不哭狼”,但在目前的情况下,Dialyzer明显地忽略了故意定义的函数规范(在它看到之前已正确调用函数之后)。我知道代码不会导致运行时崩溃。我可以以某种方式迫使Dialyzer始终认真对待我的功能规格吗?如果没有,是否有可以做到的工具?

-module(zoo).
-export([main/0]).

-type red_panda() :: bamboo | birds | eggs | berries.
-type squid() :: sperm_whale.
-type food(A) :: fun(() -> A).

-spec feeder(red_panda) -> food(red_panda());
            (squid) -> food(squid()).
feeder(red_panda) ->
    fun() ->
            element(random:uniform(4), {bamboo, birds, eggs, berries})
    end;
feeder(squid) ->
    fun() -> sperm_whale end.

-spec feed_red_panda(food(red_panda())) -> red_panda().
feed_red_panda(Generator) ->
    Food = Generator(),
    io:format("feeding ~p to the red panda~n", [Food]),
    Food.

-spec feed_squid(food(squid())) -> squid().
feed_squid(Generator) ->
    Food = Generator(),
    io:format("throwing ~p in the squid's aquarium~n", [Food]),
    Food.

main() ->
    %% Random seeding
    <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
    random:seed(A, B, C),
    %% The zoo buys a feeder for both the red panda and squid
    FeederRP = feeder(red_panda),
    FeederSquid = feeder(squid),
    %% Time to feed them!
    feed_squid(FeederSquid),
    feed_red_panda(FeederRP),
    %% This should not be right!
    feed_squid(FeederRP),
    feed_red_panda(FeederSquid).

2 个答案:

答案 0 :(得分:4)

最小化示例我有这两个版本:

Dialyzer可以捕获的第一个:

-module(zoo).
-export([main/0]).

-type red_panda_food() :: bamboo.
-type squid_food()     :: sperm_whale.

-spec feed_squid(fun(() -> squid_food())) -> squid_food().
feed_squid(Generator) -> Generator().

main() ->
    %% The zoo buys a feeder for both the red panda and squid
    FeederRP = fun() -> bamboo end,
    FeederSquid = fun() -> sperm_whale end,
    %% CRITICAL POINT %%
    %% This should not be right!
    feed_squid(FeederRP),
    %% Time to feed them!
    feed_squid(FeederSquid)

然后没有警告的那个:

    [...]
    %% CRITICAL POINT %%
    %% Time to feed them!
    feed_squid(FeederSquid)
    %% This should not be right!
    feed_squid(FeederRP).

Dialyzer警告它可以捕获的版本是:

zoo.erl:7: The contract zoo:feed_squid(fun(() -> squid_food())) -> squid_food() cannot be right because the inferred return for feed_squid(FeederRP::fun(() -> 'bamboo')) on line 15 is 'bamboo'
zoo.erl:10: Function main/0 has no local return

......并且更倾向于相信自己对用户更严格的规范的判断。

对于它没有捕获的版本,Dialyzer假设feed_squid/1参数的类型fun() -> bamboofun() -> none()的超类型(一个会崩溃的闭包,如果没有调用, feed_squid/1,仍然是一个有效的论点)。在推断出类型之后,Dialyzer无法知道是否在函数内实际调用了传递的闭包。

如果使用选项-Woverspecs,透析器仍然会发出警告:

zoo.erl:7: Type specification zoo:feed_squid(fun(() -> squid_food())) -> squid_food() is a subtype of the success typing: zoo:feed_squid(fun(() -> 'bamboo' | 'sperm_whale')) -> 'bamboo' | 'sperm_whale'

...警告没有任何东西阻止此功能处理其他进纸器或任何给定的进纸器!如果代码确实检查闭包的预期输入/输出,而不是通用,那么我很确定Dialyzer会抓住滥用。从我的角度来看,如果您的实际代码检查错误的输入而不依赖于类型规范和Dialyzer(它从未承诺无论如何都能找到所有错误),那就更好了。

警告:深刻的ESOTERIC部分跟随!

在第一种情况下报告错误但不在第二种情况下报告错误的原因与模块本地细化的进度有关。最初,函数feed_squid/1成功输入(fun() -> any()) -> any()。在第一种情况下,函数feed_squid/1将首先仅使用FeederRP进行优化,并且肯定会返回bamboo,立即伪造规范并停止对main/0的进一步分析。在第二个中,函数feed_squid/1将首先仅使用FeederSquid进行优化,并且肯定会返回sperm_whale,然后使用FeederSquidFeederRP进行优化并返回sperm_whalebamboo。然后使用FeederRP调用时,成功键入的预期返回值为sperm_whalebamboo。规范然后承诺它将是sperm_whale并且Dialyzer接受它。另一方面,论证应该是fun() -> bamboo | sperm_whale成功 - 打字方式,它是fun() -> bamboo所以只留下fun() -> bamboo。当根据规范(fun() -> sperm_whale)检查时,Dialyzer假定参数可以是fun() -> none()。如果您从未在feed_squid/1内调用传递的函数(Dialyzer的类型系统不保留的信息),并且您在规范中保证将始终返回sperm_whale,那么一切都很好!

什么可以'修复'是为了扩展类型系统,以便注意当一个作为参数传递的闭包实际上在一个调用中使用时,并在一个“生存”某些部分的唯一方法的情况下发出警告。类型推断应为fun(...) -> none()

答案 1 :(得分:2)

(注意,我在这里推测一下。我没有详细阅读透析器代码。)

“正常”完整类型检查器的优点是类型检查是可判定的。当类型检查器终止时,我们可以询问“此程序输入是否良好”并获得“是”或“否”。透析器不是这样。它主要是解决停止问题的业务。结果是会有一些明显错误的程序,但仍然会透过透析器的把手。

但是,这不是其中一种情况:)

问题是双重的。成功类型说“如果此函数通常终止 ,它的类型是什么?”。在上文中,对于任意类型feed_red_panda/1匹配fun (() -> A)的任何参数,我们的A函数终止。我们可以致电feed_red_panda(fun erlang:now/0),它也应该有效。因此,我们对main/0中的函数的两次调用不会引起问题。他们都终止了。

问题的第二部分是“你违反了规范吗?”。请注意,通常,透析器中不使用规格作为事实。它推断出类型本身并使用推理模式而不是您的规范。无论何时调用函数,都会使用参数进行注释。在我们的例子中,它将使用两种生成器类型进行注释:food(red_panda()), food(squid())。然后基于这些注释进行函数本地分析,以确定您是否违反了规范。由于注释中存在正确的参数,因此我们必须假设该函数在某些部分代码中正确使用。它也被称为鱿鱼可能是一个代码的工件,由于其他情况从未被调用。由于我们是函数本地的,我们不知道并给程序员带来疑问。

如果您将代码更改为仅使用squid-generator进行错误调用,那么我们会发现规范差异。因为我们知道唯一可能的呼叫站点违反了规范。如果您将错误的调用移到另一个函数,则 也找不到。因为注释仍然在函数上,而不在调用站点上。

可以想象透析器的未来变体可以解释每个呼叫站点可以单独处理的事实。由于透析器随着时间的推移也在变化,因此未来可能会处理这种情况。但目前,这是其中一个错误。

关键是要注意透析器不能用作“良好类型的检查器”。您无法使用它来强制执行程序结构。你需要自己做。如果你想要更多的静态检查,可能会为Erlang编写一个类型检查器并在你的代码库的一部分上运行它。但是,您将遇到代码升级和分发问题,这些问题并不容易处理。