Regla de análisis de código con Android Lint (parte 1/2)

Para Kotlin y Java a la vez

Balázs Ruda
Balázs Ruda

Sigue
Sep 9, 2019 – 9 min read

TL;DR

En esta primera parte del artículo, en primer lugar, quiero demostrar que vale la pena el tiempo para escribir una regla de análisis de código personalizado para hacer cumplir las convenciones de código. En segundo lugar, quiero demostrar que, además de Checkstyle y ktlint, también hay que tener en cuenta Android Lint a la hora de crear una regla de análisis de código relacionada con el formato, a pesar de no ser ese su propósito básico. Explicaré su principal ventaja en mi caso frente a las otras herramientas, y describiré el funcionamiento interno que proporciona este beneficio. Para inspirarme, también mostraré los pasos que seguí para hacer que mi regla de formato funcionara con Android Lint.

Motivación

Me frustro cuando recibo comentarios relacionados con el formato en mis solicitudes de fusión o cuando tengo que añadir dichos comentarios a las de otros. A veces, tenemos que cambiar de rama para arreglar un solo error de formato en nuestra RM, lo cual es bastante desmotivador. Por eso empecé a pensar en formas de automatizar la solución de este problema menor (pero importante), para poder centrarnos en cuestiones más complejas durante las revisiones de código.

Utilizamos varias herramientas de lint, como ktlint o Checkstyle, para hacer cumplir las convenciones de formato globales (orden de las variables, longitud de las líneas, etc.), pero éstas no incluyen ciertas reglas de la empresa por defecto. Para esos casos, las herramientas mencionadas anteriormente también permiten añadir reglas personalizadas. He decidido escribir una para los errores de formato más comunes que cometemos:

Está faltando una línea vacía alrededor de las «sentencias de bloque»

Siempre requerimos saltos de línea dobles antes y después de las «sentencias de bloque», que pueden ser los bloques if, switch (cuando en Kotlin), for, try o while, a menos que estén exactamente al principio o al final de un método u otro bloque. Creemos que esto hace que nuestro código sea más legible, por eso siempre dejamos un comentario durante la revisión del código si alguien viola esta regla (y si por casualidad nos damos cuenta).

Cuál es la mejor herramienta para este problema

Podemos decir que ktlint para Kotlin y Checkstyle para Java son las principales herramientas de análisis de código de formato, pero ahora voy a elegir otra herramienta todavía: Android Lint (no sólo para proyectos Android), porque también soporta la escritura de reglas personalizadas que se pueden aplicar a Kotlin y Java al mismo tiempo. Parece una opción más lógica, porque usamos ambos lenguajes en nuestros proyectos Android, y no quería ni escribir la misma regla dos veces ni mantenerlas simultáneamente. Por no hablar de la integración de la regla, que también habría que hacer dos veces.

Por qué Java sigue siendo importante en absoluto

Como desarrollador de Android, tengo que escribir código tanto en Java como en Kotlin. Sin embargo, podemos decir que ya no vale la pena centrarse en Java, porque a medida que Kotlin va surgiendo, lo usamos cada vez más en lugar de Java en nuestra base de código. Por otro lado, creo que la transición no está ocurriendo tan rápidamente en los grandes proyectos que ya están en producción. Así que, como queremos ver código bien formateado también en Java, es importante tener esta regla para ambos lenguajes.

Cómo escribir reglas de lint personalizadas

Hay muchos tutoriales y artículos sobre cómo escribir reglas de análisis de código personalizadas, así que no lo detallaré demasiado en este post. Pero si te interesa, aquí tienes un par de enlaces que te recomiendo para las diferentes herramientas: A Checkstyle, el doc oficial, a ktlint y a Android Lint, los estupendos artículos de Niklas Baudy en el medio.

Ejemplo para AST (fuente: Wikipedia sobre el Árbol de Sintaxis Abstracta)

Cómo funcionan Android Lint y otras herramientas de análisis de código estático

