使用IntelliJ IDEA静态分析器验证CDK项目

我决定测试IntelliJ IDEA静态Java代码分析器,并与它一起测试The Chemistry Development Kit项目。 在这里,我将给出一些我发现的错误。 我认为其中一些对于整个Java程序来说都是典型的,因此它们可能很有趣。


化学开发套件是用于解决化学信息学和生物信息学问题的开源Java库。 当我从事生物信息学研究时,我们积极地使用它。 该项目已经开发了20多年,拥有数十名作者,并且代码质量非常不均匀。 不过,该项目具有单元测试,并且pom.xml中规定了与JaCoCo覆盖分析器的集成。 此外,还为三个静态分析器配置了插件: FindBugsPMDCheckstyle 。 检查哪些警告仍然更加有趣。


内置在IntelliJ IDEA中的静态Java代码分析器并不劣于专用的静态分析工具,但在某些方面要优于它们。 另外,几乎所有静态分析功能都可以在Community Edition (一个免费的开源IDE)中获得。 特别是,免费版本会产生本文中描述的所有警告。


默认情况下,静态分析是在代码编辑模式下持续进行的,因此,如果您使用IntelliJ IDEA编写代码,那么即使在进行测试之前,您也可以在几秒钟内从字面上纠正许多错误。 您可以使用“ 分析” |“批处理”检查整个项目或其一部分。 使用Analyze | 检查代码或运行单独的检查 按名称运行检查 。 在这种情况下,由于检查的复杂性,有些检查将变得无法在编辑模式下进行。 但是,这种检查很少。


许多IntelliJ IDEA检查不会报告错误,而是提供不准确的代码或提供更惯用,美观或快速的替代方法。 当您不断在IDE中工作时,这很有用。 但是,就我而言,最好从警告实际错误的消息开始。 基本上, Java类别很有趣| 可能的错误 ,尽管还有其他类别值得探讨,例如“ 数字问题”


我只会告诉您一些有趣的警告。


1.一元加


该项目中已经有66个一元加号。 写+1而不是1有时候我想变得美丽。 但是,在某些情况下,如果不是+= =+ ,那么一元加号就会出现:


 int totalCharge1 = 0; while (atoms1.hasNext()) { totalCharge1 = +((IAtom) atoms1.next()).getFormalCharge(); } Iterator<IAtom> atoms2 = products.atoms().iterator(); int totalCharge2 = 0; while (atoms2.hasNext()) { totalCharge2 = +((IAtom) atoms2.next()).getFormalCharge(); } 

一个明显的错别字,它忽略了循环中除最后一次以外的所有迭代。 它不是写为“等于空格加空格的空间”,而是“等于空格加空格的空间”,这似乎很奇怪。 但是,如果您探究历史 ,陌生感就会消失。 最初,“等号”和“加号”确实存在,但在2008年,它们经历了自动格式化程序,并且代码发生了变化。 顺便说一句,这对于静态分析器来说是道德的:基于奇怪的格式发出警告是合理的,但是如果代码被自动格式化,警告将消失并且错误仍然存​​在。


2.整数除法导致小数


一个相当烦人的错误,但是静态分析器发现它很好。 这是一个例子


 angle = 1 / 180 * Math.PI; 

不幸的是,该角度不是1度而是0度。 类似错误


 Integer c1 = features1.get(key); Integer c2 = features2.get(key); c1 = c1 == null ? 0 : c1; c2 = c2 == null ? 0 : c2; sum += 1.0 - Math.abs(c1 - c2) / (c1 + c2); // integer division in floating-point context 

看来c1c2非负数,这意味着差模不会超过总和。 因此,如果两个数字都不为零,则结果将为0;如果其中一个数字为0,则结果将为1。


3.调用Class.getClass()


