面向Java的PVS-Studio:诊断开发



作为更改,今天我们将向您介绍一些有关开发和最终确定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);
  ....
}


究竟。该类具有两个名称相似的字段metricsByIdmetricsByKey我敢肯定,在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的幕后知识:我们如何开发诊断程序



All Articles