Inicialmente, Android Lint fue creado para encontrar principalmente problemas específicos de Android en el código Java. Para analizar el código Java, Android Lint solía crear un Árbol de Sintaxis Abstracto específico para Java (o simplemente AST, aprende más en Wikipedia), que es una representación en forma de árbol del código fuente. Otras herramientas de análisis de código estático también utilizan un AST específico del lenguaje para el análisis;
Checkstyle: Java-, ktlint: Kotlin-, detekt: Específico de Kotlin. Las herramientas recorren este AST y encuentran los errores comprobando sus nodos y sus atributos.

Cómo Android Lint puede comprobar dos lenguajes a la vez

La diferencia entre Android Lint y otras herramientas es que como Kotlin también se convirtió en un lenguaje soportado por Android, Android Lint empezó a soportar Kotlin también respecto a las reglas que ya teníamos para Java. Para ello, introdujeron el llamado Árbol de Sintaxis Abstracto Universal (desarrollado por JetBrains) que proporciona una representación en forma de árbol del código que se puede aplicar tanto para los lenguajes Kotlin como Java al mismo tiempo, por lo que está en un nivel de abstracción más alto que un AST específico del lenguaje. Para más información, recomiendo esta parte de la charla de Tor Norbye – el creador y mantenedor de Android Lint.

Tanto UAST como AST proporcionan detalles de alto nivel sobre el código fuente. No contienen información sobre espacios en blanco o llaves, pero suele ser suficiente para las reglas específicas de Android. Vea un ejemplo de UAST a continuación:

Al llamar al método asRecursiveLogString() en un determinado nodo UAST en la API de Android Lint, podemos imprimir el UAST.

La biblioteca UAST incluye todas las expresiones específicas del lenguaje, pero también proporciona interfaces comunes para ellas. Ejemplo para la expresión if:

La interfaz comúnUIfExpression está implementada por la JavaUIfExpression, JavaUTernaryExpression, y KotlinUIfExpression.

Árbol PSI (Árbol de la Interfaz de la Estructura del Programa)

El árbol PSI se construye sobre el UAST en el caso de Android Lint (en el caso de otras herramientas, se construye sobre el AST específico del lenguaje), y es el árbol que contiene más detalles sobre la estructura del código, como espacios en blanco, llaves y elementos similares.

En Android Lint, las Expresiones PSI se incluyen en la implementación de las Expresiones UAST, que son los nodos del UAST. Por ejemplo, org.jetbrains.kotlin.psi.KtIfExpression se puede acceder desde KotlinUIfExpression.

Incluso hay un práctico plugin de intelliJ: PsiViewer que puede facilitar la depuración de reglas de análisis de código basadas en árboles PSI. Ver el ejemplo de abajo con el mismo fragmento de código, donde podemos ver que los dos lenguajes tienen diferentes tokens específicos del lenguaje en los árboles:

Representación del árbol PSI de un método en Java y Kotlin

Usando tanto UAST como PSI Tree

Necesito tanto UAST como PSI Tree para crear mi regla de formato, porque UAST es genial para filtrar y visitar los nodos que me interesan para ambos idiomas a la vez, pero como he mencionado no proporciona información sobre el formato, por ejemplo información sobre los espacios en blanco que es esencial para mí. UAST se centra más bien en un nivel de abstracción más alto, por lo que principalmente no es para crear reglas de formato.

Desde el plugin de Gradle 3.4 y la versión relacionada de Android Lint 26.4.0, podemos obtener la representación de árbol PSI de cada nodo y sus alrededores, no sólo en Java, sino también en Kotlin. Esto hace posible el uso de Android Lint para mi regla de formato.

Cómo se implementa la regla

Primero, necesito crear mi issue, donde establezco el ámbito a JAVA_FILE, que significa tanto Java como Kotlin en Android Lint actualmente. (Aquí es donde podemos establecer otro tipo de archivos también como XML o archivos Gradle, porque Android Lint puede comprobarlos también.)

Inicialización de una clase Issue personalizada

Después, en mi clase detector, enumero los nodos que me interesan dentro de la función getApplicableUastTypes, para que Android Lint sepa que debe llamar a los métodos de visita anulados relacionados, como visitForEachExpression. En cada método de visita, simplemente llamo a mi método checker.

Cómo decirle a Android Lint qué nodos queremos comprobar

En mi método checker, el parámetro forward describe si quiero comprobar el salto de línea antes o después de la «declaración de bloque». En el cuerpo del método, investigo el número de saltos de línea de los espacios en blanco alrededor de la declaración de bloque que estoy visitando. Para ello, hago tres pasos principales:

  • Primero, mediante el método firstBlockLevelNode, compruebo cuál es el primer nodo de nivel de bloque, alrededor del cual quiero comprobar los espacios en blanco, porque si uso un bloque if para asignar un valor a una variable así,
«Declaración de bloque» en la asignación de valores

Lint investigaría los espacios en blanco justo antes de la palabra clave if, pero a mí me interesa el espacio en blanco antes de la palabra clave val. Así que, en este caso, la primera sentencia a nivel de bloque es la asignación de valor que envuelve mi sentencia if.

  • En segundo lugar, en el método firstRelevantWhiteSpaceNode, compruebo cuál es el primer nodo de espacio en blanco relevante, donde deberíamos contar los saltos de línea. A veces no hay ningún espacio en blanco relevante que comprobar, porque si mi bloque está al principio o al final de un método o de cualquier otro bloque, entonces está bien y puedo prescindir de más investigación. Ver:
Declaración de un solo bloque en un método

En este punto ya estoy utilizando los nodos PSI, porque quiero comprobar la información de los espacios en blanco que no se proporciona en UAST. Para ello, necesito obtener la propiedad sourcePsi del nodo UAST de nivel de bloque.

Tener un comentario antes de una «declaración de bloque»

Un caso límite es si hay un comentario justo encima de mi declaración de bloque. Aquí, quiero comprobar el espacio en blanco por encima del comentario, por lo que el primer espacio en blanco relevante está por encima de la declaración de comentario.

  • Por último, cuento el número de saltos de línea en el espacio en blanco relevante; Si no es mayor que 1, informo de un problema.

El resultado

Como resultado, espero las siguientes advertencias en el IDE, por lo que estará claro para cada desarrollador que hay que añadir una línea adicional. También puedo obtener estas advertencias en el informe de Lint si ejecuto la tarea lint gradle. Además, puedo incluso elevar la gravedad de la incidencia a error, si quiero bloquear el trabajo de solicitud de fusión.

Advertencia deIDE antes y después de la declaración de bloque

Conclusión

Después de integrar la regla personalizada, ya no tenemos que centrarnos en este frustrante problema; en su lugar, podemos gastar nuestro poder cerebral en encontrar problemas más complejos cuando revisamos el código de los demás. Incluso mejor, podemos garantizar al 100% que este error no se introducirá en nuestra base de código accidentalmente, porque podemos configurar nuestro trabajo de MR para que falle cuando alguien se olvide de arreglar este problema.

Aunque Android Lint no es principalmente para formatear reglas, en mi caso fue bastante práctico, porque se puede utilizar tanto para Java como para Kotlin: no es necesario escribir, mantener e integrar dos reglas.

Por otro lado, hay que tener en cuenta que esta regla es muy sencilla en el sentido de que sólo tengo que comprobar los espacios en blanco y las llaves rizadas a nivel de PSI, que son los mismos en los dos lenguajes. Por eso no tengo que escribir ningún código específico del idioma. Sin embargo, si tuviera que escribir algún código específico del lenguaje todavía (por ejemplo, manejar el operador Elvis en Kotlin o el operador Ternario en Java), todavía consideraría preferir Android Lint en lugar de escribir un ktlint y unas reglas Checkstyle, porque todavía tendría mucho menos trabajo probablemente.

Siguiente parte…

Si te ha gustado la primera parte del artículo, por favor, revisa también la segunda parte, donde detallaré la integración de mi regla en proyectos Android (y no Android), cómo solucionar los problemas ya existentes en nuestra base de código, y cómo podemos hacer pruebas unitarias y depurar nuestras reglas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *