Search code examples
flutterdartbuildcode-generationbuild-runner

Why is my flutter code generation using `build_runner` and `source_gen` not creating output files?


I am trying to create a code generation package within Flutter using the build_runner and source_gen packages. I've looked over numerous examples but cannot seem to find what is preventing my output files from being created. I know it's probably something stupid, but I've been trying for days and cannot seem to find the problem.

So far, I've got the following directory structure:

─┬ fletch
 ├── fletch_annotations
 ├── fletch_example
 └── fletch_generator

fletch_annotations has annotation classes like the following:

package:fletch_annotations/src/model/annotation/column.dart

import 'package:fletch_annotations/src/model/enum/enum.dart';

class Column {
  final FletchDataType dataType;
  final bool isNullable;
  final OnConflictAction onConflictAction;
  final OnDeleteAction onDeleteAction;
  final OnUpdateAction onUpdateAction;
  final bool isUnique;
  final String? columnName;
  final dynamic defaultValue;
  final String? check;

  const Column({
    required this.dataType,
    required this.isNullable,
    this.onConflictAction = OnConflictAction.abort,
    this.onDeleteAction = OnDeleteAction.setDefault,
    this.onUpdateAction = OnUpdateAction.setDefault,
    this.isUnique = false,
    this.columnName,
    this.defaultValue,
    this.check,
  });
}

In fletch_generator, I have the following pubspec.yaml file:

fletch_generator/pubspec.yaml

name: fletch_generator
description: A new Flutter package project.
version: 0.0.1
homepage: https://example.com #! TODO: Replace this

environment:
  sdk: ">=2.18.4 <3.0.0"

dependencies:
  analyzer: ^5.3.1
  build: ^2.3.1
  collection: ^1.17.0
  fletch_annotations:
    path: ../fletch_annotations
  source_gen: ^1.2.6

dev_dependencies:
  build_config: ^1.1.1
  build_runner: ^2.3.2
  build_test: ^2.1.5
  flutter_lints: ^2.0.1
  test: ^1.22.1

fletch_generator has corresponding model classes:

package:fletch_generator/src/model/column.dart

import 'package:fletch_annotations/fletch_annotations.dart';

class Column {
  final FletchDataType dataType;
  final bool isNullable;
  final OnConflictAction onConflictAction;
  final OnDeleteAction onDeleteAction;
  final OnUpdateAction onUpdateAction;
  final bool isUnique;
  final String? columnName;
  final dynamic defaultValue;
  final String? check;

  Column({
    required this.dataType,
    required this.isNullable,
    this.onConflictAction = OnConflictAction.abort,
    this.onDeleteAction = OnDeleteAction.setDefault,
    this.onUpdateAction = OnUpdateAction.setDefault,
    this.isUnique = false,
    this.columnName,
    this.defaultValue,
    this.check,
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Column &&
          runtimeType == other.runtimeType &&
          dataType == other.dataType &&
          isNullable == other.isNullable &&
          onConflictAction == other.onConflictAction &&
          onDeleteAction == other.onDeleteAction &&
          onUpdateAction == other.onUpdateAction &&
          isUnique == other.isUnique &&
          columnName == other.columnName &&
          defaultValue == other.defaultValue &&
          check == other.check;

  @override
  int get hashCode {
    return Object.hash(dataType, columnName, defaultValue, check);
  }

  @override
  String toString() {
    return 'Column('
        'dataType: $dataType, '
        'isNullable: $isNullable ,'
        'onConflictAction: $onConflictAction ,'
        'onDeleteAction: $onDeleteAction ,'
        'onUpdateAction: $onUpdateAction ,'
        'isUnique: $isUnique ,'
        'columnName: $columnName, '
        'defaultValue: $defaultValue, '
        'check: $check'
        ')';
  }
}

My build.yaml file in fletch_generator:

fletch_generator/build.yaml

targets:
  $default:
    builders:
      fletch_generator:
        enabled: true
      source_gen|combining_builder:
        enabled: true
        options:
          ignore_for_file:
            - lines_longer_than_80_chars
            - lint_alpha
            - lint_beta
          build_extensions: '"{{dir}}/entity/{{file}}.dart": "{{dir}}/dao/{{file}}.g.dart"'

builders:
  fletch_generator:
    import: "package:fletch_generator/builder.dart"
    builder_factories: ["fletchBuilder"]
    build_extensions: { ".dart": [".fletch.g.part"] }
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

build.dart, also in fletch_generator (in the lib directory):

package:fletch_generator/build.dart

import 'package:build/build.dart';
import 'package:fletch_generator/src/generator/column.dart';
import 'package:source_gen/source_gen.dart';

Builder fletchBuilder(final BuilderOptions _) =>
    SharedPartBuilder([ColumnGenerator()], 'fletch');

And finally, the fletch_generator definition for ColumnGenerator:

package:fletch_generator/src/generator/column.dart

import 'dart:async';

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:fletch_annotations/fletch_annotations.dart' as annotations;
import 'package:source_gen/source_gen.dart';

class ColumnGenerator extends GeneratorForAnnotation<annotations.Column> {
  @override
  FutureOr<String> generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    return '// ColumnProcessor works!';
  }
}

In fletch_example, I have a sample_class that utilizes the Column annotation:

`package:fletch_example/src/model/entity/sample_class.dart

import 'package:fletch_annotations/fletch_annotations.dart';

part '../dao/sample_class.g.dart';

class SampleClass {
  @Column(
    dataType: FletchDataType.bigInt,
    isNullable: false,
  )
  final int? sampleClassIndex;

  SampleClass({
    this.sampleClassIndex,
  });
}

As well as the appropriate dependencies in the pubspec.yaml file for fletch_example:

name: fletch_example
description: A starting point for Dart libraries or applications.
version: 1.0.0
# homepage: https://www.example.com

environment:
  sdk: ">=2.18.4 <3.0.0"

dependencies:
  fletch_annotations:
    path: ../fletch_annotations

dev_dependencies:
  build_runner: ^2.3.2
  fletch_generator:
    path: ../fletch_generator
  flutter_lints: ^2.0.1
  lints: ^2.0.0
  test: ^1.16.0

In my fletch_example directory, I run the following commands:

flutter clean
flutter pub get
flutter packages pub run build_runner build --delete-conflicting-outputs

This generates the following console output:

[INFO] Generating build script...
[INFO] Generating build script completed, took 525ms

[INFO] Precompiling build script......
[INFO] Precompiling build script... completed, took 9.5s

[INFO] Initializing inputs
[INFO] Building new asset graph...
[INFO] Building new asset graph completed, took 1.3s

[INFO] Checking for unexpected pre-existing outputs....
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms

[INFO] Running build...
[INFO] Generating SDK summary...
[INFO] 2.8s elapsed, 0/1 actions completed.
[INFO] 5.3s elapsed, 0/1 actions completed.
[INFO] Generating SDK summary completed, took 6.2s

[INFO] Running build completed, took 6.3s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 40ms

[INFO] Succeeded after 6.4s with 0 outputs (2 actions)

It also creates the following fletch_example/.dart_tool/build/entrypoint/build.dart file:

// ignore_for_file: directives_ordering
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:build_runner_core/build_runner_core.dart' as _i1;
import 'package:fletch_generator/builder.dart' as _i2;
import 'package:source_gen/builder.dart' as _i3;
import 'dart:isolate' as _i4;
import 'package:build_runner/build_runner.dart' as _i5;
import 'dart:io' as _i6;

final _builders = <_i1.BuilderApplication>[
  _i1.apply(
    r'fletch_generator:fletch_generator',
    [_i2.fletchBuilder],
    _i1.toDependentsOf(r'fletch_generator'),
    hideOutput: true,
    appliesBuilders: const [r'source_gen:combining_builder'],
  ),
  _i1.apply(
    r'source_gen:combining_builder',
    [_i3.combiningBuilder],
    _i1.toNoneByDefault(),
    hideOutput: false,
    appliesBuilders: const [r'source_gen:part_cleanup'],
  ),
  _i1.applyPostProcess(
    r'source_gen:part_cleanup',
    _i3.partCleanup,
  ),
];
void main(
  List<String> args, [
  _i4.SendPort? sendPort,
]) async {
  var result = await _i5.run(
    args,
    _builders,
  );
  sendPort?.send(result);
  _i6.exitCode = result;
}

My expectation is that a fletch_example/lib/src/model/dao/sample_class.g.dart file would be created containing the following:

// ColumnProcessor works!

If anyone could point me in the right direction, it would be so, incredibly appreciated!


Solution

  • A GeneratorForAnnotation only runs for top-level elements (like classes or top-level fields or methods) in a library. In your example, the @Column annotation is added to an instance member which does not trigger the generator.

    If you can, the easiest way to fix this would be to exceptionally change your generator so that classes with @Column annotations on their fields also need an annotation on the class declaration.

    Alternatively, you could use something like this to respect column annotations in class members as well:

    class ColumnGenerator extends Generator {
      static const _checker = TypeChecker.fromRuntime(annotations.Column);
    
      @override
      Future<String> generate(LibraryReader library, BuildStep buildStep) async {
        final values = <String>{};
    
        for (final classMember in library.classes) {
          for (final child in classMember.children) {
            final annotation = _checker.firstAnnotationOf(child);
    
            if (annotation != null) {
              values.add(await generateForAnnotatedElement(
                  child, ConstantReader(annotation), buildStep));
            }
          }
        }
    
        return values.join('\n\n');
      }
    
      FutureOr<String> generateForAnnotatedElement(
        Element element,
        ConstantReader annotation,
        BuildStep buildStep,
      ) {
        return '// ColumnProcessor works!';
      }
    }