有时人们对Class类型的对象调用getClass()方法。 结果还是具有常量值Class.class Class类型的对象。 这通常是一个错误:无需调用getClass() 。 例如, 在这里


 public <T extends ICDKObject> T ofClass(Class<T> intf, Object... objects) { try { if (!intf.isInterface()) throw new IllegalArgumentException("expected interface, got " + intf.getClass()); ... 

如果发生异常,则报告它将完全无用。 顺便说一句,错误处理过程中的错误通常是通过旧项目中的静态分析发现的:通常,错误处理过程经过最坏的测试。


4.在数组上调用toString()


这是经典的流派:toString()未为数组重新定义,其结果非常无用。 通常,这可以在诊断消息中找到。


 int[] dim = {0, 0, 0}; ... return "Dim:" + dim + " SizeX:" + grid.length + " SizeY:" + grid[0].length + " SizeZ:"... 

很难用眼睛注意到问题,因为此处dim.toString()隐式的,但字符串串联将其委托给它。 立即建议修复-将其包装在Arrays.toString(dim)


5.读取集合但未填充


这也经常在未经静态分析器进行持续测试的代码库中找到。 这是一个简单的例子


 final Set<IBond> bondsToHydrogens = new HashSet<IBond>(); // ... 220  ,  bondsToHydrogens   ! for (IBond bondToHydrogen : bondsToHydrogens) //     sgroup.removeBond(bondToHydrogen); 

显然填充只是错过了。 静态分析器具有更简单的检查来指示未使用的变量,但是此处使用了变量,因此它们是静默的。 我们需要更智能的检查,以了解有关集合的信息。


6.相反:我们填写但不阅读


相反的情况也是可能的。 这是一个数组示例


 int[] tmp = new int[trueBits.length - 1]; System.arraycopy(trueBits, 0, tmp, 0, i); System.arraycopy(trueBits, i + 1, tmp, i, trueBits.length - i - 1); 

检查知道,arraycopy方法的第三个参数仅用于写入数组,此后根本不使用该数组。 根据代码的逻辑判断,跳过trueBits = tmp;trueBits = tmp;


7.通过==比较整数


这是一个阴险的错误,因为缓存了较小的Integer对象值,并且在一天的数量超过127之前,一切都可以正常工作。这样的问题可能根本不明显


 for (int a = 0; a < cliqueSize; a++) { for (int b = 0; b < vecSize; b += 3) { if (cliqueList.get(a) == compGraphNodes.get(b + 2)) { cliqueMapping.add(compGraphNodes.get(b)); cliqueMapping.add(compGraphNodes.get(b + 1)); } } } 

好吧,似乎已经比较了某些列表中的某些对象,也许一切都很好。 必须注意这些列表包含Integer类型的对象。


8.复制地图


在这次检查中,一张图片值一千字。 看到错误了吗?


好一点了吗


9.不使用该方法的结果。


一些方法的结果是愚蠢的不使用,IDEA随即报告


 currentChars.trim(); 

可能这意味着currentChars = currentChars.trim(); 。 由于Java中的字符串是不可变的,因此如果不重新分配结果,将不会发生任何事情。 例如 ,还可以找到str.substring(2)


顺便说一下,这是一个相当复杂的检查。 除了预先准备的方法列表外,我们有时还会尝试自动确定值得使用其结果的方法。 这里,在源文本和库的字节码中都需要进行过程间分析。 所有这些都是在编辑代码的过程中即时完成的!


10.不可达的交换机分支


 // if character is out of scope don't if (c > 128) return 0; switch (c) { case '\u002d': // hyphen case '\u2012': // figure dash case '\u2013': // en-dash case '\u2014': // em-dash case '\u2212': // minus return '-'; // 002d default: return c; } 

由于我们排除了代码大于128的字符,因此分支\u2012-\u2212无法访问。 似乎不值得排除。


11.无法达到的条件


条件链中一个绝对绝妙的问题:


 if (oxNum == 0) { if (hybrid.equals("sp3")) { ... } else if (hybrid.equals("sp2")) return 47; } else if (oxNum == 1 && hybrid.equals("sp3")) return 47; else if ((oxNum == 2 && hybrid.equals("sp3")) || (oxNum == 1 && hybrid.equals("sp2")) || (oxNum == 0 && hybrid.equals("sp"))) //     return 48; else if ((oxNum == 3 && hybrid.equals("sp3")) || (oxNum >= 2 && hybrid.equals("sp2")) || (oxNum >= 1 && hybrid.equals("sp"))) return 49; 

在复杂的条件逻辑中,这种情况并不罕见:我们检查的条件不能成立,因为上面已经检查了其片段。 在这里,我们有一个单独的分支oxNum == 0 ,否则我们检查oxNum == 0 && hybrid.equals("sp") ,当然不能。


12.我们写一个零长度的数组


有时IntelliJ IDEA会通知您是否写入超出其大小的数组:


 Point3d points[] = new Point3d[0]; //    0  if (nwanted == 1) { points = new Point3d[1]; points[0] = new Point3d(aPoint); points[0].add(new Vector3d(length, 0.0, 0.0)); } else if (nwanted == 2) { //       —   points[0] = new Point3d(aPoint); points[0].add(new Vector3d(length, 0.0, 0.0)); points[1] = new Point3d(aPoint); points[1].add(new Vector3d(-length, 0.0, 0.0)); } 

13.访问索引后检查长度


该过程以及错误处理期间的另一个常见问题是:


 public void setParameters(Object[] params) throws CDKException { if (params.length > 1) { throw new CDKException("..."); } if (!(params[0] instanceof Integer)) { //     throw new CDKException("The parameter must be of type Integer"); } if (params.length == 0) return; //      maxIterations = (Integer) params[0]; } 

在数组为空的情况下,代码的编写者希望安静地退出,但是由于进行了验证,因此它将退出,大声地敲击ArrayIndexOutOfBoundsException。 显然,检查的顺序是混乱的。


14.访问后检查是否为空


再一次,违反了操作顺序, 这次null


 while (!line.startsWith("frame:") && input.ready() && line != null) { line = input.readLine(); logger.debug(lineNumber++ + ": ", line); } 

IDEA写那line != null始终为true。 碰巧检查实际上是多余的,但是这里的代码看起来好像真的可以为null。


15.分离而不是合取


人们经常将逻辑运算符AND和OR混淆。 CDK项目也不例外


 if (rStereo != 4 || pStereo != 4 || rStereo != 3 || pStereo != 3) { ... } 

无论rStereopStereo等于什么,很显然它们不能同时等于4和3,因此,此条件始终为真。


16.再次分离而不是合取


类似的错误 ,但被另一条消息捕获:


 if (getFirstMapping() != null || !getFirstMapping().isEmpty()) { ... } 

仅当getFirstMapping()返回null ,我们才能进入右侧,但是在这种情况下,我们可以保证有一个NullPointerException,IDEA会警告该异常。 顺便说一句,这里我们依靠getFirstMapping()方法结果的稳定性。 有时我们使用启发式方法,但是在这里直接分析稳定性。 由于类是最终的,因此该方法不能被覆盖。 IDEA return firstSolution.isEmpty() ? null : firstSolution检查其主体return firstSolution.isEmpty() ? null : firstSolution return firstSolution.isEmpty() ? null : firstSolution ,它确定稳定性归结于Map#isEmpty方法的稳定性,该方法先前已注释为“稳定”。


17.接口和instanceof的层次结构


检查对象是否属于任何接口时,请不要忘记接口可以相互继承:


 if (object instanceof IAtomContainer) { root = convertor.cdkAtomContainerToCMLMolecule((IAtomContainer) object); } else if (object instanceof ICrystal) { root = convertor.cdkCrystalToCMLMolecule((ICrystal) object); } ... 

ICrystal接口扩展IAtomContainer接口,因此显然无法达到第二个分支:如果晶体进入此处,它将落入第一个分支。


18.遍历一个空列表


这段代码的作者可能不太熟悉 Java语言:


 List<Integer> posNumList = new ArrayList<Integer>(size); for (int i = 0; i < posNumList.size(); i++) { posNumList.add(i, 0); } 

ArrayList的size参数指示内部数组的初始大小。 如果您事先知道要放置多少个项目,这将用于优化以减少分配的数量。 但是,实际上,列表中的项没有出现,并且size()方法返回0。因此,尝试用零初始化列表项的下一个循环完全没有用。


19.不要忘记初始化字段


分析器以一种特殊的方式检查构造函数,并考虑了字段初始化程序。 由于此,发现了这样的错误


 public class IMatrix { public double[][] realmatrix; public double[][] imagmatrix; public int rows; public int columns; public IMatrix(Matrix m) { rows = m.rows; columns = m.columns; int i, j; for (i = 0; i < rows; i++) for (j = 0; j < columns; j++) { realmatrix[i][j] = m.matrix[i][j]; // NullPointerException  imagmatrix[i][j] = 0d; } } } 

尽管字段是公共的,但这里没有人可以肯定地在构造函数之前对其进行楔入和初始化。 因此,IDEA大胆地发出警告,指出访问数组元素将引发NullPointerException。


20.不要重复两次


重复的情况也经常发生。 这是一个例子


 if (commonAtomCount > vfMCSSize && commonAtomCount > vfMCSSize) { return true; } 

这样的错误是阴险的,因为您永远不知道,第二个条件仅仅是多余的,或者作者想检查其他内容。 如果没有立即解决此问题,那么可能很难弄清楚。 这是为什么应经常使用静态分析的另一个原因。


我在项目的错误跟踪器中报告了其中一些错误 。 很好奇的是,当该项目的作者修复了一部分时,他们自己使用了IntelliJ IDEA分析器,发现了我没有写过的其他问题,并且也开始修复它们 。 我认为这是一个好兆头:作者意识到了静态分析的重要性。

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


All Articles