Configuración de Tareas Personalizadas de Lint en Gradle con Kotlin DSL

Configuración de Tareas Personalizadas de Lint en Gradle con Kotlin DSL

Víctor Manuel Cañada, Mobile Development - Expert

Víctor Manuel Cañada

Mobile Development - Expert

7 de octubre de 2024

¡Aquí estamos con un ejemplo guay de cómo puedes crear reglas Lint para controlar la visibilidad entre capas en un proyecto basado en Clean Architecture! En esta estructura, cada capa tiene su propio rol: domain, data y presentation no deberían estar chismoseando entre ellas más de lo necesario.

¿Por qué necesitamos estas reglas? Porque a veces en proyectos grandes, la tentación de mezclar dependencias entre capas puede ser real. Y si permitimos que el paquete domain empiece a depender de data o presentation, estamos rompiendo las reglas sagradas de la Clean Architecture. Eso sería como si el cerebro (domain) estuviera directamente conectado a los músculos (presentation) o a las venas (data) sin pasar por los nervios adecuados. ¡Una locura!

Con estas reglas Lint, básicamente le decimos a nuestro código: "¡Eh, domain! ¡Nada de mirar a data ni presentation!". Es un poco como ser el guardián de las buenas prácticas, controlando quién puede hablar con quién.

¿Y si usamos módulos?

Ahora, ojo al dato: si en vez de dividir nuestro código en paquetes dentro de un solo módulo, usamos módulos separados para cada capa (domain, data, presentation), ¡no necesitaríamos todas estas reglas custom! Los módulos ya tienen su propia magia para manejar las dependencias. Pueden controlar solitos quién puede acceder a qué, y te quitas de encima este lío de controlar la visibilidad manualmente. Pero si estás usando paquetes dentro de un mismo módulo, estas reglas son tu mejor amigo para que el código se mantenga limpio y organizado.

Así que, en resumen, estas reglas Lint son como ponerle límites a los paquetes dentro de un mismo módulo, asegurándote de que domain, data y presentation se mantengan en su sitio, cumpliendo con su función y sin meterse donde no deben.

Pues una vez dicho esto… ¡¡¡Leña al mono que es de goma!!! 

Empezaremos configurando Lint en un proyecto Android usando Gradle con Kotlin DSL (.gradle.kts), y lo haremos paso a paso, con ejemplos y reglas personalizadas incluidas. ¡Vamos al grano! 💪

🎯 Paso 1: Configura la tarea de Lint en build.gradle.kts

Primero, nos metemos de lleno en el archivo build.gradle.kts, donde definiremos la tarea de Lint y personalizaremos sus reportes, porque a nadie le gusta quedarse con el reporte por defecto, ¿verdad?

plugins {
    id("java-library")
    id("com.android.lint")
    alias(libs.plugins.jetbrains.kotlin.jvm)
}

lint {
    htmlReport = true
    htmlOutput = file("lint-report.html") // Archivo de reporte HTML
    textReport = true // También generará un reporte en texto
    checkReleaseBuilds = true // Verifica las compilaciones de release
    absolutePaths = false // Usa rutas relativas en los reportes
    ignoreTestSources = false // Incluye los test en los reportes
    abortOnError = true // Detiene el proceso si hay errores
}

java {
    sourceCompatibility = JavaVersion.VERSION_22
    targetCompatibility = JavaVersion.VERSION_22
}

dependencies {
    compileOnly(libs.lint.api)
    compileOnly(libs.lint.cli)
    compileOnly(libs.lint.checks)
    compileOnly(libs.lint.tests)
}

tasks.jar {
    manifest {
        attributes(
            "Lint-Registry-v2" to "com.example.custom_lint.CleanArchitectureIssueRegistry",
            "Lint-Vendor" to "The Roustic Chicken" // Reemplaza con el nombre de tu empresa
        )
    }
}

🎉 ¡Y listo! Ya tienes configurado el archivo build.gradle.kts. Ahora tu tarea de Lint tiene todo lo necesario para generar reportes en HTML y texto, verificar compilaciones de release y más.

🔍 Analizando en Profundidad el build.gradle.kts

