IntelliJ IDEA静态分析与人的思想

不久前,我研究了用于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 } else if (options.containsKey("+" + str)) { if (cur == -1) cur = res.isEmpty() ? -1 : // B res.remove(res.size() - 1); // C if (cur != -1) res.add(cur + str.length()); // D } } return res; } 

'A''B''B'是条件语句的第一个分支)更改变量cur'D'更改列表,并且'C' (条件语句的第二个分支)更改列表和变量cur 。 对我们来说重要的是cur -1是否在其中以及列表是否为空。 也就是说,您需要监视四个状态:



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



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



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



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



这给了我们什么? 没事 为何条件res.isEmpty()始终为真是完全res.isEmpty()理解的。


实际上,我们开始做错了。 在这种情况下,单独监视每个变量的状态是不够的。 在这里,相关状态起着重要作用。 幸运的是,由于2+2 = 2*2 ,我们也只有四个:



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



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



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



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



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



这是一件非常有趣的事情。 我们无法到达左下角。 而且由于无法进入,这意味着我们无法沿着任何箭头'C'行走。 也就是说, 'C'行确实无法实现,并且可以执行'B'行。 仅当条件res.isEmpty()确实始终为真时才有可能! IntelliJ IDEA是完全正确的。 抱歉,分析人员,我以为您是越野车,但徒劳无功。 你真聪明,普通人我很难追上你。


我们的分析仪没有任何人工智能的“炒作”技术,但是使用了控制流分析和数据流分析的方法,这些方法已有不到半个世纪的历史了。 然而,他有时确实得出非常重要的结论。 但是,这是可以理解的:长期以来,最好使用机器来构建图形并在图形上行走而不是与人相处。 有一个重要的未解决问题:仅仅告诉一个人他有程序错误是不够的。 硅脑必须向生物学解释它为什么如此决定,以便生物学脑理解。 如果有人对如何做到有很好的想法,我将很高兴收到您的来信。 如果您准备好实现自己的想法,我们的团队将不会拒绝与您合作!


一项验收测试已经摆在您面前:对于此示例,应自动生成一个说明。 它可以是文本,图形,树,带有图章的图片-任何东西,只要只有一个人就能理解。


问题仍然悬而未决,该方法的作者是什么意思,以及代码的实际外观如何。 负责子系统的人告诉我,该部分已被废弃,他们自己不知道如何修复它,或者最好将其完全删除。

Source: https://habr.com/ru/post/zh-CN472932/


All Articles