PVS-Studio for Java

PVS-Studio for Java

在PVS-Studio静态分析器的第七版中,我们添加了对Java语言的支持。 现在该简要介绍一下我们如何开始支持Java语言,取得了多大的发展以及进一步计划中的内容。 当然,本文将列出在开源项目上进行的首批分析仪试用。

PVS工作室


这是PVS-Studio的简短描述,适用于以前从未听说过的Java开发人员。

此工具旨在检测用C,C ++,C#和Java编写的程序源代码中的错误和潜在漏洞。 它适用于Windows,Linux和macOS环境。

PVS-Studio执行静态代码分析并生成报告,以帮助开发人员发现并消除缺陷。 对于那些对PVS-Studio如何精确搜索错误感兴趣的人,建议阅读一下文章“ PVS-Studio代码分析器中用于查找错误和潜在漏洞的技术 ”。

开始


我本可以想出一个聪明的故事,说明我们一直在猜测PVS-Studio支持哪种下一代语言。 关于Java的明智选择,它基于这种语言的高度流行等。

然而,正如生活中发生的那样,选择不是通过深入的分析,而是通过实验来做出的:)。 是的,我们考虑了PVS-Studio分析仪进一步发展的方向。 我们考虑了以下语言,例如:Java,PHP,Python,JavaScript,IBM RPG。 我们甚至倾向于Java语言,但最终选择没有做出。 对于那些对陌生的IBM RPG感兴趣的人,我想引导您阅读本说明 ,所有内容都将从中变得清晰。

2017年底,我的同事Egor Bredikhin审查了现成的解析代码库(换句话说,就是解析器)以寻找新的开发方向,这对我们很感兴趣。 最终,他遇到了多个用于解析Java代码的项目。 他设法通过基于Spoon的几个诊断程序快速创建了一个分析器原型。 而且,很明显,我们将能够在Java分析器中使用SWIG来使用C ++分析器的某些机制。 我们查看了所得到的内容,并意识到我们的下一个分析器将用于Java。

我们要感谢Egor在Java分析器方面所做的努力和辛勤工作。 他在文章“ 开发新的静态分析器:PVS-Studio Java ”中描述了开发过程本身。

竞争对手呢?


全球有许多免费的和商用的Java静态代码分析器。 没有必要在文章中将它们全部列出。 我将仅保留指向“ 静态代码分析工具列表 ”的链接(请参阅Java和多语言部分)。

但是,我知道首先要问我们关于IntelliJ IDEA,FindBugs和SonarQube(SonarJava)的问题。

IntelliJ IDEA

IntelliJ IDEA内置了功能非常强大的静态代码分析器。 此外,分析仪正在不断发展,其作者密切关注我们的活动。 因此,IntelliJ IDEA对我们来说是一个艰难的曲奇。 至少到目前为止,我们无法在诊断能力上超过IntelliJ IDEA。 因此,我们将专注于我们的其他优势。

IntelliJ IDEA中的静态分析主要是环境的功能之一,这对其施加了某些限制。 对于我们来说,我们可以自由地使用分析仪。 例如,我们可以快速使其适应特定的客户需求。 快速而深入的支持是我们的竞争优势。 我们的客户直接与开发人员联系,在PVS-Studio的一个或另一部分上工作。

在PVS-Studio中,有很多机会可以将其集成到开发大型旧项目的周期中。 例如,这是我们与SonarQube集成 。 它还包括对分析器警告的批量抑制 ,这使您可以立即在大型项目中开始使用该工具来仅跟踪新代码或修改后的代码中的错误。 PVS-Studio 可以在持续集成过程中构建 。 我认为这些功能和其他功能将帮助我们的分析仪在Java世界中找到太阳之下的位置。

虫子

FindBugs项目被放弃。 尽管如此,我们还是应该提到它,因为它也许是最著名的Java代码免费静态分析器。

SpotBugs可以称为FindBugs的后继者。 但是,它不那么受欢迎,并且尚不清楚会发生什么。

总体而言,我们认为,尽管FindBugs一直并且仍然非常受欢迎,并且除了免费的分析器之外,我们不应该再赘述。 这个项目将悄然成为历史。

PS:顺便说一下,现在使用开放项目时,PVS-Studio也可以免费使用

SonarQube(SonarJava)

我们相信我们不会与SonarQube竞争,而是对其进行补充。 PVS-Studio集成在SonarQube中,这使开发人员可以在他们的项目中发现更多的错误和潜在的安全漏洞。 我们定期告诉我们如何在不同会议上举行的大师班上将PVS-Studio工具和其他分析仪集成在SonarQube中。

如何为Java运行PVS-Studio


我们为用户提供了构建系统中最流行的分析仪集成方式:

  • Maven插件;
  • Gradle插件;
  • IntelliJ IDEA的插件

在测试阶段,我们遇到了许多拥有自行编写的构建系统的用户,尤其是在移动开发领域。 他们享受了直接运行分析器的机会,列出了源代码和类路径。

您可以在文档页面“ 如何运行PVS-Studio Java ”中找到有关运行分析仪的所有方法的详细信息。

我们无法回避在Java开发人员中非常流行的SonarQube代码质量控制平台,因此我们在SonarQube插件中添加了对Java语言的支持。

进一步的计划


我们有很多想法可能需要进一步研究,但是我们的任何分析仪都固有一些特定的计划,如下所示:

  • 创建新的诊断并改进现有诊断;
  • 改进数据流分析;
  • 可靠性和可用性的提高。

也许,我们会花时间将IntelliJ IDEA插件用于CLion。 向了解Java分析器的C ++开发人员问好:-)

开源项目中发现的错误示例


如果我没有在文章中显示新的分析仪发现的一些错误,则不寒而栗! 好吧,我们可以像往常一样做一个大型开源Java项目并写一篇经典文章来回顾错误。

但是,我立即想到有关在IntelliJ IDEA,FindBugs等项目中可以找到的内容的问题。 因此,除了开始这些项目之外,我别无选择。 因此,我决定快速检查并写出以下项目中一些有趣的错误示例:

  • IntelliJ IDEA社区版 。 我认为无需解释为什么选择此项目:)。
  • SpotBugs 如我先前所写,FindBugs项目没有进展。 因此,让我们看一下SpotBugs项目,它是FindBugs的后继项目。 SpotBugs是经典的Java代码静态分析器。
  • SonarSource公司的项目,该公司开发了用于连续监视代码质量的软件。 现在,让我们看一下SonarQubeSonarJava

撰写有关这些项目的错误的知识是一个挑战。 事实是这些项目的质量很高。 实际上,这并不奇怪。 我们的观察表明,静态代码分析器始终使用其他工具进行了良好的测试和验证。

尽管如此,我还是必须从这些项目开始。 我没有第二次机会写关于他们的文章。 我确信,PVS-Studio for Java发行后,列出的项目的开发人员将把PVS-Studio拿到船上,并开始将其用于定期检查(至少是偶尔检查其代码)。 例如,我知道JetBrains的开发者之一Tagir Valeev( lany )正在IntelliJ IDEA静态代码分析器上工作,而当我正在撰写本文时,该文章已经在使用Beta版本的PVS-Studio。 。 他给我们写了大约15封电子邮件,其中包含错误报告和建议。 谢谢,塔吉尔!

幸运的是,我不需要在一个特定项目中发现那么多错误。 目前,我的任务是证明Java的PVS-Studio分析器没有白费,并且将能够填充其他旨在提高代码质量的工具。 我只是浏览了分析器报告,并列出了一些看起来很有趣的错误。 如果可能,我尝试引用不同类型的错误。 让我们看看结果如何。

IntelliJ IDEA,整数部


private static boolean checkSentenceCapitalization(@NotNull String value) { List<String> words = StringUtil.split(value, " "); .... int capitalized = 1; .... return capitalized / words.size() < 0.2; // allow reasonable amount of // capitalized words } 

PVS-Studio警告:V6011 [CWE-682]将'double'类型的'0.2'文字与'int'类型的值进行比较。 TitleCapitalizationInspection.java 169

关键是,如果少于20%的单词以大写字母开头,则该函数应返回true。 实际上,该检查不起作用,因为发生了整数除法。 作为除法的结果,我们只能获得两个值:0或1。

仅当所有单词都以大写字母开头时,该函数才会返回false。 在所有其他情况下,除法运算将得出0,并且该函数将返回true。

IntelliJ IDEA,可疑循环


 public int findPreviousIndex(int current) { int count = myPainter.getErrorStripeCount(); int foundIndex = -1; int foundLayer = 0; if (0 <= current && current < count) { current--; for (int index = count - 1; index >= 0; index++) { // <= int layer = getLayer(index); if (layer > foundLayer) { foundIndex = index; foundLayer = layer; } } .... } 

PVS-Studio警告:V6007 [CWE-571]表达式'index> = 0'始终为true。 Updater.java 184

首先,查看条件(0 <= current && current <count) 。 仅当计数变量值大于0时才执行。

现在看一下循环:

 for (int index = count - 1; index >= 0; index++) 

变量索引用表达式count-1初始化。 当计数变量大于0时, 索引变量的初始值将始终大于或等于0。事实证明,将执行循环,直到索引变量发生溢出为止。

最有可能的是,这只是一个错字,并且必须执行减量而不是变量的增量:

 for (int index = count - 1; index >= 0; index--) 

IntelliJ IDEA,复制粘贴


 @NonNls public static final String BEFORE_STR_OLD = "before:"; @NonNls public static final String AFTER_STR_OLD = "after:"; private static boolean isBeforeOrAfterKeyword(String str, boolean trimKeyword) { return (trimKeyword ? LoadingOrder.BEFORE_STR.trim() : LoadingOrder.BEFORE_STR).equalsIgnoreCase(str) || (trimKeyword ? LoadingOrder.AFTER_STR.trim() : LoadingOrder.AFTER_STR).equalsIgnoreCase(str) || LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str) || // <= LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str); // <= } 

PVS-Studio警告:V6001 [CWE-570]在“ ||”的左侧和右侧有相同的子表达式“ LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str)”。 操作员。 检查行:127,128。ExtensionOrderConverter.java 127

最后一行效果不错。 一个开发人员跳了开枪,并增加了代码行,却忘了修复它。 结果,将str字符串与BEFORE_STR_OLD比较两次。 最有可能的比较之一必须是与AFTER_STR_OLD进行比较。

IntelliJ IDEA,错字


 public synchronized boolean isIdentifier(@NotNull String name, final Project project) { if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) { name = "\"" + name; } if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) { name += "\""; } .... } 

PVS-Studio警告:V6001 [CWE-571]在'&&'运算符的左侧和右侧有相同的子表达式'!StringUtil.endsWithChar(name,'“')'。JsonNamesValidator.java 27

此代码段检查名称是否用单引号或双引号引起来。 如果不是这样,则会自动添加双引号。

由于输入错误,仅检查名称的末尾是否包含双引号。 结果,单引号中的名称将被错误地处理。

名字

 'Abcd' 

由于添加额外的双引号将变成:

 'Abcd'" 

IntelliJ IDEA,针对阵列溢出的错误保护


 static Context parse(....) { .... for (int i = offset; i < endOffset; i++) { char c = text.charAt(i); if (c == '<' && i < endOffset && text.charAt(i + 1) == '/' && startTag != null && CharArrayUtil.regionMatches(text, i + 2, endOffset, startTag)) { endTagStartOffset = i; break; } } .... } 

PVS-Studio警告:V6007 [CWE-571]表达式'i <endOffset'始终为true。 EnterAfterJavadocTagHandler.java 183

如果if运算符没有意义, 子表达式i <endOffset 。 在任何情况下, i变量始终小于endOffset ,这取决于循环执行的条件。

开发人员很可能希望在调用函数时防止字符串溢出:

  • text.charAt(i + 1)
  • CharArrayUtil.regionMatches(文本,i + 2,endOffset,startTag)

在这种情况下,用于检查索引的子表达式必须为: (i)<endOffset-2

IntelliJ IDEA,重复检查


 public static String generateWarningMessage(....) { .... if (buffer.length() > 0) { if (buffer.length() > 0) { buffer.append(" ").append( IdeBundle.message("prompt.delete.and")).append(" "); } } .... } 

PVS-Studio警告:V6007 [CWE-571]表达式'buffer.length()> 0'始终为true。 DeleteUtil.java 62

这可能是无害的冗余代码,也可能是严重错误。

如果重复检查意外出现(例如在重构过程中),则没有任何问题。 您只需删除第二张支票即可。

另一种情况也是可能的。 第二次检查必须完全不同,并且代码的行为也不符合预期。 那是一个真正的错误。

注意事项 顺便说一下,有很多各种各样的冗余检查。 好吧,通常很明显这不是错误。 但是,我们不能将分析仪警告视为误报。 作为解释,我想举一个例子,也取自IntelliJ IDEA:

 private static boolean isMultiline(PsiElement element) { String text = element.getText(); return text.contains("\n") || text.contains("\r") || text.contains("\r\n"); } 

分析器说函数text.contains(“ \ r \ n”)始终返回false。 确实,如果找不到字符“ \ n”和“ \ r”,则没有必要搜索“ \ r \ n”。 这不是错误,并且代码不好是因为它的工作速度稍慢,对子字符串进行无意义的搜索。

在每种情况下,如何处理此类代码都是开发人员的问题。 在撰写文章时,我通​​常不关注此类代码。

IntelliJ IDEA,出现了问题


 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) { return false; } return text.charAt(0) == '0'; } 

PVS-Studio警告:V6007 [CWE-570]表达式'“ 0” .equals(text)'始终为false。 ConvertIntegerToDecimalPredicate.java 46

该代码肯定包含逻辑错误。 我发现很难说程序员到底要检查什么以及如何纠正缺陷。 因此,在这里,我将指向毫无意义的检查。

首先,必须检查字符串是否包含至少两个符号。 如果不是这样,则该函数返回false

接下来是校验“ 0” .equals(文本) 。 这是没有意义的,因为任何字符串只能包含一个字符。

因此,这里有些问题,并且代码应该固定。

SpotBugs(FindBugs的后继者),迭代次数限制错误


 public static String getXMLType(@WillNotClose InputStream in) throws IOException { .... String s; int count = 0; while (count < 4) { s = r.readLine(); if (s == null) { break; } Matcher m = tag.matcher(s); if (m.find()) { return m.group(1); } } throw new IOException("Didn't find xml tag"); .... } 

PVS-Studio警告:V6007 [CWE-571]表达式'count <4'始终为true。 实用程序394

理论上,必须仅在文件的前四行中搜索xml标记。 但是由于这一事实,一个忘记增加计数变量,整个文件将被读取。

首先,这可能是一个非常慢的操作,其次,在文件中间的某个位置,可能会发现一些被视为xml标记而不是它的标记。

SpotBugs(FindBugs的后继者),清除值


 private void reportBug() { int priority = LOW_PRIORITY; String pattern = "NS_NON_SHORT_CIRCUIT"; if (sawDangerOld) { if (sawNullTestVeryOld) { priority = HIGH_PRIORITY; // <= } if (sawMethodCallOld || sawNumericTestVeryOld && sawArrayDangerOld) { priority = HIGH_PRIORITY; // <= pattern = "NS_DANGEROUS_NON_SHORT_CIRCUIT"; } else { priority = NORMAL_PRIORITY; // <= } } bugAccumulator.accumulateBug( new BugInstance(this, pattern, priority).addClassAndMethod(this), this); } 

PVS-Studio警告:V6021 [CWE-563]该值已分配给“优先级”变量,但未使用。 FindNonShortCircuit.java 197

根据变量sawNullTestVeryOld的值设置优先级变量的值。 但是,这并不重要。 之后,在任何情况下都将为优先级变量分配另一个值。 函数逻辑中明显的错误。

SonarQube,复制粘贴


 public class RuleDto { .... private final RuleDefinitionDto definition; private final RuleMetadataDto metadata; .... private void setUpdatedAtFromDefinition(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } private void setUpdatedAtFromMetadata(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } .... } 

PVS-Studio:V6032奇怪的是,方法“ setUpdatedAtFromDefinition”的主体完全等效于另一种方法“ setUpdatedAtFromMetadata”的主体。 检查行:396,405。RuleDto.java 396

setUpdatedAtFromMetadata方法中使用了一个定义字段。 最有可能应使用元数据字段。 这与复制粘贴失败的效果非常相似。

SonarJava,初始化地图时重复


 private final Map<JavaPunctuator, Tree.Kind> assignmentOperators = Maps.newEnumMap(JavaPunctuator.class); public KindMaps() { .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... } 

PVS-Studio警告:V6033 [CWE-462]已添加带有相同键“ JavaPunctuator.PLUSEQU”的项目。 检查行:104、100。KindMaps.java 104

在地图中两次设置了相同的键值对。 最有可能的是,它无意间发生了,实际上没有真正的错误。 但是,无论如何都必须检查此代码,因为也许一个人忘记添加任何其他对。

结论


为什么要写结论如此明显? 我建议大家立即下载PVS-Studio并尝试检查Java语言上的工作项目! 下载PVS-Studio

谢谢大家的关注。 我希望不久以后,我们会为读者提供一系列有关检查各种开源Java项目的文章。

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


All Articles