Generación de código con build_runner y source_gen

En Dart, la metaprogramación no se realiza mediante reflexión en tiempo de ejecución (como dart:mirrors), debido a que la reflexión es incompatible con la compilación AOT (Ahead-of-Time) y el tree-shaking. Si el compilador tuviera que permitir la inspección dinámica de nombres y tipos en runtime, no podría eliminar el código muerto, ya que no sabría qué clases o métodos podrían ser llamados dinámicamente, lo que resultaría en binarios excesivamente pesados e ineficientes.

Para resolver esto, el ecosistema utiliza la generación de código en tiempo de compilación. El proceso se apoya en tres pilares: build_runner actúa como el orquestador que gestiona el ciclo de vida y el caché; el paquete build gestiona el grafo de dependencias de los archivos (el asset graph); y source_gen es la abstracción de alto nivel que nos permite implementar generadores de forma sencilla.

Cuando necesitas crear lógica repetitiva (como serializadores JSON, inyectores de dependencias o mocks), implementas un GeneratorForAnnotation<T>. Este componente analiza los Elements (la representación abstracta de tu código fuente) que contienen la anotación T y devuelve un fragmento de código en forma de cadena. La configuración se define en un archivo build.yaml, donde especificas qué archivos debe procesar tu Builder para evitar ciclos infinitos o un tiempo de ejecución excesivo. Si el proceso falla por una mala configuración de las rutas o si olvidas la directiva part en tu archivo original, el compilador simplemente no reconocerá el código generado, lanzando errores de “símbolo no encontrado” a pesar de que el archivo .g.dart exista en tu disco.

// Este código es una simulación técnica de cómo operan los generadores.
// En un proyecto real, el Generador vive en un paquete separado y 
// build_runner lo invoca mediante el grafo de activos.

// 1. La Anotación: El marcador que el generador buscará.
class Mapper {
  const Mapper();
}

// 2. Simulación de la interfaz Element de la librería 'element_reader'.
// En producción, esto es parte del ecosistema 'build'.
abstract class MockElement {
  String get name;
  String get type;
  List<MockElement> get fields;
}

class MockClass implements MockElement {
  @override
  final String name;
  @override
  final String type;
  @override
  final List<MockElement> fields;

  MockClass(this.name, this.type, this.fields);
}

class MockField implements MockElement {
  @override
  final String name;
  @override
  final String type;
  @override
  final List<MockElement> get fields => [];

  MockField(this.name, this.type);
}

// 3. El Generador: Implementación de la lógica de transformación.
// En la vida real, esto implementaría GeneratorForAnnotation<Mapper> de source_gen.
class SimpleMapperGenerator {
  /// Simula la lógica que ejecutaría GeneratorForAnnotation.
  /// Devuelve el código que se escribirá en el archivo .g.dart
  String generate(MockClass element) {
    final buffer = StringBuffer();
    
    buffer.writeln('// Generado por SimpleMapperGenerator. No editar manualmente.');
    buffer.writeln('extension ${element.name}X on ${element.name} {');
    buffer.writeln('  Map<String, dynamic> toMap() {');
    buffer.writeln('    return {');
    
    for (var field in element.fields) {
      // Generamos el acceso al campo asumiendo que es accesible.
      buffer.writeln('      \'${field.name}\': this.${field.name},');
    }
    
    buffer.writeln('    };');
    buffer.writeln('  }');
    buffer.writeln('}');
    
    return buffer.toString();
  }
}

void main() {
  // Simulamos un archivo fuente con la anotación @Mapper
  final userClass = MockClass(
    'User', 
    'User', 
    [
      MockField('id', 'int'),
      MockField('username', 'String'),
      MockField('email', 'String'),
    ],
  );

  final generator = SimpleMapperGenerator();
  
  print('--- Iniciando fase de generación (Simulación de build_runner) ---');
  final generatedCode = generator.generate(userClass);
  
  print('\n[ARCHIVO: user.g.dart]');
  print(generatedCode);
  print('\n--- Fase de generación completada con éxito ---');
}

Análisis del proceso

En el ejemplo anterior, la lógica reside en SimpleMapperGenerator. En un entorno de producción real, este componente implementaría la interfaz GeneratorForAnnotation<Mapper>.

Cuando ejecutas dart run build_runner build, el motor analiza el MockClass (que el compilador de build ha construido a partir de tu código fuente real). El método generate no escribe directamente en el disco; su función es devolver un String que representa el nuevo código. Es build_runner el encargado de tomar ese String, crear el archivo user.g.dart y, lo más importante, añadirlo al grafo de dependencias.

Fíjate en cómo el buffer construye la extensión UserX. El generador recorre los fields del element para construir el mapa. En el mundo real, source_gen te proporciona acceso a los tipos de datos de esos campos para que puedas decidir, por ejemplo, si un campo debe ser tratado como un DateTime o un String, permitiendo una generación de código fuertemente tipada y segura para el AOT.

El uso de la palabra clave extension en el código generado es una decisión de diseño común para no contaminar la clase original y permitir que el código generado sea una “capa” adicional de funcionalidad, manteniendo la separación de responsabilidades entre tu lógica de negocio y la lógica de serialización.

El error frecuente

Un error clásico al trabajar con generadores es la desincronización entre el código fuente y el código generado, resultando en errores de compilación tipo The getter 'toMap' isn't defined for the type 'User'.

Esto ocurre por dos razones principales:

  1. Falta de la directiva part: Aunque build_runner haya creado exitosamente el archivo user.g.dart, si no has escrito part 'user.g.dart'; en la parte superior de tu archivo user.dart, el compilador de Dart tratará ambos archivos como entidades totalmente independientes. El código generado será invisible para la clase User.
  2. Filtros de build.yaml demasiado restrictivos: Si configuras el build.yaml para que tu builder solo se aplique a archivos en lib/models/, pero tu clase está en lib/src/, el builder se saltará el archivo sin avisar, dejando al desarrollador intentando debugear por qué la generación no ocurre.

110

Dejar un comentario

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

Scroll al inicio