¿Alguna vez se ha necesitado crear una multitud de instancias de una clase de datos con parámetros ligeramente diferentes? Quizás para probar, generar datos de muestra o poblar una interfaz de usuario con varios estados. Crearlos manualmente puede ser tedioso y propenso a errores. ¿Qué pasaría si pudiéramos automatizar esto?
Esta fue la chispa que me llevó a explorar el procesamiento de símbolos de Kotlin (KSP) y Kotlinpoet para crear Kombinator, un procesador de anotación que genera automáticamente todas las combinaciones posibles de los parámetros de constructor de una clase de datos.
En este artículo, lo guiaré a través del proceso, las decisiones de diseño y algunos de los desafíos interesantes que se enfrentan al construir Kombinator.
El objetivo: ¿Qué es Kombinator?
En esencia, Kombinator tiene como objetivo simplificar la creación de múltiples instancias de una clase de datos de Kotlin. Anota una clase de datos (o sus parámetros) con Kombina y proporcionar los valores posibles para cada parámetro. Kombinator luego genera un objeto que contiene propiedades para cada combinación única de estos parámetros, más una práctica getAllCombinations () Funciona para recuperarlos a todos como una lista.
Por ejemplo, imagina un Usuarios Clase de datos:
Con Kombinator, podrías anotarlo así:
Kombinator generaría un objeto, digamos Usuarios de la comBinationsluciendo algo como esto (simplificado):
- Procesamiento de símbolos de Kotlin (KSP): KSP es una API desarrollada por Google que le permite escribir complementos de compiladores livianos para Kotlin. Procesa el código Kotlin en el momento de la compilación, proporcionando acceso a la estructura de su código (como clases, funciones, parámetros y anotaciones) sin necesidad de profundizar en las complejidades de los compiladores de Kotlin. Esto lo hace más eficiente y estable que el Kapt más antiguo (herramienta de procesamiento de anotación de Kotlin).
- Kotlinpoet: Desde Sq., Kotlinpoet es una biblioteca fantástica para generar archivos de origen .KT. Proporciona una API fluida para construir el código Kotlin mediante programación, manejando las importaciones, el formato y otros detalles maravillosamente.
El viaje: construcción de kombinator
Desglosemos cómo funciona Kombinator y los componentes clave involucrados.
- La anotación de kombina
Este es el punto de entrada. Es una anotación de retención de origen, lo que significa que solo está presente durante la compilación y no lo convierte en el código de bytecodo remaining. Se dirige a clases y parámetros de valor.
Una opción de diseño essential aquí period tener parámetros de matriz separados para cada tipo de datos compatibles (AllPossiblestringParams, todopossibleintparamsand so forth.). Esto lo hace tipo seguro en el sitio de uso de anotaciones y simplifica el análisis dentro del procesador. Inicialmente, consideré un enfoque más genérico, pero esta explicidad resultó más robusta.
2. El procesador de procesadores y el procesador
KSP funciona descubriendo SymbolsprocessorProvider implementaciones. Nuestro Proveedor de procesadores es sencillo:
Simplemente crea una instancia de nuestro procesador principal. El procesador es donde sucede la magia:
El método de proceso es el punto de entrada para KSP. Él:
- Obtiene el nombre calificado de nuestro Kombina anotación.
- Usa el resolución para encontrar todos los símbolos (clases, parámetros, and so forth.) anotados con Kombina.
- Filtros para válidos Ksclassdeclaration instancias.
- Invoca un Visor de dataclass para cada clase anotada.
3. DatacLassVisitor: Inspeccionar y recopilar información
DatacLassVisitor (implementado como una clase interna para acceder a maderero y codegenerador) usa el patrón de visitante KSP para atravesar el AST de la clase anotada.
Una clase de datos de ayuda, ConstructorParameterinfofue esencial para organizar los detalles extraídos sobre cada parámetro del constructor:
4. Valores de anotación de lectura y construcción de grupos combinables
Aquí es donde la lógica se pone interesante. Necesitamos determinar qué valores combinar para cada parámetro.
- Booleanos: Para parámetros booleanos sin valores predeterminados, asumimos automáticamente [false, true] como los valores combinables.
- Enumeros: Para los parámetros enum sin valores predeterminados, extraemos todas las entradas de ENUM.
- Otros tipos (de Kombine): Esto es manejado por el readparámetro función. Un principio de diseño clave aquí es que si un Kombina La anotación está presente directamente en un parámetro de constructor, sus valores especificados siempre tendrán prioridad sobre cualquier valor definido en un nivel de clase Kombina Anotación para ese mismo tipo de parámetro. Esto permite un management de grano fino.
- Manejo de valores predeterminados: Es importante tener en cuenta que Kombinator está diseñado para generar combinaciones basadas en valores o opciones inherentes proporcionadas explícitamente (como booleanos y enumines). Si un parámetro tiene un valor predeterminado en el constructor de la clase de datos y no está explícitamente dirigido por un Kombina Anotación (ya sea a nivel de parámetro o clase con valores para su tipo), Kombinator no lo incluirá automáticamente en la generación de combinación. La expectativa es que outline explícitamente el rango de valores que desea combinar para los parámetros que le interesa variar. Si un parámetro con un valor predeterminado está dirigido a Kombinaentonces se utilizarán los valores de la anotación, anulando el valor predeterminado para las combinaciones generadas.
El readparámetro función (de Readparameter.kt) es essential. Se itera a través de parámetros de constructor que no son booleanos, enumeros y de manera essential, solo considera aquellos que no tienen valores predeterminados a menos que sean anotados explícitamente con Kombina para proporcionar valores. Esto se debe a que el objetivo es combinar en función de los conjuntos de posibilidades proporcionados, no solo usar un único valor predeterminado.
Y el readAnnotationArrayargument La utilidad ayuda a extraer valores del Kombina Argumentos de la matriz de anotación:
Desafío – Tipos sin firmar: Un desafío notable surgió con los tipos sin firmar (Ubyte, Ushort, and so forth.). Cuando KSP lee los valores de una anotación como Val AllPossibleCyteParams: Ubytearray = [1u, 2u]el argumento. Valor para AllPossibleCyteParams sorprendentemente produce una lista
5. Generando el código con Kotlinpoet
Una vez que tenemos el Teams CombinableParameter (una lista de pares, donde cada par está ConstructorParameterinfo y es Lista
WriteProperties: Esta función itera a través de todas las combinaciones posibles y crea un kotlinpoet PropertySpec para cada uno.
GenerateinstanceProperty: Esto crea la propiedad actual actual para una instancia.
Desafío – literales de kotlinpoet: Kotlinpoet es poderoso, pero debe usar los especificadores de formato correctos para literales. %S para cadenas (agrega citas), %l para literales generales, pero los flotadores necesitan sufijo F (por lo tanto %LF), los chars necesitan cotizaciones individuales (‘ %l’), y los tipos sin signo necesitan sufijo AU (logrado con %lu después de lanzar el valor KSP a su tipo de firma subyacente y luego a largo plazo para KotlinPoet’s %Lu a Lu a Work como se esperaba para el trabajo de todos los insignificados. Esto requirió una construcción cuidadosa de Bloques de código.
código de generación: Finalmente, esta función ensambla el objeto generado, agrega el getAllCombinations () función y escribe el archivo.
Gestión de dependencias con KSP: El objeto de dependencias es importante. Dependencias (agregando = falso, archivo) le cube a KSP que el archivo generado depende del archivo de origen (archivo) que contiene la clase anotada. Si el archivo fuente cambia, KSP sabe que necesita reprocesar y potencialmente regenerar esta salida. agregando = falso significa este procesador no agregue información de múltiples archivos fuente para producir una salida única.
6. Manejo de registro y error
A lo largo del proceso, utilizando ksplogger (logger.error (), logger.warn (), logger.information ()) es essential para proporcionar retroalimentación al usuario sobre lo que está haciendo el procesador, cualquier configuración errónea o error encontrado. Los mensajes de error de Borrar son clave para una buena experiencia de desarrollador.
Desafíos y aprendizajes
- Comprensión de los tipos de KSP versus tipos de kotlinpoet: KSP proporciona sus propias representaciones de tipos (Kstype, Ksdeclaration). Estos a menudo deben convertirse en el nombre de tipo de Kotlinpoet utilizando extensiones como Totypename () de com.squareup.kotlinpoet.ksp.totypename.
- Argumentos de anotación de lectura: Acceso a los argumentos de anotación (anotación. Argumentos) y sus valores (argumento. Valor) es sencillo, pero el tipo de argumento. El valor a veces puede ser un poco sorprendente (por ejemplo, lista
Para las matrices, el problema de los tipos sin firmar mencionados anteriormente). Se necesitan un lanzamiento robusto y la verificación de tipo. - Lógica de combinación iterativa: El algoritmo en WriteProperties Para iterar a través de todas las combinaciones (los índices de incremento, como contar en diferentes bases), es un problema combinatorio clásico.
- Formato de kotlinpoet: Masterización Bloques de código y los especificadores de formato ( %n para nombres, %t para tipos, %l para literales, %s para cadenas) es clave para generar código Kotlin limpio y correcto. El sangría (⇥) y los caracteres Unindent (⇤) son muy útiles para la legibilidad.
Conclusión
Construir Kombinator fue una inmersión gratificante en el mundo de KSP y Kotlinpoet. Se mostró cómo estas herramientas se pueden usar para reducir significativamente la caldera y automatizar tareas de codificación repetitiva. Si bien ciertamente hubo obstáculos, particularmente en torno al manejo de tipo y los matices de la API de KSP, el resultado remaining es una utilidad que realmente puede ahorrar tiempo y esfuerzo.
Si está buscando automatizar la generación de código en sus proyectos de Kotlin, le recomiendo explorar KSP. La curva de aprendizaje es manejable y el poder que ofrece es sustancial.
¡Feliz Koding (y Kombining)!
GitHub: