PVS-Studio para Java

PVS-Studio para Java

En la séptima versión del analizador estático PVS-Studio, agregamos compatibilidad con el lenguaje Java. Es hora de una breve historia de cómo hemos comenzado a admitir el lenguaje Java, qué tan lejos hemos llegado y qué hay en nuestros planes adicionales. Por supuesto, este artículo enumerará los primeros ensayos del analizador en proyectos de código abierto.

PVS-Studio


Aquí hay una breve descripción de PVS-Studio para desarrolladores de Java que no han oído hablar de él anteriormente.

Esta herramienta está diseñada para detectar errores y vulnerabilidades potenciales en el código fuente de los programas, escritos en C, C ++, C # y Java. Funciona en entornos Windows, Linux y macOS.

PVS-Studio realiza un análisis de código estático y genera un informe que ayuda al desarrollador a encontrar y eliminar defectos. Para aquellos que estén interesados ​​en cómo exactamente PVS-Studio busca errores, les sugiero que echen un vistazo al artículo " Tecnologías utilizadas en el analizador de código PVS-Studio para encontrar errores y vulnerabilidades potenciales ".

Comenzando


Podría haber tenido una historia inteligente de cómo hemos estado especulando sobre qué próximo idioma admitir en PVS-Studio. Acerca de una elección sensata de Java, que se basa en una gran popularidad de este lenguaje, etc.

Sin embargo, como sucede en la vida, la elección no se realizó mediante un análisis profundo, sino mediante un experimento :). Sí, reflexionamos sobre la dirección del desarrollo del analizador PVS-Studio. Consideramos lenguajes tales como: Java, PHP, Python, JavaScript, IBM RPG. Incluso nos inclinamos por el lenguaje Java, pero no se tomó la decisión final. Para aquellos cuya mirada se basó en el desconocido RPG de IBM, me gustaría dirigirlos a esta nota , desde la cual todo quedará claro.

A finales de 2017, mi colega Egor Bredikhin revisó las bibliotecas de código de análisis disponibles (en otras palabras, analizadores) para obtener nuevas direcciones de desarrollo, interesantes para nosotros. Finalmente, se encontró con varios proyectos para analizar el código Java. Logró hacer rápidamente un prototipo de analizador con un par de diagnósticos basados ​​en Spoon . Además, ha quedado claro que podríamos usar en el analizador Java algunos mecanismos del analizador C ++ usando SWIG . Analizamos lo que obtuvimos y nos dimos cuenta de que nuestro próximo analizador sería para Java.

Nos gustaría agradecer a Egor por su empresa y el arduo trabajo que ha realizado sobre el analizador Java. El proceso de desarrollo en sí fue descrito por él en el artículo " Desarrollo de un nuevo analizador estático: PVS-Studio Java ".

¿Qué pasa con los competidores?


Existen muchos analizadores de código estático gratuitos y comerciales para Java en todo el mundo. No tiene sentido enumerarlos a todos en el artículo. Solo dejaré el enlace a la " Lista de herramientas para el análisis de código estático " (consulte la sección Java y Multi-idioma).

Sin embargo, sé que ante todo se nos preguntará sobre IntelliJ IDEA, FindBugs y SonarQube (SonarJava).

IDEA IntelliJ

Un analizador de código estático muy potente está integrado en IntelliJ IDEA. Además, el analizador está evolucionando, sus autores siguen de cerca nuestras actividades. Entonces, IntelliJ IDEA es una cookie difícil para nosotros. No podremos superar IntelliJ IDEA en habilidades de diagnóstico, al menos por ahora. Por lo tanto, nos concentraremos en nuestras otras ventajas.

El análisis estático en IntelliJ IDEA es principalmente una de las características del entorno, que le impone ciertas limitaciones. En cuanto a nosotros, tenemos libertad en lo que podemos hacer con nuestro analizador. Por ejemplo, podemos adaptarlo rápidamente a las necesidades específicas del cliente. El soporte rápido y profundo es nuestra ventaja competitiva. Nuestros clientes se comunican directamente con los desarrolladores, trabajando en una u otra parte de PVS-Studio.

En PVS-Studio, hay muchas oportunidades para integrarlo en un ciclo de desarrollo de grandes proyectos antiguos. Por ejemplo, es nuestra integración con SonarQube . También incluye la supresión masiva de las advertencias del analizador, lo que le permite comenzar a usar la herramienta de inmediato en un proyecto grande para rastrear errores solo en el código nuevo o modificado. PVS-Studio se puede construir en un proceso de integración continua. Creo que estas y otras características ayudarán a nuestro analizador a encontrar un lugar bajo el sol en el mundo de Java.

Findbugs

El proyecto FindBugs está abandonado. Sin embargo, deberíamos mencionarlo por el hecho de que, quizás, es el analizador estático gratuito más famoso de código Java.

SpotBugs podría llamarse el sucesor de FindBugs. Sin embargo, es menos popular y todavía no está claro qué sucederá con él.

En términos generales, creemos que a pesar de que FindBugs ha sido y sigue siendo extremadamente popular, y además de eso un analizador gratuito, no debemos detenernos en él. Este proyecto se convertirá silenciosamente en una historia.

PD: Por cierto, ahora PVS-Studio también se puede usar de forma gratuita cuando se trabaja con proyectos abiertos.

SonarQube (SonarJava)

Creemos que no competimos con SonarQube, sino que lo complementamos. PVS-Studio se integra en SonarQube, que permite a los desarrolladores encontrar más errores y posibles vulnerabilidades de seguridad en sus proyectos. Regularmente contamos cómo integrar la herramienta PVS-Studio y otros analizadores en SonarQube en clases magistrales que realizamos en términos de diferentes conferencias.

Cómo ejecutar PVS-Studio para Java


Pusimos a disposición de los usuarios las formas más populares de integración del analizador en el sistema de compilación:

  • Plugin para Maven;
  • Plugin para Gradle;
  • Plugin para IntelliJ IDEA

Durante la fase de prueba, conocimos a muchos usuarios que tienen sistemas de compilación autoescritas, especialmente en el ámbito del desarrollo móvil. Disfrutaron de la oportunidad de ejecutar el analizador directamente, enumerando las fuentes y classpath.

Puede encontrar información detallada sobre todas las formas de ejecutar el analizador en la página de documentación " Cómo ejecutar PVS-Studio Java ".

No pudimos evitar la plataforma SonarQube de control de calidad de código, que es tan popular entre los desarrolladores de Java, por lo que agregamos soporte del lenguaje Java en nuestro complemento para SonarQube .

Planes adicionales


Tenemos muchas ideas que pueden requerir mayor investigación, pero algunos planes específicos, inherentes a cualquiera de nuestros analizadores, son los siguientes:

  • Creación de nuevos diagnósticos y mejora de los existentes;
  • Mejora del análisis de flujo de datos;
  • Incremento de fiabilidad y usabilidad.

Tal vez, encontraremos tiempo para adaptar el complemento IntelliJ IDEA para CLion. Hola a los desarrolladores de C ++ que leyeron sobre el analizador de Java :-)

Ejemplos de errores encontrados en proyectos de código abierto


¡Tiembla mis maderas si no estoy mostrando en el artículo algunos errores encontrados con el nuevo analizador! Bueno, podríamos haber tomado un gran proyecto Java de código abierto y escribir un artículo clásico revisando errores, como solemos hacer .

Sin embargo, inmediatamente anticipo preguntas sobre lo que podemos encontrar en proyectos como IntelliJ IDEA, FindBugs, etc. Así que no tengo otra salida que no sea comenzar con estos proyectos. Entonces, decidí revisar y escribir rápidamente varios ejemplos interesantes de errores de los siguientes proyectos:

  • IntelliJ IDEA Community Edition . Creo que no hay necesidad de explicar por qué se eligió este proyecto :).
  • SpotBugs Como escribí anteriormente, el proyecto FindBugs no está progresando. Así que echemos un vistazo al proyecto SpotBugs, que es el sucesor de FindBugs. SpotBugs es un analizador estático clásico de código Java.
  • Algo de los proyectos de la compañía SonarSource, que desarrolla software para el monitoreo continuo de la calidad del código. Ahora echemos un vistazo al interior de SonarQube y SonarJava .

Escribir sobre errores de estos proyectos es un desafío. El hecho es que estos proyectos son de muy alta calidad. En realidad, no es sorprendente. Nuestras observaciones muestran que los analizadores de código estático siempre se prueban y verifican bien con otras herramientas.

A pesar de todo esto, tendré que comenzar exactamente con estos proyectos. No tendré la segunda oportunidad de escribir sobre ellos. Estoy seguro de que después del lanzamiento de PVS-Studio para Java, los desarrolladores de los proyectos enumerados tomarán PVS-Studio a bordo y comenzarán a usarlo para verificaciones regulares o, al menos, ocasionales de su código. Por ejemplo, sé que Tagir Valeev ( lany ), uno de los desarrolladores de JetBrains, que trabaja en el analizador de código estático IntelliJ IDEA, en este momento, cuando estoy escribiendo el artículo, ya está jugando con la versión Beta de PVS-Studio . Nos escribió unos 15 correos electrónicos con informes de errores y recomendaciones. Gracias Tagir!

Afortunadamente, no necesito encontrar tantos errores en un proyecto en particular. Por el momento, mi tarea es mostrar que el analizador PVS-Studio para Java no apareció en vano, y podrá llenar una línea de otras herramientas diseñadas para mejorar la calidad del código. Acabo de mirar los informes del analizador y enumeré algunos errores que parecían interesantes. Si es posible, traté de citar diferentes tipos de errores. Veamos cómo resultó.

IDEA IntelliJ, División Entera


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 } 

Advertencia de PVS-Studio: V6011 [CWE-682] El literal '0.2' del tipo 'doble' se compara con un valor del tipo 'int'. TitleCapitalizationInspection.java 169

El punto era que la función debería ser verdadera si menos del 20% de las palabras comienzan con una letra mayúscula. En realidad, la comprobación no funciona, porque se produce la división de enteros. Como resultado de la división, solo podemos obtener dos valores: 0 o 1.

La función devolverá falso, solo si todas las palabras comienzan con una letra mayúscula. En todos los demás casos, la operación de división dará como resultado 0 y la función devolverá verdadero.

IDEA IntelliJ, lazo sospechoso


 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; } } .... } 

Advertencia de PVS-Studio: V6007 [CWE-571] La expresión 'index> = 0' siempre es verdadera. Updater.java 184

Primero, observe la condición (0 <= actual && actual <cuenta) . Se ejecuta solo en caso de que el valor de la variable de conteo sea ​​mayor que 0.

Ahora mira el bucle:

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

El índice variable se inicializa con un recuento de expresiones - 1 . Como la variable de conteo es mayor que 0, el valor inicial de la variable de índice siempre será mayor o igual a 0. Resulta que el ciclo se ejecutará hasta que ocurra un desbordamiento de la variable de índice .

Lo más probable es que sea solo un error tipográfico, y una disminución, no un incremento de una variable tiene que ejecutarse:

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

IDEA IntelliJ, copiar y pegar


 @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); // <= } 

Advertencia de PVS-Studio: V6001 [CWE-570] Hay subexpresiones idénticas 'LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase (str)' a la izquierda y a la derecha de '||' operador Líneas de verificación: 127, 128. ExtensionOrderConverter.java 127

Buen efecto antiguo de la última línea . Un desarrollador saltó el arma y, al multiplicar la línea de código, olvidó arreglarlo. Como resultado, una cadena str se compara con BEFORE_STR_OLD dos veces. Lo más probable es que una de las comparaciones sea con AFTER_STR_OLD .

IntelliJ IDEA, Typo


 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 += "\""; } .... } 

Advertencia de PVS-Studio: V6001 [CWE-571] Hay subexpresiones idénticas '! StringUtil.endsWithChar (nombre,' "')' a la izquierda y a la derecha del operador '&&'. JsonNamesValidator.java 27

Este fragmento de código verifica que el nombre esté encerrado entre comillas simples o dobles. Si no es así, las comillas dobles se agregan automáticamente.

Debido a un error tipográfico, el final del nombre se verifica solo por la presencia de comillas dobles. Como resultado, el nombre entre comillas simples se procesará incorrectamente.

El nombre

 'Abcd' 

debido a la adición de comillas dobles adicionales se convertirá en:

 'Abcd'" 

IDEA IntelliJ, protección incorrecta contra el desbordamiento de matriz


 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; } } .... } 

Advertencia de PVS-Studio: V6007 [CWE-571] La expresión 'i <endOffset' siempre es verdadera. EnterAfterJavadocTagHandler.java 183

La subexpresión i <endOffset en la condición del operador if no tiene sentido. La variable i siempre es menor que endOffset en cualquier caso, que se deduce de la condición de la ejecución del bucle.

Lo más probable es que un desarrollador quisiera protegerse de un desbordamiento de cadena al llamar a funciones:

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

En este caso, la subexpresión para verificar el índice debe ser: (i) <endOffset-2 .

IDEA IntelliJ, verificación repetida


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

Advertencia de PVS-Studio: V6007 [CWE-571] La expresión 'buffer.length ()> 0' siempre es verdadera. DeleteUtil.java 62

Esto puede ser un código redundante inocuo o un error crucial.

Si un cheque duplicado apareció accidentalmente, por ejemplo, durante la refactorización, no hay nada de malo en eso. Simplemente puede eliminar la segunda verificación.

Otro escenario también es posible. La segunda verificación debe ser bastante diferente y el código no se comporta como se esperaba. Entonces es un error real.

Nota Por cierto, hay muchas verificaciones redundantes. Bueno, a menudo está claro que no es un error. Sin embargo, no podemos considerar las advertencias del analizador como falsos positivos. Para una explicación, me gustaría citar un ejemplo así, también tomado de IntelliJ IDEA:

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

El analizador dice que la función text.contains ("\ r \ n") siempre devuelve falso. De hecho, si no se encuentra el carácter "\ n" y "\ r", no tiene sentido buscar "\ r \ n". No es un error, y el código es malo solo porque funciona un poco más lento, realizando una búsqueda sin sentido de una subcadena.

Cómo lidiar con dicho código, en cada caso, es una pregunta para los desarrolladores. Cuando escribo artículos, generalmente no presto atención a dicho código.

IDEA IntelliJ, algo está mal


 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'; } 

Advertencia de PVS-Studio: V6007 [CWE-570] La expresión '"0" .equals (texto)' siempre es falsa. ConvertIntegerToDecimalPredicate.java 46

El código contiene un error lógico seguro. Me resulta difícil decir qué quería comprobar exactamente el programador y cómo corregir el defecto. Así que aquí, solo señalaré un cheque sin sentido.

Al principio hay que comprobar que la cadena contiene al menos dos símbolos. Si no es así, entonces la función devuelve falso .

Luego viene el cheque "0" .equals (texto) . No tiene sentido, porque ninguna cadena puede contener un solo carácter.

Entonces, algo está mal aquí, y el código debe ser reparado.

SpotBugs (sucesor de FindBugs), error de limitación en el número de iteraciones


 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"); .... } 

Advertencia de PVS-Studio: V6007 [CWE-571] La expresión 'cuenta <4' siempre es verdadera. Util.java 394

En teoría, una búsqueda de la etiqueta xml debe realizarse solo en las primeras cuatro líneas del archivo. Pero debido al hecho de que uno olvidó incrementar la variable de conteo , se leerá todo el archivo.

En primer lugar, esta puede ser una operación muy lenta y, en segundo lugar, en algún lugar en el medio del archivo, se podría encontrar algo que se percibiría como una etiqueta xml, no siendo eso.

SpotBugs (sucesor de FindBugs), compensación de un valor


 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); } 

Advertencia de PVS-Studio: V6021 [CWE-563] El valor se asigna a la variable 'prioridad' pero no se utiliza. FindNonShortCircuit.java 197

El valor de la variable de prioridad se establece dependiendo del valor de la variable sawNullTestVeryOld . Sin embargo, no importa en absoluto. Después de eso, a la variable de prioridad se le asignará otro valor en cualquier caso. Un error obvio en la lógica de la función.

SonarQube, Copy-Paste


 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 Es extraño que el cuerpo del método 'setUpdatedAtFromDefinition' sea completamente equivalente al cuerpo de otro método 'setUpdatedAtFromMetadata'. Líneas de verificación: 396, 405. RuleDto.java 396

Se utiliza un campo de definición en el método setUpdatedAtFromMetadata . Lo más probable es que se use el campo de metadatos . Esto es muy similar a los efectos de un Copy-Paste fallido.

SonarJava, se duplica al inicializar el mapa


 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); .... } 

Advertencia de PVS-Studio: V6033 [CWE-462] Ya se ha agregado un elemento con la misma clave 'JavaPunctuator.PLUSEQU'. Líneas de verificación: 104, 100. KindMaps.java 104

El mismo par clave-valor se establece en el mapa dos veces. Lo más probable es que sucedió inadvertidamente y en realidad no hay un error real. Sin embargo, este código tiene que verificarse en cualquier caso, porque, tal vez, uno olvidó agregar cualquier otro par.

Conclusión


¿Por qué escribir conclusiones cuando es tan obvio? ¡Les sugiero que descarguen PVS-Studio ahora mismo e intenten verificar sus proyectos de trabajo en el lenguaje Java! Descargar PVS-Studio .

Gracias a todos por su atención. Espero que pronto complazcamos a nuestros lectores con una serie de artículos sobre la comprobación de varios proyectos Java de código abierto.

Source: https://habr.com/ru/post/es436496/


All Articles