Desarrollo de un nuevo analizador estático: PVS-Studio Java

Cuadro 3

El analizador estático PVS-Studio es conocido en el mundo de C, C ++ y C # como una herramienta para detectar errores y vulnerabilidades potenciales. Sin embargo, tenemos pocos clientes del sector financiero, ya que resultó que Java e IBM RPG (!) Ahora tienen demanda allí. Pero siempre quisimos acercarnos al mundo empresarial, por lo tanto, después de pensarlo un poco, decidimos comenzar a crear un analizador Java.

Introduccion


Por supuesto, había preocupaciones. Es fácil tomar el mercado de analizadores en IBM RPG. No estoy del todo seguro de que haya herramientas decentes para el análisis estático de este lenguaje. En el mundo de Java, las cosas son completamente diferentes. Ya existe una línea de herramientas para el análisis estático, y para avanzar, debe crear un analizador realmente potente y genial.

Sin embargo, nuestra empresa ha tenido experiencia en el uso de varias herramientas para el análisis estático de Java, y estamos seguros de que podemos hacer muchas cosas mejor.

Además, teníamos una idea de cómo usar toda la potencia de nuestro analizador C ++ en un analizador Java. Pero lo primero es lo primero.

Arbol


Cuadro 6


En primer lugar, era necesario decidir cómo obtendríamos el árbol de sintaxis y el modelo semántico.

El árbol de sintaxis es el elemento básico alrededor del cual se construye el analizador. Al realizar comprobaciones, el analizador se mueve a través del árbol de sintaxis y examina sus nodos individuales. Sin ese árbol, el análisis estático serio es prácticamente imposible. Por ejemplo, buscar errores usando expresiones regulares es poco prometedor .

Vale la pena señalar que solo un árbol de sintaxis no es suficiente. El analizador también necesita información semántica. Por ejemplo, necesitamos conocer los tipos de todos los elementos del árbol, poder ir a la declaración de variables, etc.

Examinamos varias opciones para obtener un árbol de sintaxis y un modelo semántico:


Abandonamos la idea de usar ANTLR casi de inmediato, ya que esto complicaría innecesariamente el desarrollo del analizador (el análisis semántico tendría que implementarse por nuestra cuenta). Al final, decidimos parar en la biblioteca Spoon:

  • No es solo un analizador, sino un ecosistema completo: proporciona no solo un árbol de análisis, sino también oportunidades para el análisis semántico, por ejemplo, le permite obtener información sobre los tipos de variables, ir a la declaración de variables, obtener información sobre la clase primaria, etc.
  • Está basado en Eclipse JDT y puede compilar código.
  • Es compatible con la última versión de Java y se actualiza constantemente.
  • Buena documentación y API clara.

Aquí hay un ejemplo de un metamodelo que Spoon proporciona y con el que trabajamos al crear reglas de diagnóstico:

Cuadro 10


Este metamodelo corresponde al siguiente código:
class TestClass { void test(int a, int b) { int x = (a + b) * 4; System.out.println(x); } } 

Una de las cosas buenas de Spoon es que simplifica el árbol de sintaxis (eliminar y agregar nodos) para que sea más fácil trabajar con él. Al mismo tiempo, se garantiza la equivalencia semántica del metamodelo simplificado del original.

Para nosotros, esto significa, por ejemplo, que ya no tenemos que preocuparnos por omitir corchetes adicionales al atravesar un árbol. Además, cada expresión se coloca en un bloque, se revelan las importaciones y se realizan otras simplificaciones similares.

Por ejemplo, un código como este:

 for (int i = ((0)); (i < 10); i++) if (cond) return (((42))); 

se presentará de la siguiente manera:

 for (int i = 0; i < 10; i++) { if (cond) { return 42; } } 

Basado en el árbol de sintaxis, se realiza un llamado análisis basado en patrones. Esta es una búsqueda de errores en el código fuente de un programa utilizando patrones de código de error bien conocidos. En el caso más simple, el analizador busca lugares que parezcan un error en el árbol de acuerdo con las reglas descritas en los diagnósticos correspondientes. El número de tales patrones es grande y su complejidad puede variar mucho.

El ejemplo más simple de un error detectado por el análisis basado en patrones es el siguiente código del proyecto jMonkeyEngine:

 if (p.isConnected()) { log.log(Level.FINE, "Connection closed:{0}.", p); } else { log.log(Level.FINE, "Connection closed:{0}.", p); } 

Los bloques then y else de la instrucción if son iguales; lo más probable es que haya un error lógico.

Aquí hay otro ejemplo similar del proyecto Hive:

 if (obj instanceof Number) { // widening conversion return ((Number) obj).doubleValue(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof String) { return Double.valueOf(obj.toString()); } else if (obj instanceof Timestamp) { return new TimestampWritable((Timestamp)obj).getDouble(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); } 

Este código contiene dos condiciones idénticas en una secuencia de la forma if (....) else if (....) else if (....) . Vale la pena revisar esta sección del código para ver si hay un error lógico o eliminar el código duplicado.

Análisis de flujo de datos.


Además del árbol de sintaxis y el modelo semántico, el analizador necesita un mecanismo para analizar el flujo de datos .

El análisis del flujo de datos le permite calcular los valores válidos de variables y expresiones en cada punto del programa y, gracias a esto, encontrar errores. Llamamos a estos valores válidos valores virtuales.

Los valores virtuales se crean para variables, campos de clase, parámetros de métodos y otras cosas en la primera mención. Si se trata de una asignación, el mecanismo de flujo de datos calcula el valor virtual analizando la expresión a la derecha; de lo contrario, se toma como valor virtual todo el rango válido de valores para este tipo de variable. Por ejemplo:

 void func(byte x) // x: [-128..127] { int y = 5; // y: [5] ... } 

Cada vez que cambia el valor de una variable, el motor de flujo de datos vuelve a calcular el valor virtual. Por ejemplo:

 void func() { int x = 5; // x: [5] x += 7; // x: [12] ... } 

El motor de flujo de datos también procesa declaraciones de control:

 void func(int x) // x: [-2147483648..2147483647] { if (x > 3) { // x: [4..2147483647] if (x < 10) { // x: [4..9] } } else { // x: [-2147483648..3] } ... } 

En este ejemplo, al ingresar la función, no hay información sobre el rango de valores de la variable x , por lo tanto, se establece de acuerdo con el tipo de la variable (de -2147483648 a 2147483647). Luego, el primer bloque condicional impone una restricción x > 3, y los rangos se combinan. Como resultado, en el bloque entonces el rango de valores para x es de 4 a 2147483647, y en el bloque else de -2147483648 a 3. La segunda condición x <10 se procesa de la misma manera.

Además, debe poder realizar cálculos puramente simbólicos. El ejemplo más simple:

 void f1(int a, int b, int c) { a = c; b = c; if (a == b) // <= always true .... } 

Aquí, a la variable a se le asigna el valor c , a la variable b también se le asigna el valor c , después de lo cual se comparan a y b . En este caso, para encontrar el error, solo recuerde la pieza de madera correspondiente al lado derecho.

Aquí hay un ejemplo un poco más complejo con cálculos de caracteres:

 void f2(int a, int b, int c) { if (a < b) { if (b < c) { if (c < a) // <= always false .... } } } 

En tales casos, ya es necesario resolver el sistema de desigualdades en forma simbólica.

El mecanismo de flujo de datos ayuda al analizador a encontrar errores que son muy difíciles de encontrar mediante el análisis basado en patrones.

Estos errores incluyen:

  • Desbordamientos;
  • Ir al extranjero la matriz;
  • Acceso por referencia nula o potencialmente nula;
  • Condiciones sin sentido (siempre verdadero / falso);
  • Fugas de memoria y recursos;
  • División por 0;
  • Y algunos otros.

El análisis del flujo de datos es especialmente importante cuando se buscan vulnerabilidades. Por ejemplo, si cierto programa recibe información del usuario, es probable que la información se use para causar una denegación de servicio o para obtener el control del sistema. Los ejemplos incluyen errores que causan desbordamientos del búfer para alguna entrada o, por ejemplo, inyecciones SQL. En ambos casos, para que el analizador estático detecte tales errores y vulnerabilidades, es necesario monitorear el flujo de datos y los posibles valores de las variables.

Debo decir que el mecanismo de análisis de flujo de datos es un mecanismo complejo y extenso, y en este artículo mencioné solo los conceptos básicos.

Veamos algunos ejemplos de errores que pueden detectarse utilizando el mecanismo de flujo de datos.

Proyecto Colmena:

 public static boolean equal(byte[] arg1, final int start1, final int len1, byte[] arg2, final int start2, final int len2) { if (len1 != len2) { // <= return false; } if (len1 == 0) { return true; } .... if (len1 == len2) { // <= .... } } 

La condición len1 == len2 siempre se cumple, ya que la verificación opuesta ya se ha realizado anteriormente.

Otro ejemplo del mismo proyecto:

 if (instances != null) { // <= Set<String> oldKeys = new HashSet<>(instances.keySet()); if (oldKeys.removeAll(latestKeys)) { .... } this.instances.keySet().removeAll(oldKeys); this.instances.putAll(freshInstances); } else { this.instances.putAll(freshInstances); // <= } 

Aquí, en el bloque else , se garantiza la desreferenciación del puntero nulo. Nota: aquí las instancias son las mismas que this.instances .

Ejemplo del proyecto JMonkeyEngine:

 public static int convertNewtKey(short key) { .... if (key >= 0x10000) { return key - 0x10000; } return 0; } 

Aquí la variable clave se compara con el número 65536, sin embargo, es de tipo short y el valor máximo posible para short es 32767. En consecuencia, la condición nunca se cumple.

Un ejemplo del proyecto Jenkins:
 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

La variable cnt se introdujo en este código para limitar el número de pases a cinco, pero se olvidó de incrementarlo, como resultado de lo cual la verificación es inútil.

Mecanismo de anotación


Además, el analizador necesita un mecanismo de anotación. Annotations es un sistema de marcado que proporciona al analizador información adicional sobre los métodos y clases utilizados, además de lo que se puede obtener analizando su firma. El marcado se realiza manualmente, es un proceso largo y laborioso, ya que para lograr los mejores resultados es necesario anotar una gran cantidad de clases y métodos estándar del lenguaje Java. También tiene sentido anotar bibliotecas populares. En general, las anotaciones pueden considerarse como la base de conocimiento del analizador sobre los contratos de métodos y clases estándar.

Aquí hay un pequeño ejemplo de un error que puede detectarse mediante anotaciones:

 int test(int a, int b) { ... return Math.max(a, a); } 

En este ejemplo, debido a un error tipográfico, se pasó la misma variable como segundo argumento al método Math.max como primer argumento. Tal expresión no tiene sentido y es sospechosa.

Sabiendo que los argumentos del método Math.max siempre deben ser diferentes, el analizador estático podrá emitir una advertencia para dicho código.

Mirando hacia el futuro, daré algunos ejemplos de nuestro marcado de clases y métodos integrados (código C ++):

 Class("java.lang.Math") - Function("abs", Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Returns(Arg1, [](const Int &v) { return v.Abs(); }) - Function("max", Type::Int32, Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(Arg1, Arg2) .Returns(Arg1, Arg2, [](const Int &v1, const Int &v2) { return v1.Max(v2); }) Class("java.lang.String", TypeClassification::String) - Function("split", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotNull(Arg1)) .Returns(Ptr(NotNullPointer)) Class("java.lang.Object") - Function("equals", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(This, Arg1)) Class("java.lang.System") - Function("exit", Type::Int32) .Set(FunctionClassification::NoReturn) 

Explicaciones:

  • Clase - clase anotada;
  • Función : método de la clase anotada;
  • Puro : anotación que muestra que el método es limpio, es decir determinista y sin efectos secundarios;
  • Establecer : establecer un indicador arbitrario para el método.
  • FunctionClassification :: NoDiscard : una marca que significa que se debe usar el valor de retorno del método;
  • FunctionClassification :: NoReturn : un indicador que indica que el método no devuelve el control;
  • Arg1 , Arg2 , ... , ArgN : argumentos del método;
  • Devoluciones : el valor de devolución del método;
  • Requiere : un contrato para el método.

Vale la pena señalar que, además del marcado manual, existe otro enfoque para la anotación: salida automática de contratos basada en código de bytes. Está claro que este enfoque le permite mostrar solo ciertos tipos de contratos, pero permite obtener información adicional en general de todas las dependencias, y no solo de aquellas que fueron anotadas manualmente.

Por cierto, ya existe una herramienta que puede mostrar contratos como @Nullable , NotNull basado en bytecode - FABA . Según tengo entendido, el derivado de FABA se usa en IntelliJ IDEA.

Ahora también estamos considerando agregar análisis de bytecode para obtener contratos para todos los métodos, ya que estos contratos podrían complementar nuestras anotaciones manuales.

Las reglas de diagnóstico en el trabajo a menudo se refieren a anotaciones. Además de los diagnósticos, las anotaciones utilizan el mecanismo de flujo de datos. Por ejemplo, utilizando la anotación del método java.lang.Math.abs , puede calcular con precisión el valor del módulo de números. Al mismo tiempo, no tiene que escribir ningún código adicional, simplemente marque el método correctamente.

Considere un ejemplo de un error del proyecto Hibernate que se puede detectar mediante anotaciones:

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

En este código, el método equals () compara el objeto buySequence consigo mismo. Seguramente este es un error tipográfico y a la derecha debería ser that.purchaseSequence , no buySequence .

Cómo el Dr. Frankenstein ensambló un analizador de piezas


Imagen 2


Dado que los mecanismos del flujo de datos y las anotaciones en sí no están muy vinculados a un lenguaje en particular, se decidió reutilizar estos mecanismos desde nuestro analizador de C ++. Esto nos permitió obtener rápidamente toda la potencia del núcleo del analizador C ++ en nuestro analizador Java. Además, esta decisión también fue influenciada por el hecho de que estos mecanismos se escribieron en C ++ moderno con un montón de metaprogramación y plantilla de magia, y, en consecuencia, no son muy adecuados para transferir a otro idioma.

Para asociar la parte de Java con el núcleo C ++, decidimos usar SWIG (Simplified Wrapper and Interface Generator) , una herramienta para generar automáticamente envoltorios e interfaces para vincular programas C y C ++ con programas escritos en otros lenguajes. Para Java, SWIG genera código JNI (Java Native Interface) .

SWIG es ideal para casos en los que ya hay una gran cantidad de código C ++ que debe integrarse en un proyecto Java.

Daré un ejemplo mínimo de trabajo con SWIG. Supongamos que tenemos una clase C ++ que queremos usar en un proyecto Java:

CoolClass.h

 class CoolClass { public: int val; CoolClass(int val); void printMe(); }; 

CoolClass.cpp

 #include <iostream> #include "CoolClass.h" CoolClass::CoolClass(int v) : val(v) {} void CoolClass::printMe() { std::cout << "val: " << val << '\n'; } 

Primero debe crear un archivo de interfaz SWIG con una descripción de todas las funciones y clases exportadas. También en este archivo, si es necesario, se realizan configuraciones adicionales.

Ejemplo.i

 %module MyModule %{ #include "CoolClass.h" %} %include "CoolClass.h" 

Después de eso, puedes ejecutar SWIG:

 $ swig -c++ -java Example.i 

Generará los siguientes archivos:

  • CoolClass.java: una clase con la que trabajaremos directamente en un proyecto Java;
  • MyModule.java: una clase de módulo en la que se colocan todas las funciones y variables libres;
  • MyModuleJNI.java: contenedores Java;
  • Example_wrap.cxx: envoltorios de C ++.

Ahora solo necesita agregar los archivos .java resultantes al proyecto Java y el archivo .cxx al proyecto C ++.

Finalmente, necesita compilar el proyecto C ++ como una biblioteca dinámica y cargarlo en el proyecto Java usando System.loadLibary () :

App.java

 class App { static { System.loadLibary("example"); } public static void main(String[] args) { CoolClass obj = new CoolClass(42); obj.printMe(); } } 

Esquemáticamente, esto se puede representar de la siguiente manera:

Cuadro 8


Por supuesto, en un proyecto real no todo es tan simple y hay que esforzarse un poco más:

  • Para utilizar clases y métodos de plantilla de C ++, deben crearse instancias para todos los parámetros de plantilla aceptados utilizando la directiva % template ;
  • En algunos casos, es posible que deba detectar excepciones que se generan desde la parte de C ++ en la parte de Java. De manera predeterminada, SWIG no detecta excepciones de C ++ (se produce una falla predeterminada), sin embargo, es posible hacerlo utilizando la directiva % excepcion ;
  • SWIG le permite extender el código más en el lado de Java utilizando la directiva % extender . Por ejemplo, en nuestro proyecto, agregamos el método toString () a los valores virtuales para que podamos verlos en el depurador de Java;
  • Para emular el comportamiento RAII de C ++, la interfaz AutoClosable se implementa en todas las clases de interés;
  • El mecanismo de los directores permite el uso del polimorfismo de lenguaje cruzado;
  • Para los tipos que se asignan solo dentro de C ++ (en su grupo de memoria), los constructores y finalizadores se eliminan para mejorar el rendimiento. El recolector de basura ignorará estos tipos.

Puede leer más sobre todos estos mecanismos en la documentación SWIG .

Nuestro analizador está construido usando gradle, que llama a CMake, que, a su vez, llama a SWIG y compila la parte de C ++. Para los programadores, esto sucede casi imperceptiblemente, por lo que no experimentamos ningún inconveniente especial durante el desarrollo.

El núcleo de nuestro analizador C ++ está construido bajo Windows, Linux, macOS, por lo que el analizador Java también funciona en estos sistemas operativos.

¿Qué es una regla de diagnóstico?


Los diagnósticos en sí y el código para el análisis están escritos en Java. Esto se debe a una estrecha interacción con Spoon. Cada regla de diagnóstico es un visitante, cuyos métodos están sobrecargados, en los que se eluden los elementos que nos interesan:

Cuadro 9

Por ejemplo, el marco de diagnóstico V6004 se ve así:

 class V6004 extends PvsStudioRule { .... @Override public void visitCtIf(CtIf ifElement) { // if ifElement.thenStatement statement is equivalent to // ifElement.elseStatement statement => add warning V6004 } } 

Complementos


Para la integración más fácil del analizador estático en el proyecto, hemos desarrollado complementos para los sistemas de ensamblaje Maven y Gradle. El usuario solo puede agregar nuestro complemento al proyecto.

Para Gradle:

 .... apply plugin: com.pvsstudio.PvsStudioGradlePlugin pvsstudio { outputFile = 'path/to/output.json' .... } 

Para Maven:

 .... <plugin> <groupId>com.pvsstudio</groupId> <artifactId>pvsstudio-maven-plugin</artifactId> <version>0.1</version> <configuration> <analyzer> <outputFile>path/to/output.json</outputFile> .... </analyzer> </configuration> </plugin> 

Después de eso, el complemento recibirá de forma independiente la estructura del proyecto y comenzará el análisis.

Además, hemos desarrollado un complemento prototipo para IntelliJ IDEA.

Imagen 1

Este complemento también funciona en Android Studio.

Actualmente se está desarrollando un complemento para Eclipse.

Análisis incremental


Hemos proporcionado un modo de análisis incremental que le permite verificar solo los archivos modificados y, por lo tanto, reduce significativamente el tiempo requerido para el análisis de código. Gracias a esto, los desarrolladores podrán ejecutar el análisis tantas veces como sea necesario.

El análisis incremental incluye varias etapas:

  • Cuchara de almacenamiento en caché de metamodelos;
  • Reconstrucción de la parte modificada del metamodelo;
  • Análisis de archivos modificados.

Nuestro sistema de prueba


Para probar el analizador Java en proyectos reales, escribimos un kit de herramientas especial que le permite trabajar con la base de datos de proyectos abiertos. Fue escrito en ^ W Python + Tkinter y es multiplataforma.

Funciona de la siguiente manera:

  • El proyecto de prueba de una versión específica se descarga del repositorio en GitHub;
  • El proyecto está siendo ensamblado;
  • Nuestro complemento se agrega a pom.xml o build.gradle (usando git apply);
  • El analizador estático se inicia utilizando el complemento;
  • El informe resultante se compara con el punto de referencia para este proyecto.

Este enfoque garantiza que no se pierdan buenas respuestas como resultado de cambiar el código del analizador. A continuación se muestra la interfaz de nuestra utilidad de prueba.

Cuadro 11

Los proyectos en informes que tienen alguna diferencia con el estándar están marcados en rojo. El botón Aprobar le permite guardar la versión actual del informe como referencia.

Ejemplos de error


Por tradición, citaré varios errores de varios proyectos abiertos que encontró nuestro analizador Java. En el futuro, se planea escribir artículos con un informe más detallado sobre cada proyecto.

Proyecto hibernate


Advertencia de PVS-Studio: la función V6009 'igual' recibe argumentos extraños. Inspeccione los argumentos: esto, 1. PurchaseRecord.java 57

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

En este código, el método equals () compara el objeto buySequence consigo mismo. Lo más probable es que este sea un error tipográfico y a la derecha debería ser that.purchaseSequence , no buySequence .

Advertencia de PVS-Studio: la función V6009 'igual' recibe argumentos extraños. Inspeccionar argumentos: esto, 1. ListHashcodeChangeTest.java 232

 public void removeBook(String title) { for( Iterator<Book> it = books.iterator(); it.hasNext(); ) { Book book = it.next(); if ( title.equals( title ) ) { it.remove(); } } } 

Una operación similar a la anterior: a la derecha debe ser book.title , no title .

Proyecto colmena


Advertencia de PVS-Studio: V6007 La expresión 'colOrScalar1.equals ("Column")' siempre es falsa. GenVectorCode.java 2768

Advertencia de PVS-Studio: V6007 La expresión 'colOrScalar1.equals ("Scalar")' siempre es falsa. GenVectorCode.java 2774

Advertencia de PVS-Studio: V6007 La expresión 'colOrScalar1.equals ("Column")' siempre es falsa. GenVectorCode.java 2785

 String colOrScalar1 = tdesc[4]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column")) { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... } 

Los operadores están claramente confundidos aquí y en lugar de ' ||' usado ' &&' .

Proyecto JavaParser


Advertencia de PVS-Studio: V6001 Hay subexpresiones idénticas 'tokenRange.getBegin (). GetRange (). IsPresent ()' a la izquierda y a la derecha del operador '&&'. Node.java 213

 public Node setTokenRange(TokenRange tokenRange) { this.tokenRange = tokenRange; if (tokenRange == null || !(tokenRange.getBegin().getRange().isPresent() && tokenRange.getBegin().getRange().isPresent())) { range = null; } else { range = new Range( tokenRange.getBegin().getRange().get().begin, tokenRange.getEnd().getRange().get().end); } return this; } 

El analizador descubrió que las mismas expresiones están a la izquierda y a la derecha del operador && (mientras que todos los métodos en la cadena de llamadas están limpios). Lo más probable es que, en el segundo caso, sea necesario usar tokenRange.getEnd () y no tokenRange.getBegin () .

Advertencia de PVS-Studio: V6016 Acceso sospechoso al elemento del objeto 'typeDeclaration.getTypeParameters ()' mediante un índice constante dentro de un bucle. ResolvedReferenceType.java 265

 if (!isRawType()) { for (int i=0; i<typeDeclaration.getTypeParams().size(); i++) { typeParametersMap.add( new Pair<>(typeDeclaration.getTypeParams().get(0), typeParametersValues().get(i))); } } 

El analizador detectó acceso sospechoso al elemento de la colección en un índice constante dentro del bucle. Puede haber un error en este código.

Proyecto Jenkins


Advertencia de PVS-Studio: V6007 La expresión 'cnt <5' siempre es verdadera. AbstractProject.java 557

 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

La variable cnt se introdujo en este código para limitar el número de pases a cinco, pero se olvidó de incrementarlo, como resultado de lo cual la verificación es inútil.

Proyecto chispa


Advertencia de PVS-Studio: V6007 La expresión 'sparkApplications! = Null' siempre es verdadera. SparkFilter.java 127

 if (StringUtils.isNotBlank(applications)) { final String[] sparkApplications = applications.split(","); if (sparkApplications != null && sparkApplications.length > 0) { ... } } 

La comprobación de nulos del resultado devuelto por el método de división no tiene sentido, ya que este método siempre devuelve una colección y nunca devuelve nulo .

Proyecto Cuchara


Advertencia de PVS-Studio: V6001 Hay subexpresiones idénticas '! M.getSimpleName (). Comienza con ("set")' a la izquierda y a la derecha del operador '&&'. SpoonTestHelpers.java 108

 if (!m.getSimpleName().startsWith("set") && !m.getSimpleName().startsWith("set")) { continue; } 

En este código, las mismas expresiones están a la izquierda y a la derecha del operador && (todos los métodos en la cadena de llamadas están limpios). Lo más probable es que el código contenga un error lógico. Advertencia de

PVS-Studio: V6007 La expresión 'idxOfScopeBoundTypeParam> = 0' siempre es verdadera. MethodTypingContext.java 243

 private boolean isSameMethodFormalTypeParameter(....) { .... int idxOfScopeBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= int idxOfSuperBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= return idxOfScopeBoundTypeParam == idxOfSuperBoundTypeParam; } } .... } 

Aquí se sellaron en la segunda condición y en lugar de idxOfSuperBoundTypeParam escribió idxOfScopeBoundTypeParam .

Proyecto de seguridad de primavera


Advertencia-Estudio de PVS: V6001 Hay Idéntico las sub-expresiones a la izquierda ya la derecha de la '||' operador Líneas de verificación: 38, 39. AnyRequestMatcher.java 38

 @Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { return obj instanceof AnyRequestMatcher || obj instanceof security.web.util.matcher.AnyRequestMatcher; } 

La operación es similar a la anterior: aquí el nombre de la misma clase se escribe de diferentes maneras.

Advertencia de PVS-Studio: V6006 El objeto se creó pero no se está utilizando. La palabra clave 'throw' podría faltar. DigestAuthenticationFilter.java 434

 if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException( DigestAuthenticationFilter.this.messages .getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); } 

En este código, olvidaron agregar throw antes de la excepción. Como resultado, se lanza el objeto de excepción BadCredentialsException , pero no se usa de ninguna manera, es decir, No se lanza una excepción.

Advertencia de PVS-Studio: V6030 El método ubicado a la derecha de '|' Se llamará a los operadores independientemente del valor del operando izquierdo. Quizás, es mejor usar '||'. RedirectUrlBuilder.java 38

 public void setScheme(String scheme) { if (!("http".equals(scheme) | "https".equals(scheme))) { throw new IllegalArgumentException("..."); } this.scheme = scheme; } 

En este código, el uso de | injustificado, ya que al usarlo, el lado derecho se calculará incluso si el lado izquierdo ya es cierto. En este caso, esto no tiene sentido práctico, por lo tanto, el operador | vale la pena reemplazar con || .

Proyecto IntelliJ IDEA


Advertencia de PVS-Studio: V6008 Desreferencia potencial nula del 'editor'. IntroducirVariableBase.java:609

 final PsiElement nameSuggestionContext = editor == null ? null : file.findElementAt(...); // <= final RefactoringSupportProvider supportProvider = LanguageRefactoringSupport.INSTANCE.forLanguage(...); final boolean isInplaceAvailableOnDataContext = supportProvider != null && editor.getSettings().isVariableInplaceRenameEnabled() && // <= ... 

El analizador detectó que en este código puede producirse una desreferencia del puntero nulo del editor . Vale la pena agregar un cheque adicional.

Advertencia de PVS-Studio: la expresión V6007 siempre es falsa. RefResolveServiceImpl.java:814

 @Override public boolean contains(@NotNull VirtualFile file) { .... return false & !myProjectFileIndex.isUnderSourceRootOfType(....); } 

Es difícil para mí decir lo que el autor tenía en mente, pero ese código parece muy sospechoso. Incluso si de repente no hay ningún error aquí, creo que vale la pena volver a escribir este lugar para no confundir al analizador y otros programadores. Advertencia de

PVS-Studio: V6007 La expresión 'resultado [0]' siempre es falsa. CopyClassesHandler.java:298

 final boolean[] result = new boolean[] {false}; // <= Runnable command = () -> { PsiDirectory target; if (targetDirectory instanceof PsiDirectory) { target = (PsiDirectory)targetDirectory; } else { target = WriteAction.compute(() -> ((MoveDestination)targetDirectory).getTargetDirectory( defaultTargetDirectory)); } try { Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project); if (files != null) { if (openInEditor) { for (PsiFile file : files) { CopyHandler.updateSelectionInActiveProjectView( file, project, selectInActivePanel); } EditorHelper.openFilesInEditor( files.toArray(PsiFile.EMPTY_ARRAY)); } } } catch (IncorrectOperationException ex) { Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon()); } }; CommandProcessor processor = CommandProcessor.getInstance(); processor.executeCommand(project, command, commandName, null); if (result[0]) { // <= ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project) .activateEditorComponent()); } 

Sospecho que aquí se olvidaron de alguna manera cambiar el valor del resultado . Debido a esto, el analizador informa que verificar si (resultado [0]) no tiene sentido.

Conclusión


La dirección de Java es muy versátil: es de escritorio, Android, web y mucho más, por lo que tenemos mucho espacio para la actividad. En primer lugar, por supuesto, desarrollaremos aquellas áreas que tendrán mayor demanda.

Estos son nuestros planes para el futuro cercano:

  • Anotaciones de salida basadas en bytecode;
  • Integración en proyectos en Ant (¿alguien más lo usa en 2018?);
  • Plugin para Eclipse (en desarrollo);
  • Aún más diagnósticos y anotaciones;
  • Mejora del mecanismo de flujo de datos.

También sugiero a aquellos que deseen participar en la prueba de la versión alfa de nuestro analizador Java cuando esté disponible. Para ello, escriba a nosotros en apoyo . Agregaremos su contacto a la lista y le escribiremos cuando preparemos la primera versión alfa.


Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Egor Bredikhin. Desarrollo de un nuevo analizador estático:

¿Has leído el artículo y tienes una pregunta?

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


All Articles