Análisis estático de código con Checkstyle

2
9485

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

Checkstyle Logo

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:

  1. Antes de que se ejecute ningún Check, TreeWalker permite realizar inicializaciones a través del método beginTree().
  2. 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.
  3. Cuando un sub-árbol haya sido evaluado se lanzará el método leaveToken() dejando constancia de que la sub-estructura ya ha sido recorrida.
  4. 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

2 COMENTARIOS

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad