Implemente el análisis estático en el proceso, en lugar de buscar errores con él

Me inspiré para escribir este artículo en una gran cantidad de materiales sobre análisis estático que aparecen cada vez más. Primero, esto Blog del estudio PVS, que se promociona activamente en Habré con revisiones de errores encontrados por su herramienta en proyectos de código abierto. PVS-studio implementado recientemente Soporte Javay, por supuesto, los desarrolladores de IntelliJ IDEA, cuyo analizador integrado es probablemente el más avanzado para Java en la actualidad, no podía mantenerse alejado.

Al leer tales reseñas, uno tiene la sensación de que estamos hablando de un elixir mágico: presione el botón y aquí está: una lista de defectos frente a sus ojos. Parece que a medida que mejoran los analizadores, automáticamente habrá más y más errores, y los productos escaneados por estos robots serán cada vez mejores, sin ningún esfuerzo de nuestra parte.

Pero no hay elixires mágicos. Me gustaría hablar sobre lo que generalmente no se discute en publicaciones como "estas son las cosas que nuestro robot puede encontrar": lo que los analizadores no pueden hacer, cuál es su función real y su lugar en el proceso de entrega de software y cómo implementarlos correctamente.

Implemente el análisis estático en el proceso, en lugar de buscar errores con él
Trinquete (fuente: Wikipedia).

Lo que los analizadores estáticos nunca pueden hacer

¿Qué es, desde un punto de vista práctico, el análisis de código fuente? Alimentamos algunas fuentes y en poco tiempo (mucho más corto que ejecutar pruebas) obtenemos información sobre nuestro sistema. La limitación fundamental y matemáticamente insuperable es que solo podemos obtener una clase de información bastante limitada.

El ejemplo más famoso de un problema que no puede resolverse mediante análisis estático es detener el problema: este es un teorema que prueba que es imposible desarrollar un algoritmo general que determine a partir del código fuente del programa si se repetirá o terminará en un tiempo finito. Una extensión de este teorema es teorema de arroz, que establece que para cualquier propiedad no trivial de las funciones computables, determinar si un programa arbitrario evalúa una función con tal propiedad es un problema algorítmicamente irresoluble. Por ejemplo, es imposible escribir un analizador que pueda determinar a partir de cualquier código fuente si el programa analizado es una implementación de un algoritmo que calcula, por ejemplo, elevar al cuadrado un número entero.

Por lo tanto, la funcionalidad de los analizadores estáticos tiene limitaciones insuperables. Un analizador estático nunca podrá determinar en todos los casos cosas como, por ejemplo, la ocurrencia de "excepción de puntero nulo" en lenguajes anulables, o en todos los casos determinar la ocurrencia de "atributo no encontrado" en lenguajes con escritura dinámica. Todo lo que puede hacer el analizador estático más avanzado es resaltar casos especiales, el número de los cuales, entre todos los posibles problemas con su código fuente, es, sin exagerar, una gota en el océano.

El análisis estático no es una búsqueda de errores

La conclusión se deriva de lo anterior: el análisis estático no es un medio para reducir el número de defectos en un programa. Me atrevería a decir que cuando se aplique por primera vez a su proyecto, encontrará lugares "divertidos" en el código, pero lo más probable es que no encuentre ningún defecto que afecte la calidad de su programa.

Los ejemplos de defectos encontrados automáticamente por los analizadores son impresionantes, pero no debemos olvidar que estos ejemplos se encontraron escaneando un gran conjunto de grandes bases de código. Por el mismo principio, los crackers que pueden probar varias contraseñas simples en una gran cantidad de cuentas finalmente encuentran aquellas cuentas que tienen una contraseña simple.

¿Significa esto que no se debe aplicar el análisis estático? ¡Por supuesto que no! Y exactamente por la misma razón por la que vale la pena verificar cada nueva contraseña para ingresar a la lista de bloqueo de contraseñas "simples".

El análisis estático es más que encontrar errores

De hecho, los problemas prácticamente resueltos por el análisis son mucho más amplios. Después de todo, en general, el análisis estático es cualquier verificación de los códigos fuente que se lleva a cabo antes de su lanzamiento. Aqui hay algunas cosas que puedes hacer:

  • Comprobación del estilo de codificación en el sentido más amplio de la palabra. Esto incluye verificar el formato y buscar el uso de paréntesis vacíos/adicionales, establecer umbrales en métricas como número de líneas/complejidad del método ciclomático, etc., todo lo que potencialmente hace que el código sea más legible y mantenible. En Java esta herramienta es Checkstyle, en Python es flake8. Los programas de esta clase se denominan normalmente "linters".
  • No solo se puede analizar el código ejecutable. Los archivos de recursos como JSON, YAML, XML, .properties pueden (¡y deben!) verificar automáticamente su validez. ¿No es mejor descubrir que, debido a algunas comillas no emparejadas, la estructura JSON se rompe en una etapa temprana de la validación automática de solicitud de extracción que cuando se ejecutan pruebas o en tiempo de ejecución? Las herramientas apropiadas están disponibles: por ejemplo, YAMLlint, JSONLint.
  • La compilación (o el análisis de lenguajes de programación dinámicos) también es un tipo de análisis estático. Como regla general, los compiladores pueden emitir advertencias que indican problemas con la calidad del código fuente y no deben ignorarse.
  • A veces, la compilación no se trata solo de compilar código ejecutable. Por ejemplo, si tiene documentación en el formato AsciiDoctor, luego en el momento de su transformación en el controlador HTML/PDF AsciiDoctor (Complemento de Maven) puede emitir advertencias, por ejemplo, sobre enlaces internos rotos. Y esta es una buena razón para no aceptar el Pull Request con cambios en la documentación.
  • La revisión ortográfica también es un tipo de análisis estático. Utilidad un hechizo es capaz de verificar la ortografía no solo en la documentación, sino también en los códigos fuente del programa (comentarios y literales) en diferentes lenguajes de programación, incluidos C/C++, Java y Python. ¡Un error ortográfico en la interfaz de usuario o en la documentación también es un defecto!
  • Pruebas de configuración (para lo que es, consulte este и este informes), aunque se ejecutan en un tiempo de ejecución de prueba unitaria como pytest, en realidad también son una especie de análisis estático, ya que no ejecutan códigos fuente durante su ejecución.

Como puede ver, encontrar errores en esta lista tiene el papel menos importante, y todo lo demás está disponible mediante el uso de herramientas gratuitas de código abierto.

¿Cuál de estos tipos de análisis estático debería utilizarse en su proyecto? Por supuesto, ¡cuanto más, mejor! Lo principal es implementarlo correctamente, lo cual se discutirá más adelante.

Pipeline de entrega como un filtro de varias etapas y análisis estático como su primera cascada

La metáfora clásica para la integración continua es la canalización (pipeline) a través de la cual fluyen los cambios, desde cambiar el código fuente hasta la entrega a producción. La secuencia estándar de etapas de esta tubería se ve así:

  1. análisis estático
  2. compilación
  3. pruebas unitarias
  4. pruebas de integración
  5. pruebas de interfaz de usuario
  6. comprobación manual

Los cambios rechazados en la etapa N de la canalización no se propagan a la etapa N+1.

¿Por qué exactamente de esta manera y no de otra manera? En la parte de prueba de la canalización, los evaluadores reconocerán la conocida pirámide de prueba.

Implemente el análisis estático en el proceso, en lugar de buscar errores con él
Pirámide de prueba. Fuente: artículo Martín Fowler.

En la parte inferior de esta pirámide hay pruebas que son más fáciles de escribir, se ejecutan más rápido y no tienden a dar falsos positivos. Por lo tanto, debería haber más de ellos, deberían cubrir más código y ejecutarse primero. En la parte superior de la pirámide, ocurre lo contrario, por lo que la cantidad de pruebas de integración y de interfaz de usuario debe reducirse al mínimo necesario. La persona en esta cadena es el recurso más costoso, más lento y menos confiable, por lo que se encuentra al final y solo realiza el trabajo si las etapas anteriores no encontraron ningún defecto. Sin embargo, de acuerdo con los mismos principios, la canalización se construye en partes que no están directamente relacionadas con las pruebas.

Me gustaría ofrecer una analogía en forma de un sistema de filtración de agua de varias etapas. A la entrada se suministra agua sucia (cambios con defectos), a la salida debemos conseguir agua limpia, en la que se eliminen todos los contaminantes no deseados.

Implemente el análisis estático en el proceso, en lugar de buscar errores con él
Filtro multietapa. Fuente: Wikimedia Commons

Como sabe, los filtros de limpieza están diseñados de tal manera que cada siguiente cascada puede filtrar una fracción cada vez menor de contaminantes. Al mismo tiempo, las cascadas de purificación más gruesas tienen un mayor rendimiento y un menor costo. En nuestra analogía, esto significa que las puertas de calidad de entrada son más rápidas, requieren menos esfuerzo para iniciarse y su funcionamiento es menos pretencioso, y esta es exactamente la secuencia en la que están integradas. El papel del análisis estático, que, como ahora entendemos, es capaz de eliminar solo los defectos más graves, es el papel del "barro" enrejado al comienzo mismo de la cascada de filtros.

El análisis estático por sí solo no mejora la calidad del producto final, al igual que una “trampa de lodo” no hace que el agua sea potable. Y sin embargo, al igual que otros elementos del transportador, su importancia es obvia. Aunque en un filtro de múltiples etapas, las etapas de salida son potencialmente capaces de capturar todo de la misma manera que las etapas de entrada, está claro a qué consecuencias conducirá un intento de arreglárselas solo con etapas finas de purificación, sin etapas de entrada.

El propósito del "recolector de lodo" es descargar cascadas posteriores de capturar defectos muy graves. Por ejemplo, como mínimo, un revisor de código no debe distraerse con un código con formato incorrecto y violaciones de los estándares de codificación establecidos (como paréntesis adicionales o ramas anidadas demasiado profundas). Los errores como NPE deben detectarse mediante pruebas unitarias, pero si incluso antes de la prueba el analizador nos indica que el error debe ocurrir inevitablemente, esto acelerará significativamente su solución.

Creo que ahora está claro por qué el análisis estático no mejora la calidad de un producto si se usa ocasionalmente y debe usarse constantemente para filtrar cambios con defectos graves. Preguntar si el uso de un analizador estático mejorará la calidad de su producto es más o menos equivalente a preguntar "¿Mejorará la calidad potable del agua extraída de un estanque sucio si se pasa por un colador?"

Implementación en un proyecto heredado

Una pregunta práctica importante: ¿cómo introducir el análisis estático en el proceso de integración continua como una "puerta de calidad"? En el caso de las pruebas automáticas, todo es obvio: hay un conjunto de pruebas, el fallo de cualquiera de ellas es motivo suficiente para creer que el montaje no pasó la puerta de calidad. Un intento de instalar una puerta de la misma manera basada en los resultados del análisis estático falla: hay demasiadas advertencias de análisis en el código heredado, no desea ignorarlas por completo, pero también es imposible detener la entrega de un producto solo porque contiene advertencias del analizador.

Cuando se utiliza por primera vez, el analizador genera una gran cantidad de advertencias sobre cualquier proyecto, la gran mayoría de las cuales no están relacionadas con el correcto funcionamiento del producto. Es imposible corregir todos estos comentarios a la vez, y muchos de ellos no son necesarios. Después de todo, sabemos que nuestro producto funciona como un todo, ¡incluso antes de la introducción del análisis estático!

Como resultado, muchas personas se limitan al uso episódico del análisis estático, o lo usan solo en modo informativo, cuando el informe del analizador simplemente se emite durante el ensamblaje. Esto equivale a la ausencia de cualquier análisis, porque si ya tenemos muchas advertencias, entonces la ocurrencia de otra (por grave que sea) cuando cambia el código pasa desapercibida.

Se conocen las siguientes formas de introducir puertas de calidad:

  • Establece un límite en el número total de advertencias, o el número de advertencias dividido por el número de líneas de código. Esto no funciona bien, porque dicha puerta salta libremente los cambios con nuevos defectos hasta que se excede su límite.
  • Arreglar, en algún momento, todas las advertencias antiguas en el código como ignoradas y fallar la compilación cuando ocurren nuevas advertencias. Esta funcionalidad la proporcionan PVS-studio y algunos recursos en línea, como Codacy. No tuve la oportunidad de trabajar en PVS-studio, en cuanto a mi experiencia con Codacy, su principal problema es que la definición de qué es un error "antiguo" y qué es un error "nuevo" es un algoritmo bastante complicado que no siempre funcionan correctamente, especialmente si los archivos se modifican mucho o se les cambia el nombre. En mi memoria, Codacy podía omitir nuevas advertencias en una solicitud de extracción y, al mismo tiempo, no omitir una solicitud de extracción debido a advertencias que no estaban relacionadas con cambios en el código de este PR.
  • En mi opinión, la solución más efectiva se describe en el libro. Entrega Continua método de "trinquete". La idea principal es que una propiedad de cada versión es la cantidad de advertencias de análisis estático, y solo se permiten cambios que no aumenten la cantidad total de advertencias.

Trinquete

Funciona así:

  1. En la etapa inicial, se implementa un registro en los metadatos de publicación del número de advertencias en el código encontrado por los analizadores. Por lo tanto, cuando construye upstream, su administrador de repositorio no solo se escribe "versión 7.0.2", sino "versión 7.0.2 que contiene 100500 advertencias de Checkstyle". Si está utilizando un administrador de repositorio avanzado (como Artifactory), es fácil almacenar dichos metadatos sobre su lanzamiento.
  2. Ahora, cada solicitud de incorporación de cambios compara la cantidad de advertencias que recibe con la cantidad de la versión actual. Si PR conduce a un aumento en este número, entonces el código no pasa la puerta de calidad en el análisis estático. Si el número de advertencias disminuye o no cambia, entonces pasa.
  3. En la próxima versión, el número de advertencias recalculado se volverá a escribir en los metadatos de la versión.

Así, poco a poco, pero de forma constante (como con un trinquete), el número de avisos tenderá a cero. Por supuesto, se puede engañar al sistema introduciendo un nuevo aviso, pero corrigiendo el de otra persona. Esto es normal, porque a la larga da el resultado: las advertencias se corrigen, por regla general, no una por una, sino inmediatamente por un grupo de cierto tipo, y todas las advertencias que se eliminan fácilmente se eliminan con bastante rapidez.

Este gráfico muestra el número total de advertencias Checkstyle durante seis meses de operación de tal "trinquete" en uno de nuestros proyectos de código abierto. El número de advertencias ha disminuido en un orden de magnitud, y esto sucedió naturalmente, ¡en paralelo con el desarrollo del producto!

Implemente el análisis estático en el proceso, en lugar de buscar errores con él

Utilizo una versión modificada de este método, contando por separado las advertencias por módulo de proyecto y herramienta de análisis, lo que da como resultado un archivo YAML con metadatos de ensamblaje que se parece a esto:

celesta-sql:
  checkstyle: 434
  spotbugs: 45
celesta-core:
  checkstyle: 206
  spotbugs: 13
celesta-maven-plugin:
  checkstyle: 19
  spotbugs: 0
celesta-unit:
  checkstyle: 0
  spotbugs: 0

En cualquier sistema de CI avanzado, se puede implementar un trinquete para cualquier herramienta de análisis estático sin depender de complementos y herramientas de terceros. Cada uno de los analizadores produce su informe en un texto simple o formato XML que es fácil de analizar. Queda por registrar solo la lógica necesaria en el script de CI. Puede ver cómo se implementa esto en nuestros proyectos de código abierto basados ​​en Jenkins y Artifactory, puede aquí o aquí. Ambos ejemplos dependen de la biblioteca. ratchetlib: método countWarnings() cuenta etiquetas xml en archivos generados por Checkstyle y Spotbugs de la forma habitual, y compareWarningMaps() implementa el mismo trinquete, arrojando un error cuando aumenta el número de advertencias en cualquiera de las categorías.

Es posible una implementación de trinquete interesante para el análisis ortográfico de comentarios, textos literales y documentación utilizando aspell. Como sabe, al verificar la ortografía, no todas las palabras desconocidas para el diccionario estándar son incorrectas, se pueden agregar al diccionario del usuario. Si hace que el diccionario del usuario forme parte del código fuente del proyecto, la puerta de calidad ortográfica se puede formular de la siguiente manera: una ejecución del hechizo con el diccionario estándar y del usuario no debería no encontrar errores ortográficos.

Sobre la importancia de arreglar la versión del analizador

En conclusión, se debe tener en cuenta lo siguiente: no importa cómo implemente el análisis en su canal de entrega, la versión del analizador debe ser fija. Si permite que el analizador se actualice espontáneamente, al crear la siguiente solicitud de extracción, pueden "surgir" nuevos defectos, que no están relacionados con cambios en el código, pero están relacionados con el hecho de que el nuevo analizador simplemente puede encontrar más defectos. y esto interrumpirá su proceso de aceptación de solicitudes de incorporación de cambios. Actualizar el analizador debe ser una acción consciente. Sin embargo, fijar la versión de cada componente del ensamblaje es un requisito necesario en general y un tema para una discusión por separado.

Hallazgos

  • El análisis estático no encontrará errores y no mejorará la calidad de su producto como resultado de una sola aplicación. El único efecto positivo sobre la calidad es su uso continuo durante el proceso de entrega.
  • Encontrar errores no es la principal tarea de análisis en absoluto, la gran mayoría de las funciones útiles están disponibles en herramientas de código abierto.
  • Implemente puertas de calidad basadas en los resultados del análisis estático en la primera etapa de la tubería de entrega, utilizando un trinquete para el código heredado.

referencias

  1. Entrega Continua
  2. A. Kudryavtsev: Análisis de programas: cómo entender que eres un buen programador informe sobre diferentes métodos de análisis de código (¡no solo estático!)

Fuente: habr.com

Añadir un comentario