不久前,我研究了用于Java代码的IntelliJ IDEA静态分析器的输出,并遇到了一个有趣的案例。 由于相应的代码段不是开源的,因此我将其匿名化并使其与外部依赖关系解除绑定。 我们假设他看起来像这样:
private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str); } else if (options.containsKey("+" + str)) { if (cur == -1) cur = res.isEmpty() ? -1 : res.remove(res.size() - 1); if (cur != -1) res.add(cur + str.length()); } } return res; }
代码就像代码,正在转换某些东西,正在完成某件事,但是静态分析器不喜欢它。 在这里,我们看到两个警告:

在表达式res.isEmpty()
IDE表示条件始终为true,而在cur
,赋值是没有意义的,因为该变量中已经存在相同的值。 显而易见,分配问题是第一个问题的直接结果。 如果res.isEmpty()
确实始终为true,则将字符串简化为
if (cur == -1) cur = -1;
这确实是多余的。 但是为什么这个表达总是正确的? 毕竟, res
是一个列表,它以相同的周期填充。 循环的迭代次数以及我们要进入的分支取决于IDE无法知道的输入参数。 我们可以在上一次迭代中向res
添加一个元素,然后列表将不会为空。
我是第一次看到这段代码,并花了很多时间来处理这种情况。 起初,我几乎确信我遇到了分析仪中的错误,因此必须对其进行修复。 让我们看看是否是这样。
首先,我们将标记方法状态更改的所有行。 这是对cur
变量的更改或对res
列表的更改:
private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str);
行'A'
和'B'
( 'B'
是条件语句的第一个分支)更改变量cur
, 'D'
更改列表,并且'C'
(条件语句的第二个分支)更改列表和变量cur
。 对我们来说重要的是cur
-1是否在其中以及列表是否为空。 也就是说,您需要监视四个状态:

如果字符串'A'
在此之前为-1
, cur
改为cur
。 而且我们不知道结果是否为-1
。 因此,有两种选择:

字符串'B'
仅在cur
为-1
也有效。 同时,正如我们已经注意到的,原则上,她什么也不做。 但是,尽管如此,我们还是注意到了这一点:

与前面的字符串一样,字符串'C'
与cur == -1
并任意更改(例如'A'
)。 但是同时,它仍然可以将非空列表res
变为空,或者如果有多个元素,则将其保留为非空。

最后,字符串'D'
增加了列表的大小:它可以从空变为非空,或者增加非空。 它不能将非空变为空:

这给了我们什么? 没事 为何条件res.isEmpty()
始终为真是完全res.isEmpty()
理解的。
实际上,我们开始做错了。 在这种情况下,单独监视每个变量的状态是不够的。 在这里,相关状态起着重要作用。 幸运的是,由于2+2 = 2*2
,我们也只有四个:

用双边框标记了进入方法时的初始状态。 好吧,再试一次。 'A'
更改或保存任何res
cur
, res
不变:

'B'
仅与cur == -1 && res.isEmpty()
,不执行任何操作。 添加:

'C'
仅与cur == -1 && !res.isEmpty()
。 同时, cur
和res
任意更改:在'C'
我们最终处于任何状态:

最后, 'D'
可以以cur != -1 && res.isEmpty()
,并使列表为非空,或者以cur != -1 && !res.isEmpty()
并停留在该位置:

乍一看,它似乎变得越来越糟:该图变得更加复杂,并且不清楚如何使用它。 但是实际上,我们已经接近解决方案。 现在,箭头显示了执行方法的全部可能流程。 因为我们知道我们从什么状态开始,所以让我们沿着箭头走一下:

这是一件非常有趣的事情。 我们无法到达左下角。 而且由于无法进入,这意味着我们无法沿着任何箭头'C'
行走。 也就是说, 'C'
行确实无法实现,并且可以执行'B'
行。 仅当条件res.isEmpty()
确实始终为真时才有可能! IntelliJ IDEA是完全正确的。 抱歉,分析人员,我以为您是越野车,但徒劳无功。 你真聪明,普通人我很难追上你。
我们的分析仪没有任何人工智能的“炒作”技术,但是使用了控制流分析和数据流分析的方法,这些方法已有不到半个世纪的历史了。 然而,他有时确实得出非常重要的结论。 但是,这是可以理解的:长期以来,最好使用机器来构建图形并在图形上行走而不是与人相处。 有一个重要的未解决问题:仅仅告诉一个人他有程序错误是不够的。 硅脑必须向生物学解释它为什么如此决定,以便生物学脑理解。 如果有人对如何做到有很好的想法,我将很高兴收到您的来信。 如果您准备好实现自己的想法,我们的团队将不会拒绝与您合作!
一项验收测试已经摆在您面前:对于此示例,应自动生成一个说明。 它可以是文本,图形,树,带有图章的图片-任何东西,只要只有一个人就能理解。
问题仍然悬而未决,该方法的作者是什么意思,以及代码的实际外观如何。 负责子系统的人告诉我,该部分已被废弃,他们自己不知道如何修复它,或者最好将其完全删除。