1. Personaliza Lint a tu gusto

Aquí es donde configuramos cómo queremos que funcione Lint. Vamos a controlar los tipos de reporte, si verificamos compilaciones de release, cómo se manejan los errores y más.

lint {
    htmlReport = true
    htmlOutput = file("lint-report.html") // Archivo de reporte HTML
    textReport = true // También generará un reporte en texto
    checkReleaseBuilds = true // Verifica las compilaciones de release
    absolutePaths = false // Usa rutas relativas en los reportes
    ignoreTestSources = true // Tests en los reportes
    abortOnError = true // Detiene el proceso si hay errores
}
  • htmlReport = true: queremos un informe visualmente amigable en formato HTML. El informe se verá bonito, con colores y todo.

  • htmlOutput = file("lint-report.html"): aquí definimos dónde queremos que aparezca ese reporte HTML. En este caso, se guardará en el archivo lint-report.html.

  • textReport = true: por si acaso, también generamos un reporte en texto plano, porque nunca está de más tener algo más sencillo.

  • checkReleaseBuilds = true: queremos que Lint sea más estricto con nuestras compilaciones de release. No queremos que un bug se cuele en la versión final.

  • absolutePaths = false: para evitar que nuestros reportes muestren las rutas completas de los archivos, mejor usamos rutas relativas. Así no compartimos más información de la que necesitamos.

  • ignoreTestSources = true: ¡los tests importan! Pero esta vez, no le aplican esta regla.

  • abortOnError = true: ¿hay errores graves? Pues que Lint lo pare todo. No avanzamos hasta resolverlos.

1.3. Añadir dependencias de Lint

¡Momento de las dependencias! Aquí es donde indicamos qué librerías de Lint necesitamos para nuestras reglas personalizadas.

dependencies {  
    compileOnly(libs.lint.api)  
    compileOnly(libs.lint.cli)  
    compileOnly(libs.lint.checks)  
    compileOnly(libs.lint.tests)  
}
  • libs.lint.api: esta dependencia nos permite escribir nuestras propias reglas de Lint.

  • libs.lint.cli: nos da acceso a la interfaz de línea de comandos de Lint para ejecutar nuestras reglas.

  • libs.lint.checks: incluye reglas estándar de Lint para verificar nuestro código.

  • libs.lint.tests: proporciona herramientas útiles para probar nuestras reglas personalizadas.

1.4. Personaliza el JAR y el manifiesto

Finalmente, configuramos la tarea jar para añadir un par de detalles en el manifiesto, como el registro de las reglas personalizadas que vamos a definir.

tasks.jar {  
    manifest {  
        attributes(  
            "Lint-Registry-v2" to "com.example.custom\_lint.CleanArchitectureIssueRegistry",  
            "Lint-Vendor" to "The Roustic Chicken" // Reemplaza con el nombre de tu empresa  
        )  
    }  
}
  • Lint-Registry-v2: aquí le indicamos a Lint dónde encontrar las reglas personalizadas, en este caso en CleanArchitectureIssueRegistry.

  • Lint-Vendor: personaliza el nombre del proveedor de las reglas, que en este ejemplo hemos llamado "The Roustic Chicken" o como me gusta a mí “El Pollo Campero”. Cambia esto por el nombre de tu empresa o proyecto.

Y con esto ya tienes configurada la tarea de Lint en build.gradle.kts. Listo para cazar cualquier error arquitectónico o violación de dependencias que se te escape por ahí. ¡A por un código más limpio!

🛠️ Paso 2: Implementar Reglas Personalizadas de Lint

¡Ahora viene la magia! ✨ Vamos a escribir algunas reglas de Lint personalizadas. Aquí, te mostraré cómo asegurarnos de que nuestro querido paquete domain no tenga dependencias no deseadas de data o presentation.

Regla:DomainDependencyDetector

Esta pequeña regla supervisará que el paquete domain se mantenga puro y no dependa de otros paquetes de data o presentation, porque la arquitectura limpia manda. 🤘

package com.example.custom_lint

import com.android.tools.lint.detector.api.*
import org.jetbrains.uast.UClass
import org.jetbrains.uast.getContainingUFile

class DomainDependencyDetector : Detector(), SourceCodeScanner {

    override fun applicableSuperClasses(): List<String>? =
        listOf("java.lang.Object")

    override fun visitClass(context: JavaContext, declaration: UClass) {
        val uFile = declaration.getContainingUFile()
        val packageName = uFile?.packageName ?: return

        val regexDomain = Regex("""^com\.example(?:\.home)?\.domain(?:\.\w+)*$""")

        if (regexDomain.matches(packageName)) {
            val imports = context.uastFile?.imports ?: return

            for (importStatement in imports) {
                val import = importStatement.asSourceString()
                val regexData = Regex("""^import\s+com\.example(?:\.\w+)?\.data(?:\.\w+)*""")
                val regexPresentation = Regex("""^import\s+com\.example(?:\.\w+)?\.presentation(?:\.\w+)*""")

                if (regexData.matches(import) || regexPresentation.matches(import)) {
                    context.report(
                        ISSUE, declaration,
                        context.getLocation(importStatement),
                        "El paquete domain no debe depender de data o presentation."
                    )
                }
            }
        }
    }

    companion object {
        val ISSUE: Issue = Issue.create(
            id = "ImportDependencyViolation",
            briefDescription = "Import Dependency Violation",
            explanation = "Ensures that imports comply with architectural rules.",
            category = Category.CUSTOM_LINT_CHECKS,
            priority = 6,
            severity = Severity.ERROR,
            implementation = Implementation(
                DomainDependencyDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}

🧩 ¿Qué pasa aquí?

1- Verificación del Paquete: la regla verifica que el paquete actual sea parte del dominio.

2- Chequeo de Importaciones: si se detecta una importación de data o presentation en el paquete domain, salta la alarma 🚨.

3- Reporte del Error: si encuentra algo sospechoso, se reporta el problema con un mensaje claro: "El paquete domain no debe depender de data o presentation".

Y voilà, has implementado tu primera regla personalizada de Lint. ¿A que no fue tan difícil? 😊

🔍 Analizando en Profundidad el DomainDependencyDetector

La regla DomainDependencyDetector tiene como misión asegurar que el paquete domain no dependa de otros paquetes, específicamente data o presentation, respetando así los principios de la arquitectura limpia. Veamos cómo lo hace y cómo utilizamos una expresión regular para identificar los paquetes problemáticos.

Estructura General de DomainDependencyDetector

El detector hereda de Detector e implementa la interfaz SourceCodeScanner. Esto le permite escanear clases fuente en busca de patrones o violaciones de reglas. La clave de este detector es revisar las importaciones de los archivos dentro del paquete domain y verificar si alguna depende de data o presentation.

Cómo Funciona Paso a Paso:

1. Aplicable Superclases:


override fun applicableSuperClasses(): List<String>? = listOf("java.lang.Object")

Aquí indicamos que esta regla se aplica a clases que extienden** java.lang.Object**, que es prácticamente cualquier clase en Java. De esta manera, nos aseguramos de que se analicen todas las clases en el proyecto.

2. Obtención del Paquete y Expresión Regular (Regex):

val uFile = declaration.getContainingUFile()
val packageName = uFile?.packageName ?: return

val regexDomain = Regex("""^com\.example(?:\.home)?\.domain(?:\.\w+)*$""")

Primero, obtenemos el nombre del paquete de la clase actual usando getContainingUFile(). Esto nos da el archivo fuente (UFile), del cual luego extraemos el nombre del paquete.

Luego, definimos la expresión regular regexDomain, que se encargará de verificar si el archivo pertenece al paquete domain. Vamos a desglosar la expresión regular:

  • ^com\.example: busca un paquete que comience con com.example.

  • (?:\.home)?: este es un grupo no capturante que indica que .home es opcional. Esto es útil si tienes varias variantes del paquete domain, por ejemplo, com.example.domain o com.example.home.domain.

  • \.domain: verificamos que el paquete contenga .domain.

  • (?:\.\w+)*: esta parte permite que haya subpaquetes dentro de domain, como com.example.domain.usecase, al permitir cualquier número de subdirectorios a través de \w+ (que representa una secuencia de letras, números o guiones bajos).

Esta expresión garantiza que solo se apliquen las reglas si el archivo está dentro del paquete domain.

3. Verificación de Importaciones:

val imports = context.uastFile?.imports ?: return
for (importStatement in imports) {
val import = importStatement.asSourceString()
val regexData = Regex("""^import\s+com\.example(?:\.\w+)?\.data(?:\.\w+)*""")
val regexPresentation = Regex("""^import\s+com\.example(?:\.\w+)?
\.presentation(?:\.\w+)*""")

Ahora que ya sabemos que estamos en el paquete domain, es hora de revisar todas las importaciones de este archivo. Aquí definimos dos nuevas expresiones regulares: regexData y regexPresentation, que verifican si se está importando algo de los paquetes data o presentation.

  • regexData: Busca importaciones del paquete data. La expresión com.example(?:.\w+)?.data permite que la importación provenga de cualquier variante del paquete data, como com.example.data o com.example.submodule.data.

  • regexPresentation: Similar al anterior, pero busca cualquier importación que venga del paquete presentation.

if (regexData.matches(import) || regexPresentation.matches(import)) {
    context.report(
        ISSUE, declaration,
        context.getLocation(importStatement),
        "El paquete domain no debe depender de data o presentation."
    )
}

Si alguna importación coincide con regexData o regexPresentation, reportamos el error utilizando context.report. Aquí se especifica el problema, la ubicación del mismo y un mensaje claro que indica que el paquete domain no debe depender de data o presentation.

En resumen, esta regla asegura que el paquete domain permanezca independiente y "puro", siguiendo el principio de la arquitectura limpia que busca una separación clara de responsabilidades entre las capas de la aplicación.

🛠️ Resto de las Reglas

Si eres un perfeccionista (como todos los buenos desarrolladores 😉), probablemente querrás crear más reglas para otros paquetes. No te preocupes, también te cubro aquí:

Regla:DataDependencyDetector

package com.example.custom_lint

class DataDependencyDetector : Detector(), SourceCodeScanner {

    override fun visitClass(context: JavaContext, declaration: UClass) {
        val packageName = declaration.getContainingUFile()?.packageName ?: return
        val regexData = Regex("""^com\.example(?:\.home)?\.data(?:\.\w+)*$""")

        if (regexData.matches(packageName)) {
            val imports = context.uastFile?.imports ?: return

            for (importStatement in imports) {
                val regexPresentation = Regex("""^import\s+com\.example(?:\.\w+)?\.presentation(?:\.\w+)*""")
                if (regexPresentation.matches(importStatement.asSourceString())) {
                    context.report(
                        ISSUE, declaration,
                        context.getLocation(importStatement),
                        "El paquete data no debe depender de presentation."
                    )
                }
            }
        }
    }

    companion object {
        val ISSUE: Issue = Issue.create(
            id = "DataDependency",
            briefDescription = "Dependencia prohibida en Data",
            explanation = "El paquete data no debe depender de presentation.",
            category = Category.CORRECTNESS,
            priority = 6,
            severity = Severity.ERROR,
            implementation = Implementation(
                DataDependencyDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}

Regla: PresentationDependencyDetector

package com.example.custom_lint

class PresentationDependencyDetector : Detector(), SourceCodeScanner {

    override fun visitClass(context: JavaContext, declaration: UClass) {
        val packageName = declaration.getContainingUFile()?.packageName ?: return
        val regexPresentation = Regex("""^com\.example(?:\.home)?\.presentation(?:\.\w+)*$""")

        if (regexPresentation.matches(packageName)) {
            val imports = context.uastFile?.imports ?: return

            for (importStatement in imports) {
                val regexData = Regex("""^import\s+com\.example(?:\.\w+)?\.data(?:\.\w+)*""")
                if (regexData.matches(importStatement.asSourceString())) {
                    context.report(
                        ISSUE, declaration,
                        context.getLocation(importStatement),
                        "El paquete presentation no debe depender de data."
                    )
                }
            }
        }
    }

    companion object {
        val ISSUE: Issue = Issue.create(
            id = "PresentationDependency",
            briefDescription = "Dependencia prohibida en Presentation",
            explanation = "El paquete presentation no debe depender de data.",
            category = Category.CORRECTNESS,
            priority = 6,
            severity = Severity.ERROR,
            implementation = Implementation(
                PresentationDependencyDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}

🧩Paso 3: Registro de Reglas en IssueRegistry

Finalmente, es hora de registrar todas estas maravillosas reglas en un solo IssueRegistry.

package com.example.custom_lint

class CleanArchitectureIssueRegistry : IssueRegistry() {
    override val issues = listOf(
        DomainDependencyDetector.ISSUE,
        DataDependencyDetector.ISSUE,
        PresentationDependencyDetector.ISSUE
    )
    override val api = CURRENT_API
    override val vendor: Vendor = Vendor("The Country Chicken")
}

🔗Profundizando en el Registro de Reglas en IssueRegistry

Para que nuestras reglas personalizadas sean utilizadas por Lint, necesitamos registrarlas. Aquí es donde entra en juego la clase CleanArchitectureIssueRegistry, que actúa como el "catálogo" de nuestras reglas personalizadas. Vamos a ver cómo funciona y por qué es importante.

Elementos Clave:

1. La Lista de Issues:

override val issues = listOf(
    DomainDependencyDetector.ISSUE,
    DataDependencyDetector.ISSUE,
    PresentationDependencyDetector.ISSUE
)

La propiedad issues es una lista que contiene todas las reglas de Lint que queremos registrar. Cada regla tiene un Issue estático, como DomainDependencyDetector.ISSUE, que es lo que realmente define el problema que estamos buscando (y cómo se reportará).

Aquí registramos tres detectores:

  • DomainDependencyDetector: Asegura que el paquete domain no dependa de data o presentation.
  • DataDependencyDetector: Similar al anterior, pero verifica que el paquete data no dependa de presentation.
  • PresentationDependencyDetector: Asegura que presentation no dependa de data.

Al incluir estos tres detectores en la lista de issues, Lint sabrá que debe ejecutar estas reglas cuando analice el código.

2. API de Lint:


override val api = CURRENT_API

Este valor le dice a Lint qué versión de la API de Lint estamos utilizando. El valor CURRENT_API asegura que estamos utilizando la versión más reciente compatible con el sistema.

3. Información del Vendor:


override val vendor: Vendor = Vendor("The Country Chicken")

El campo vendor permite definir un nombre que identifique quién ha creado estas reglas personalizadas. Esto es útil si varias personas o equipos están creando reglas personalizadas dentro de la misma organización.

🤔 ¿Por Qué Necesitamos el IssueRegistry?

El registro es el puente entre Lint y nuestras reglas personalizadas. Sin él, Lint no sabría qué reglas aplicar. Además, permite que Lint las integre en el flujo de análisis estándar de cualquier proyecto que utilice nuestra biblioteca de reglas.

Cuando Lint se ejecuta, busca clases que extiendan IssueRegistry para obtener la lista de todas las reglas que deben ser ejecutadas. Registrar tus reglas en IssueRegistry es el paso crucial para que entren en acción durante el análisis estático de código.

¿Qué Sucede Cuando Lint se Ejecuta?

Cuando Lint se ejecuta (ya sea manualmente o como parte de un proceso de build), pasa por las siguientes etapas:

1- Carga del Registro: Busca todas las clases que extienden IssueRegistry.

2- Registro de Reglas: Carga las reglas listadas en la propiedadissues.

3- Ejecución del Análisis: Analiza el código fuente usando las reglas registradas.

4- Reporte de Problemas: Si alguna regla detecta una violación, Lint genera un reporte (en HTML, texto, etc.), resaltando los problemas encontrados.

Gracias al CleanArchitectureIssueRegistry, nuestras reglas se ejecutan automáticamente cada vez que se corre Lint, asegurando que el código respete la arquitectura definida.

Antes de despedirnos... ¡Ya solo falta un paso!

Tenemos que incluir nuestras reglas Lint personalizadas en el build.gradle de todos los módulos donde queremos aplicarlas. Para ello, simplemente agregamos la siguiente línea en la sección de dependencies del módulo correspondiente:

dependencies {
    lintChecks(project(":custom_lint"))
}

Y ahora, a ponerlo a prueba! Abre el terminal de Android Studio y escribe:

./gradlew lint

Este comando ejecutará Lint y analizará todos los archivos. Si hay alguna violación de nuestras reglas, aparecerán los errores en el terminal, y se generarán informes en formato .txt y .html con todos los detalles.

Para que lo veas en acción, te dejamos un proyecto de prueba en GitHub donde puedes experimentar con las reglas que hemos creado. Si quieres forzar un error y comprobar cómo funciona, solo tienes que ir al MainViewModel y, dentro del método init, crear una instancia de UserDto. Dado que UserDto pertenece a la capa de datos y MainViewModel a la capa de presentación, nuestras reglas de arquitectura lanzarán un error inmediatamente. ¡Ideal para que veas cómo Lint te avisa de este tipo de violaciones!

IMAGEN11.webp Forzamos el error creando un obj de la capa data en la de presentation.

IMAGEN21.webp Lanzamos la tarea lint, y se nos muestra el error.

IMAGEN31.webp Se nos muestra las rutas donde están los informes.

IMAGEN41.webp Se nos muestra las rutas donde están los informes.

IMAGEN51.webp Informe del error en texto plano.

IMAGEN61.webp Informe del error en formato HTML.

Mejoras

😅 Seamos honestos, lanzar el comando ./gradlew lint cada vez que quieras comprobar si tu código pasa las reglas puede llegar a ser un poco… incómodo. Estás en pleno desarrollo, ajustando detalles, y tener que volver al terminal una y otra vez no es lo más ágil del mundo.

¡Pero tranquilidad! Hay formas de automatizar esto y hacer que Lint trabaje sin que tengas que mover un dedo. Por ejemplo:

  • Ejecutar Lint cada vez que haces un commit: Así te aseguras de que el código cumple con las reglas antes de subir cualquier cambio.
  • Ejecutar Lint cada vez que compilas o ejecutas el proyecto: Creando un par de tasks puedes integrar Lint directamente en tu flujo de trabajo diario.

Pero bueno… esa es otra historia que veremos con más detalle en un próximo artículo. ¡Quédate pendiente porque te va a hacer la vida mucho más fácil! 😉

¡Y eso es todo, amigos! Esperamos que este recorrido por el mundo de las reglas Lint y la Clean Architecture te haya resultado tan interesante como útil (y un poco divertido, ¿no?). Ya sabes, ahora tus capas están más protegidas que nunca, y tu código te lo va a agradecer con menos errores y más claridad.

¡Nos vemos en el próximo artículo, donde seguiremos desentrañando los misterios del desarrollo! ¡Hasta entonces….

Qué la fuerza del Pollo Campero, te acompañe 🐔!

Víctor Manuel Cañada, Mobile Development - Expert

Víctor Manuel Cañada

Mobile Development - Expert

Descubrí mi pasión por la programación a los 8 años con un MSX de 8 bits que usaba Basic. El manual del ordenador me sirvió para aprender lógica de programación, marcando mi vida desde entonces. Me motiva aprender y compartir conocimientos. Disfruto de series y películas de fantasía épica y thrillers psicológicos. También me apasiona recorrer rutas en moto, buscando paisajes remotos y carreteras con curvas. Mi truco de programación: Si algo va mal, usa "Pollo Campero" como clave en tus logs de depuración.


Nuestras últimas novedades

¿Te interesa saber cómo nos adaptamos constantemente a la nueva frontera digital?

Contract Testing con Pact - La guía definitiva
Contract Testing con Pact - La guía definitiva

Tech Insight

19 de diciembre de 2024

Contract Testing con Pact - La guía definitiva

Agilidad, complejidad y método empírico
Agilidad, complejidad y método empírico

Insight

18 de diciembre de 2024

Agilidad, complejidad y método empírico

Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025
Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025

Insight

10 de diciembre de 2024

Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025

Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo
Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo

Insight

3 de diciembre de 2024

Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo