martes, 2 de julio de 2013

Herramientas mágicas: CppCheck

En la primer entrega de la serie Herramientas Mágicas les presenté Valgrind, un analizador dinámico. Lo de dinámico es porque analiza al programa en movimiento, mientras se ejecuta. No es el único de este tipo, ya volveremos a eso, pero ahora voy a presentar un analizador estático: CppCheck. Esto quiere decir que, por el contrario, este tipo de analizador no ejecuta al programa en cuestión, sino que se basa solamente en inspeccionar muy minuciosamente su código fuente.

Para hacer una analogía con algo conocido, digamos que un analizador estático es parecido a medio compilador. Sería la parte del compilador que mira el código generando los warnings y errores, no la que traduce. Pero el objetivo del compilador es traducir a código máquina. Dado que para ello tiene que hacer un análisis previo del código fuente, va detectando "de paso" (como efecto colateral) potenciales errores que avisa en forma de warning. Pero solo hace el análisis necesario para traducir y eventualmente optimizar, y no pierde tiempo en otros detalles. Un analizador estático como CppCheck, en cambio, tiene por objetivo el encontrar errores, por lo que el tipo de análisis que hace toma más tiempo, para detectar cosas más específicas o intrincadas.

Pueden descargarlo desde su sitio web e instalarlo. Luego pueden utilizar su interfaz, o invocarlo a través de ZinjaI desde el menú Herramientas. Al ejecutar el análisis estático en ZinjaI verán una nueva ventana que irá mostrando las dos salidas (la estándar arriba, que indica el progreso, la de error abajo, que muestra los mensajes que nos interesan). Una vez finalizado el análisis, los resultados se organizarán según categorías en un panel inferior acoplado a la ventana principal (muy similar a la forma en que se presentan los resultados de valgrind). A continuación, un ejemplo de código y resultados para empezar a ver qué tipo de errores puede detectar.

Ejemplo de resultados de CppCheck en ZinjaI

El primer error que muestra la imagen es bastante interesante. Hay un for que va de 0 a n leyendo datos en un arreglo, pero el arreglo está declarado de 10 elementos, y en ese punto n vale 15. Noten que puede encontrar errores de memoria con solo inspeccionar nuestro código. Aquí es más o menos evidente ese problema, pero si el arreglo estuviese declarado en otro lado (algún .h por ejemplo), o n inicializada más lejos podría ser más difícil de detectar a simple vista. Y noten también que no es tan trivial para un análisis automático detectar esto (tiene que rastrear la evolución de n).

Otros problemas son en realidad potenciales problemas. Por ejemplo, la clase de la imagen tiene un puntero como atributo, al cual se le hace un new en el constructor, pero no tiene ni un destructor que libere esa memoria, ni un constructor de copia que la duplique si intentamos asignar dos instancias de esa clase. Aunque no los necesitemos en este main (no hay copias, y la clase se destruye solo al finalizar el programa), sería buena práctica implementarlos si pensamos reutilizar esa clase más adelante. En la misma categoría se detectan por ejemplo atributos que no son inicializados en el constructor (directa o indirectamente, puede ser mediante llamadas a otras funciones, o mediante sus propios constructores).

Otros mensajes tienen que ver con la eficiencia del programa. Por ejemplo, para preguntar si la lista está vacía el ejemplo usa el método size (que calcula el tamaño, y dependiendo de la implementación podría requerir recorrerla), en lugar de empty (que en cualquier implementación se puede hacer con una sola operación). Y también detecta cosas innecesarias que ensucian nuestro código, como declarar una variable y no hacer con ella nada útil (vean que y se usa en una asignación, pero cppcheck se da cuenta de que el valor asignado luego no se usa para nada, y entonces concluye que tanto la variable como la asignación pueden eliminarse sin cambiar el programa).

Y estos son apenas algunos ejemplos que elegí, fáciles de ver y entender, pero hay muchísimos errores más que cppcheck es capaz de detectar. Algunos ejemplos pueden ser condiciones redundantes o contradictorias, errores comunes en el uso de bibliotecas estándar (con énfasis en STL), ámbitos de variables que pueden reducirse, funciones que no se invocan nunca, argumentos que convendría pasar por referencia, etc, por citar los que recuerdo en este momento, seguro hay otros más interesantes. De hecho hay tantos que a veces molestan, por eso cuando un supuesto error está cometido adrede, se puede indicar a cppcheck que haga una excepción en esa línea (mediante un comentario especial en el fuente), o hasta pedirle que ignore todos los errores de ese tipo. En ZinjaI, se configura desde el menú de herramientas, aunque muy probablemente en la próxima versión puedan agregar la excepción (cualquiera de los dos tipos) directamente desde el panel de resultados simplemente haciendo click derecho sobre el error y eligiendo la opción en el menú contextual.

Cuadro de configuración para CppCheck en ZinjaI

Según los desarrolladores de cppcheck, tienen especial antención en evitar falsos positivos (evitar marcar errores que no lo son), pero a veces pasa, o a veces son realmente errores que en casos particulares no interesa corregir, o que sabemos que por la entrada que recibirá el programa no se van a manifestar. Además, no solo marca errores, sino problemas de estilo, lo cual es mucho más frecuente de encontrar, y a veces en bibliotecas que no son nuestras, y por ende no podemos corregir. Para estos casos, podemos configurar las excepciones, desactivar categorías completas, o pedirle explícitamente que ignore ciertos fuentes, todo ello desde ZinjaI mediante el menú herramientas.

Hay muchos programas de este tipo. Dentro de lo libre y portable, CppCheck me pareció lo mejor y según lo que leí es de lo que más se utiliza. La gran alternativa que ha surgido últimamente está basada en llvm y clang, y tiene como ventaja ser parte del proceso de compilación (ya que es parte del compilador, aunque lo hace más lento). ZinjaI ya incluye un soporte experimental para clang (se configura fácilmente como toolchain alternativo), pero por el momento es más difícil de conseguir e instalar que gcc (en Windows no encontré una forma fácil, en GNU/Linux está en los repositorios, pero no siempre actualizado, y no es un detalle menor ya que ha mejorado mucho en las últimas versiones). En algún punto experimentaré más con esto, pero de momento uso CppCheck en mi día a día y no tengo de qué quejarme.

En conclusión, es un tipo herramienta fundamental para mantener un código prolijo, y en muchas ocasiones resulta útil para corregir errores (aún antes de que se manifiesten). No cuesta nada pasar el analizador estático regularmente por nuestros proyectos para asegurarnos de que todo vaya bien, o al menos eso parezca.

No hay comentarios:

Publicar un comentario