作为更改,今天我们将向您介绍一些有关开发和最终确定PVS-Studio Java诊断规则的过程。让我们看看为什么旧的分析器触发器在发布之间不会浮动太多,而新的触发器却不太疯狂。而且,我们将破坏一些“ javists的计划”,并展示在下一个发行版的诊断帮助下发现的一些美丽的(并非如此)错误。
诊断和自测试程序开发过程
自然,每个新的诊断规则都从一个想法开始。而且由于Java分析器是PVS-Studio开发中最年轻的方向,因此我们基本上从C / C ++和C#部门窃取了这些想法。但是,并非所有事情都那么糟糕:我们还添加了我们自己(包括用户-谢谢!)发明的规则,以便以后相同的部门可以从我们这里窃取它们。正如他们所说的那样。
在大多数情况下,代码中规则的完全执行实际上是一项流水线任务。您创建了一个包含几个综合示例的文件,动手标记应该出现错误的位置,并准备好使用调试器来遍历语法树,直到您无聊并涵盖了所有已发现的情况。有时,规则变得非常简单(例如,V6063包含几行内容),有时您必须考虑足够长的逻辑时间。
但是,这仅仅是开始。如您所知,我们并不特别喜欢综合示例,因为它们很难反映真实项目中分析器触发器的类型。顺便说一下,我们单元测试中的大多数示例都是从真实项目中提取的-几乎不可能自己发明所有可能的案例。单元测试还使我们不会丢失文档中示例的触发器。有先例,是的,只有嘘。
因此,必须首先以某种方式找到实际项目中的积极因素。而且您还需要以某种方式检查:
- 该规则将不会因为“疯狂”的解决方案很普遍而导致的开源疯狂。
- ( - , );
- data-flow ( ) - ;
- open-source ;
- over 9000%;
- "" , ;
- .
通常,在这里,就像骑马的骑士(有些a脚,但我们正在努力),SelfTester脱颖而出。它的主要也是唯一的任务是自动检查一堆项目,并显示相对于版本控制系统中的“参考”已添加,消失或更改了哪些触发器。简而言之,为分析器报告提供差异,并在项目中显示相应的代码。目前,用于Java的SelfTester正在测试62个有胡子版本的开源项目,例如DBeaver,Hibernate和Spring。所有项目的完整运行需要2-2.5小时,这无疑是痛苦的,但无能为力。
在上面的屏幕截图中,“绿色”项目是没有任何更改的项目。手动检查“红色”项目中的每个差异,如果正确,则通过相同的“批准”按钮进行确认。顺便说一下,只有在SelfTester提供纯绿色结果的情况下,才会构建分析仪分发套件。通常,这就是我们保持不同版本之间结果一致性的方式。
除了保持结果的一致性外,SelfTester还使我们能够在诊断发布之前消除大量误报。典型的开发模式如下所示:
- , . , " double-checked locking" ;
- SelfTester-, ;
- , -;
- SelfTester- , ;
- 3-4, ;
- , , ( , );
- , master.
幸运的是,完整的SelfTester运行非常罕见,您不必经常等待“ 2-2.5小时”。有时,运气会绕开,触发因素会出现在Sakai和Apache Hive等大型项目中-是时候喝咖啡,喝咖啡和喝咖啡了。您也可以研究文档,但这并不适合所有人。
“既然有这么神奇的工具,为什么我们需要进行单元测试?”
然后,测试速度明显加快。几分钟-已经有结果了。它们还使您可以准确查看规则的哪一部分掉线了。而且,并非所有规则的所有允许触发都总是被SelfTester项目捕获,但是还必须检查其可操作性。
老朋友的新问题
最初,本文的这一部分以“ SelfTester中的项目版本很旧,因此所出现的大多数错误很可能已修复”开头。但是,当我决定要确保这一点时,我感到很惊讶。每个错误仍然存在。一切。这意味着两件事:
- 这些错误对于应用程序的运行并非至关重要。顺便说一句,它们中的许多都在测试代码中,并且几乎不能将不正确的测试称为一致性。
- 这些错误是在大型项目的不经常使用的文件中发现的,开发人员几乎不去查看。因此,错误的代码注定要在其中停留很长时间:很可能直到由于该错误而发生一些严重的错误为止。
对于那些希望深入研究的人,将提供指向我们正在检查的特定版本的链接。
PS以上内容并不意味着静态分析仅捕获未使用代码中的无害错误。我们检查项目的发布版本(和几乎发布版本),其中开发人员和测试人员(有时是不幸的是,用户)在手工中发现了最相关的错误,这些错误既漫长,昂贵又痛苦。您可以在我们的文章“静态代码分析无法找到,因为未使用该错误”中了解更多信息。
Apache Dubbo和空白菜单
GitHub
Diagnostics“ V6080考虑检查打印错误。很可能应该在下一个条件下检查分配的变量”已在7.08版中发布,但尚未出现在我们的文章中,因此该对其进行修复。
Menu.java:40
public class Menu
{
private Map<String, List<String>> menus = new HashMap<String, List<String>>();
public void putMenuItem(String menu, String item)
{
List<String> items = menus.get(menu);
if (item == null) // <=
{
items = new ArrayList<String>();
menus.put(menu, items);
}
items.add(item);
}
....
}
“键集合”字典的经典示例和同样经典的错字。开发人员想要创建一个与键相对应的集合(如果该键尚不存在),但是他混淆了变量的名称,不仅得到了该方法的错误操作,而且在最后一行得到了NullPointerException。对于Java 8及更高版本,要实现此类字典,应使用computeIfAbsent方法:
public class Menu
{
private Map<String, List<String>> menus = new HashMap<String, List<String>>();
public void putMenuItem(String menu, String item)
{
List<String> items = menus.computeIfAbsent(menu, key -> new ArrayList<>());
items.add(item);
}
....
}
玻璃鱼和双重检查锁定
GitHub
下一版本中将包含的诊断之一是检查“双重检查锁定”模式的正确实现。Glassfish成为了SelfTester项目检测记录的记录持有者:PVS-Studio使用此规则在项目中总共发现了10个问题区域。我邀请读者玩乐,并在下面的代码片段中寻找其中的两个。要获得帮助,请参考文档:“ V6082不安全的双重检查锁定”。好吧,或者,如果您根本不想,请在文章结尾。
EjbComponentAnnotationScanner.java
public class EjbComponentAnnotationScanner
{
private Set<String> annotations = null;
public boolean isAnnotation(String value)
{
if (annotations == null)
{
synchronized (EjbComponentAnnotationScanner.class)
{
if (annotations == null)
{
init();
}
}
}
return annotations.contains(value);
}
private void init()
{
annotations = new HashSet();
annotations.add("Ljavax/ejb/Stateless;");
annotations.add("Ljavax/ejb/Stateful;");
annotations.add("Ljavax/ejb/MessageDriven;");
annotations.add("Ljavax/ejb/Singleton;");
}
....
}
SonarQube和数据流
GitHub
改进诊断不仅在于直接更改其代码以捕获更多可疑位置或消除误报。手动标记数据流的方法在分析器的开发中也起着重要作用-例如,您可以编写这样的库方法始终返回非null的信息。在编写新的诊断程序时,我们意外地发现Map#clear()方法未标记。除了很明显的愚蠢代码“ V6009集合为空。'清除'功能的调用毫无意义”诊断开始流行以来,我们还找到了一个很好的错字。
MetricRepositoryRule.java:90
protected void after()
{
this.metricsById.clear();
this.metricsById.clear();
}
乍一看,再次清除字典并不是一个错误。而且,如果我们的视线没有下降得更低(实际上是下一种方法),我们甚至会认为这是随机重复的行。
protected void after()
{
this.metricsById.clear();
this.metricsById.clear();
}
public Metric getByKey(String key)
{
Metric res = metricsByKey.get(key);
....
}
究竟。该类具有两个名称相似的字段metricsById和metricsByKey。我敢肯定,在after方法中,开发人员想清除两个字典,但是自动完成使他失败了,或者他错误地输入了相同的名字。因此,在调用after之后,保存相关数据的两个词典将不同步。
酒井和空收藏
GitHub
将在下一版本中包含的另一项新诊断是“ V6084总是返回空集合的可疑返回”。忘记添加项目到集合很容易,尤其是当每个项目需要首先初始化时。从个人经验来看,此类错误通常不是导致应用程序崩溃,而是导致异常行为或缺少任何功能。
DateModel.java:361
public List getDaySelectItems()
{
List selectDays = new ArrayList();
Integer[] d = this.getDays();
for (int i = 0; i < d.length; i++)
{
SelectItem selectDay = new SelectItem(d[i], d[i].toString());
}
return selectDays;
}
顺便说一句,相同的类包含非常相似的方法而没有相同的错误。例如:
public List getMonthSelectItems()
{
List selectMonths = new ArrayList();
Integer[] m = this.getMonths();
for (int i = 0; i < m.length; i++)
{
SelectItem selectMonth = new SelectItem(m[i], m[i].toString());
selectMonths.add(selectMonth);
}
return selectMonths;
}
对未来的计划
除了各种不太有趣的内部事物之外,我们还在考虑将Spring Framework的诊断程序添加到Java分析器中。它不仅是Javists的主要面包,而且还包含许多不明显的瞬间,在这些瞬间人们可能会绊倒。我们还不确定这些诊断最终将以何种形式出现,何时会发生以及是否会发生。但是我们确信我们需要使用Spring for SelfTester的创意以及开源项目的创意。因此,如果您有任何想法,建议(在评论或私人留言中,也可以)!而且,我们收集的善良越多,优先级就会越高。
最后,Glassfish的双重检查锁定实现中存在错误:
- 该字段未声明为“易失性”。
- 该对象首先发布,然后初始化。
为什么所有这些都不好-您可以在文档中再次看到。
如果您想与说英语的读者分享这篇文章,请使用翻译链接:Nikita Lazeba。PVS-Studio for Java的幕后知识:我们如何开发诊断程序。