在PVS-Studio静态分析器的第七版中,我们添加了对Java语言的支持。 现在该讨论一下我们如何开始为Java语言提供支持,我们做了什么以及未来计划如何。 而且,当然,本文将显示分析仪在开放项目上的首次测试。
PVS工作室
对于以前从未听说过PVS-Studio工具的Java开发人员,我将对其进行简要说明。
PVS-Studio是用于检测用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 IDEAIntelliJ IDEA内置了功能非常强大的静态代码分析器。 此外,分析仪正在开发中,其作者正在密切
监视我们的活动。 有了IntelliJ IDEA,我们将变得最困难。 至少到目前为止,我们将无法在诊断功能上超过IntelliJ IDEA。 因此,我们将尝试着重于其他优势。
首先,IntelliJ IDEA中的静态分析是开发环境的芯片之一,这对其施加了一定的限制。 我们可以免费使用分析仪。 例如,我们可以使分析仪快速适应客户的特定需求。 快速而深入的支持是我们的竞争优势。 我们的客户与开发PVS-Studio特定部分的程序员直接沟通。
PVS-Studio具有将其集成到大型旧项目的开发周期中的多种可能性。 这是
与SonarQube集成的 。 这是
对分析器消息的
极大抑制 ,它使您可以立即在大型项目中使用分析器来仅跟踪新代码或更改的代码中的错误。 PVS-Studio已
集成到持续集成过程中。 我认为这些功能和其他功能将帮助我们的分析仪在Java世界中找到阳光下的地方。
虫子FindBugs项目被
放弃 。 但是应该记住这一点,因为它可能是最著名的Java代码免费静态分析器。
FindBugs的后继者是
SpotBugs项目。 但是,他的受欢迎程度较弱,对他的影响也尚不完全清楚。
总的来说,我们认为尽管FindBugs曾经并且仍然非常流行,并且还是一个免费的分析器,但是我们不应该考虑它。 这个项目只是悄无声息地成为过去。
PS:顺便说一下,现在
使用开放项目时,PVS-Studio也可以
免费使用 。
SonarQube(SonarJava)我们相信我们不会与SonarQube竞争,而是会对其进行补充。 PVS-Studio与SonarQube集成,开发人员可以在他们的项目中发现更多的错误和潜在的漏洞。 如何将PVS-Studio工具和其他分析器集成到SonarQube中,我们定期在各种会议上举办的大师班上进行演讲(
示例 )。
如何启动PVS-Studio for Java
我们向用户提供了将分析仪集成到装配系统中的最流行方法:
- Maven插件;
- Gradle插件;
- IntelliJ IDEA的插件
在测试阶段,我们遇到了许多拥有自行编写的装配系统的用户,尤其是在移动开发中。 他们喜欢直接运行分析器,列出源和类路径的功能。
您可以在文档页面“
如何启动PVS-Studio Java ”中找到有关启动分析仪的所有方法的详细信息。
我们不能忽略在Java开发人员中非常流行的
SonarQube代码质量控制平台,因此我们在
SonarQube插件中添加了Java语言支持。
进一步的计划
我们有很多想法需要进一步研究,但是一些针对我们分析仪的特定计划如下所示:
- 创建新的诊断并完善现有的诊断;
- 开发数据流分析;
- 提高可靠性和可用性。
我们可能会抽出时间将IntelliJ IDEA插件用于CLion。 向使用Java分析器的开发人员介绍C ++ :-)
开源项目中发现的错误示例
如果我没有显示本文中使用新分析仪发现的任何错误,我将不是我。 我们可以像往常一样进行一些大型的开放源Java项目,并撰写带有错误分析的经典文章。
但是,我立即预见到是否可以在IntelliJ IDEA,FindBugs等项目中找到某些问题。 因此,我根本没有出路,而我将从这些项目开始。 因此,我决定快速检查并写出以下项目中一些有趣的错误示例:
在这些项目中编写错误是一项艰巨的任务。 事实是这些项目的质量很高。 实际上,这并不奇怪。 正如我们的观察结果所示,静态分析器的代码始终使用其他工具进行了良好的测试和验证。
尽管如此,我还是必须从这些项目开始。 我将没有第二次机会写一些关于他们的东西。 我确信,PVS-Studio for Java发行后,这些项目的开发人员将使PVS-Studio投入使用,并将开始将其用于定期或至少定期检查其代码。 例如,我知道从事撰写IntelliJ IDEA静态代码分析器的JetBrains的开发人员之一Tagir Valeyev(
lany )在撰写本文时已经在使用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;
PVS-Studio警告:V6011 [CWE-682]将'double'类型的'0.2'文字与'int'类型的值进行比较。 TitleCapitalizationInspection.java 169
按照预期,如果少于20%的单词以大写字母开头,则函数应返回true。 实际上,该检查不起作用,因为发生了整数除法。 作为除法的结果,只能获得两个值:0或1。
仅当所有单词均以大写字母开头时,该函数才会返回假值。 在所有其他情况下,除法将产生0,并且该函数将返回真实值。
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++) {
PVS-Studio警告:V6007 [CWE-571]表达式'index> = 0'始终为true。 Updater.java 184
首先,查看条件
(0 <= current && current <count) 。 仅当
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) ||
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;
PVS-Studio警告:V6021 [CWE-563]该值已分配给'priority'变量,但未使用。 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项目的文章,以使读者满意。

如果您想与说英语的读者分享这篇文章,请使用以下链接:Andrey Karpov。
PVS-Studio for Java 。