Hoy día la calidad de nuestro código no debería ser un factor negociable, y gracias a Checkstyle veremos que es muy fácil mantener el control sobre el cumplimiento de unos mínimos exigibles.
Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. Ejecución y Configuración de Checkstyle
- 4. Estructura y funcionamiento
- 5. Desarrollar nuestras propias reglas
- 6. Conclusiones
- 7. Referencias
1. Introducción
La calidad del código depende, entre otros factores, de los defectos que se introducen en el mismo durante las fases de desarrollo o mantenimiento.
Para mantener o aumentar la calidad de nuestro código debemos ayudarnos, entre otras herramientas, de técnicas de análisis estáticos de código que, básicamente, se encargan de buscar defectos en el código sin necesidad de que este se ejecute.
El catálogo de estas técnicas es amplio, algunos ejemplos pueden ser: Análisis de valores extremos, análisis del flujo de control y de datos, reglas de codificación, reglas de seguridad o análisis de variable viva, entre otros muchos.
Por suerte, actualmente existen múltiples herramientas que mantienen implementaciones de estas técnicas para evaluar de manera automática el código implementado en distintos lenguajes.
JavaScript:
- JSLint
- JSHint
Objective-C:
- Clang
Java:
- Checkstyle
- FindBugs
- PMD
En este tutorial vamos a centrarnos en Checkstyle, herramienta OpenSource distribuida bajo licencia GNU LGPL. Veremos cómo realizar análisis, cómo configurar su alcance y, finalmente, cómo extender sus comprobaciones mediante reglas propias.
¡Al lío!
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
- Sistema Operativo: macOS Sierra 10.12.3.
- Entorno de desarrollo: IntelliJ IDEA ULTIMATE 2016.3
- Apache Maven 3.3.0.
- JDK 1.8.0_111
- Checkstyle 7.1.2
3. Ejecución y Configuración de Checkstyle
Por lo general, la ejecución de checkstyle se realizará desde otra de las muchas herramientas para las que se mantienen plugins de integración, actualmente: Eclipse, IntelliJ, NetBeans, Maven o Gradle, SonarQube ó Jenkins, sin embargo, en este momento nos vamos a centrar en la ejecución manual del análisis.
Lo primero que debemos realizar para poder proceder con el análisis es descargar la última release de Checkstyle desde sourceForge ó de su versión en desarrollo desde github.
Para proceder con la ejecución basta con hacer uso del fichero JAR descargado como se muestra a continuación:
$ java -jar checkstyle-7.5.1-all.jar -c /sun_checks.xml MyClass.java
Con la opción -c establecemos el fichero de configuración que delimitará el alcance de nuestro análisis.
Por defecto, checkstyle nos ofrece 2 ficheros de configuración que podremos utilizar:
- sun_checks.xml: Comprobará si nuestro código sigue un conjunto de estándares y buenas prácticas
entre las que se incluyen las convenciones de Sun ó el JLS (Java Language Specification). - google_checks.xml: Comprobará si nuestro código sigue los estándares de Google para proyectos
java (Pueden consultarse aquí).
Cualquiera de estas dos configuraciones nos servirá para conocer el estado de nuestro código a grandes rasgos y sin entrar en condiciones particulares.
En cualquier caso siempre podemos generar nuestros propios ficheros de configuración delimitando el alcance de los análisis, basta con seguir unas pautas como se puede ver aquí.
El resultado de la ejecución consistirá en un volcado por pantalla del total de las violaciones de las reglas que checkstyle haya detectado en su análisis, como se puede ver en el ejemplo a continuación:
$ java -jar checkstyle-7.5.1-all.jar com.puppycrawl.tools.checkstyle.Main -c /sun_checks.xml ~/IdeaProjects/Calculadora/src/ Starting audit... [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:0: Falta el archivo package-info.java. [JavadocPackage] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:3: Falta el comentario Javadoc. [JavadocType] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:4:1: '{' en la columna 1 debería estar en la línea anterior. [LeftCurly] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:5:5: Clase 'Calculadora' se parece a diseñada para la extensión (puede ser una subclase), pero el método 'suma' no tiene javadoc que explica cómo hacerlo de forma segura. Si la clase no está diseñado para la extensión de considerar la posibilidad de la clase 'Calculadora' final o haciendo el método 'suma' anotación static/final/abstract/empty, o la adición permitido para el método. [DesignForExtension] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:5:5: Falta el comentario Javadoc. [JavadocMethod] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:5:21: El parámetro operando1 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:5:36: El parámetro operando2 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:5:50: '{' no está precedido de espacio en blanco. [WhitespaceAround] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:9:5: Clase 'Calculadora' se parece a diseñada para la extensión (puede ser una subclase), pero el método 'resta' no tiene javadoc que explica cómo hacerlo de forma segura. Si la clase no está diseñado para la extensión de considerar la posibilidad de la clase 'Calculadora' final o haciendo el método 'resta' anotación static/final/abstract/empty, o la adición permitido para el método. [DesignForExtension] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:9:5: Falta el comentario Javadoc. [JavadocMethod] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:9:22: El parámetro operando1 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:9:37: El parámetro operando2 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:9:51: '{' no está precedido de espacio en blanco. [WhitespaceAround] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:13:5: Clase 'Calculadora' se parece a diseñada para la extensión (puede ser una subclase), pero el método 'multiplica' no tiene javadoc que explica cómo hacerlo de forma segura. Si la clase no está diseñado para la extensión de considerar la posibilidad de la clase 'Calculadora' final o haciendo el método 'multiplica' anotación static/final/abstract/empty, o la adición permitido para el método. [DesignForExtension] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:13:5: Falta el comentario Javadoc. [JavadocMethod] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:13:27: El parámetro operando1 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:13:42: El parámetro operando2 debería ser final. [FinalParameters] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/main/java/com/ejemplos/Calculadora.java:13:56: '{' no está precedido de espacio en blanco. [WhitespaceAround] Audit done. Checkstyle ends with 18 errors.
4. Estructura y funcionamiento
Las funcionalidades incluidas en Checksyle se encuentran implementadas en módulos formando una estructura de árbol. Los módulos más cercanos a la raíz, el módulo “núcleo” de Checkstyle, implementan la interfaz FileSetCheck que, básicamente, toma un conjunto de ficheros de entrada y como resultado lanza mensajes de error.
Checkstyle ofrece una serie de implementaciones por defecto de FileSetCheck:
- AbstractFileSetCheck.
- AbstractHeaderCheck.
- FileLengthCheck.
- FileTabCharacterCheck.
- HeaderCheck.
- JavaPackageCheck.
- RegexpHeaderCheck.
- …
Nos vamos a centrar en la implementación TreeWalker, que ese encarga de transformar cada fichero de entrada Java en un árbol sintáctico (AST) y obtener el resultado de aplicar secuencialmente validaciones en forma de Checks (implementaciones de la clase abstracta AbstractCheck).
Es decir, TreeWalker se dedica a recorrer en profundidad (depth-first) el árbol AST, llamando a los Checks correspondientes según los siguientes pasos:
- Antes de que se ejecute ningún Check, TreeWalker permite realizar inicializaciones a través del método beginTree().
- A continuación y recursivamente irá descendiendo por cada nodo desde el nodo raíz hasta los nodos hojas, llamando al método visitToken() de cada Check para comprobar las validaciones correspondientes.
- Cuando un sub-árbol haya sido evaluado se lanzará el método leaveToken() dejando constancia de que la sub-estructura ya ha sido recorrida.
- Finalmente, cuando se vuelva al nodo raíz para finalizar el recorrido se lanzará el método finishTree().
A continuación veremos cómo, mediante la implementación de la clase abstracta AbstractCheck, podemos implementar el método visitToken donde implementaremos la lógica de nuestras reglas de validación.
5. Desarrollar nuestras propias reglas
Como en otras ocasiones, vamos a proceder con la generación de un proyecto basado en maven, donde vamos a definir las dependencias de nuestro proyecto. El fichero pom quedará como el que se puede ver a continuación:
4.0.0 com.adictosaltrabajo.tutoriales.checks checkstyle-checks 1.0-SNAPSHOT com.puppycrawl.tools checkstyle 7.1.2 org.hamcrest hamcrest-all 1.3 test org.mockito mockito-all test 1.10.19 junit junit test 4.12
A continuación, vamos a proceder con la implementación de una regla de ejemplo mediante TDD para lo que necesitamos poder probar el Check que vamos a desarrollar.
Para esto último vamos a hacer uso de 3 clases creadas por el equipo de desarrollo de Checkstyle y que se puede encontrar en su repositorio de github, en la sección de test, a saber: com.puppycrawl.tools.checkstyle.BaseCheckTestSupport,
com.puppycrawl.tools.checkstyle.AuditEventUtFormatter y com.puppycrawl.tools.checkstyle.BriefUtLogger.
Haremos que nuestra clase de pruebas extienda la clase BaseCheckTestSupport y estableceremos los diferentes Tests. A continuación se muestra la clase de test.
package com.adictosaltrabajo.tutoriales.checks; import com.puppycrawl.tools.checkstyle.BaseCheckTestSupport; import com.puppycrawl.tools.checkstyle.DefaultConfiguration; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; import org.junit.Test; import java.io.File; import java.io.IOException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; public class MyExampleCheckTest extends BaseCheckTestSupport { public static final String MAX_LENGTH_TO_ERROR_TEST = "15"; public static final String MAX_LENGTH_TO_NON_ERROR_TEST = "50"; @Override protected String getPath(String filename) throws IOException { return super.getPath("checks" + File.separator + "myExampleCheck" + File.separator + filename); } @Test public void getRequiredTokensShouldReturnEmptyArray() { final MyExampleCheck checkObj = new MyExampleCheck(); assertThat(checkObj.getRequiredTokens(), is(CommonUtils.EMPTY_INT_ARRAY)); } @Test public void getAcceptableTokensShouldReturnMethodDefTokenArray() { final MyExampleCheck checkObj = new MyExampleCheck(); final int[] actual = checkObj.getAcceptableTokens(); final int[] expected = { TokenTypes.METHOD_DEF }; assertThat(actual, is(expected)); } @Test public void validationShouldReturnMultipleErrorsWhenMethodNameLengthExceedTheMaximum() throws Exception { final DefaultConfiguration checkConfiguration = createCheckConfig(MyExampleCheck.class); checkConfiguration.addAttribute("maxLength", MAX_LENGTH_TO_ERROR_TEST); final String[] expected = { "37: the method name is too long, only " + MAX_LENGTH_TO_ERROR_TEST + " method name length are allowed", "42: the method name is too long, only " + MAX_LENGTH_TO_ERROR_TEST + " method name length are allowed", "51: the method name is too long, only " + MAX_LENGTH_TO_ERROR_TEST + " method name length are allowed", "61: the method name is too long, only " + MAX_LENGTH_TO_ERROR_TEST + " method name length are allowed" }; verify(checkConfiguration, getPath("RestControllerHandler.java"), expected); } @Test public void validationShouldReturnNonErrorsWhenMethodNameLengthDoNotExceedTheMaximum() throws Exception { final DefaultConfiguration checkConfiguration = createCheckConfig(MyExampleCheck.class); checkConfiguration.addAttribute("maxLength", MAX_LENGTH_TO_NON_ERROR_TEST); final String[] expected = CommonUtils.EMPTY_STRING_ARRAY; verify(checkConfiguration, getPath("RestControllerHandler.java"), expected); } @Test public void validationShouldReturnNonErrorsWhenClassHasNoMethods() throws Exception { final DefaultConfiguration checkConfiguration = createCheckConfig(MyExampleCheck.class); checkConfiguration.addAttribute("maxLength", MAX_LENGTH_TO_ERROR_TEST); final String[] expected = CommonUtils.EMPTY_STRING_ARRAY; verify(checkConfiguration, getPath("OtherClassToTest.java"), expected); } }
Los ficheros RestcontrollerHandler.java y OtherClassToTest.java se encuentran en la ruta /test/resources/checks/myExampleCheck de nuestro proyecto.
La regla consistirá en comprobar que los métodos de nuestras clases no mantengan más de 15 caracteres. Esta regla en sí no tiene mucho sentido, pero nos servirá a modo de ejemplo para mostrar cómo proceder para implementar reglas.
A continuación, el código de nuestra regla:
package com.adictosaltrabajo.tutoriales.checks; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; public class MyExampleCheck extends AbstractCheck { private int maxLength = 15; public int[] getDefaultTokens() { return new int[]{TokenTypes.METHOD_DEF}; } public void setMaxLength(int maxLength) { this.maxLength = maxLength; } @Override public void visitToken(DetailAST ast) { DetailAST methodNameNode = ast.findFirstToken(TokenTypes.IDENT); if(methodNameNode.getText().length() > maxLength) { String message = "the method name is too long, only " + this.maxLength + " method name length are allowed"; log(methodNameNode.getLineNo(), message); } } }
A continuación vamos a explicar el código de la anterior clase:
- El primer punto, como hemos visto anteriormente, pasa por extender la clase AbstractCheck.
- Seguidamente, el método getDefaultTokens establece los tokens que identificarán los tipos de nodo sobre los que el método vistToken de este check en particular se ejecutará. Como la regla de validación que estamos creando comprueba la longitud del nombre de los métodos, estableceremos como único token a validar TokenTypes.METHOD_DEF. La clase TokenTypes nos ofrece la totalidad de los tipos de nodos existentes en el árbol AST.
- Posteriormente establecemos un parámetro configurable que marcará la longitud máxima de los nombres de método.
- Para finalizar implementamos el método visitToken donde estableceremos la lógica de validación. En su interior, la llamada al método log(methodNameNode.getLineNo(), message); da de alta una violación de la regla en el análisis global.
Tan solo queda establecer los parámetros de configuración a través del fichero de configuración xml:
Tras empaquetar el proyecto podremos ejecutar el análisis incluyendo nuestra nueva regla mediante:
java -classpath ./target/checkstyle-checks-1.0-SNAPSHOT.jar:checkstyle-7.5.1-all.jar com.puppycrawl.tools.checkstyle.Main -c /config.xml ~/IdeaProjects/Calculadora/src/
El resultado será similar al que se puede ver a continuación:
Starting audit... [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:20: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:25: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:30: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:35: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:40: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:45: the method name is too long, only 10 method name length are allowed [MyExample] [ERROR] /Users/jrodriguez/IdeaProjects/Calculadora/src/test/java/com/ejemplos/CalculadoraTest.java:50: the method name is too long, only 10 method name length are allowed [MyExample] Audit done. Checkstyle ends with 7 errors.s
6. Conclusiones
Como hemos visto, el análisis estático de código es una parte primordial en el mantenimiento y aseguramiento de la calidad de nuestro código y mediante Checkstyle podemos hacerlo de forma cómoda, rápida y sencilla.
Además, podemos establecer el alcance de nuestros análisis y generar nuestras propias validaciones de calidad en caso de que nuestro contexto así lo requiera.
¡Ya no tenéis excusas para no proceder con estos análisis!
7. Referencias
- Checkstyle
- Wikipedia Análisis estático
- Lluna, E; (2011). Análisis estático de código en el ciclo de desarrollo de software de seguridad crítica. REICIS. Revista Española de Innovación, Calidad e Ingeniería del Software, 7() 26-38.
- OWASPAnálisis estático de código
Excelente, muchas gracias, ahora a buscar como automatizarlo con jenkins
Hola, una consulta: hace análisis sintáctico de código? por ejemplo: void Main({return;}