見出し画像

freezedみたいなコード生成を伴うパッケージはどうやって作るの?という話

Dartでのコード生成

こんにちは。 Showcase Gig でエンジニアをしている、鈴木です。

Flutter(Dart)のパッケージの中にはfreezedauto_routeのようにbuild_runnerを使ってコードを生成するものがあります。 今回はこのようにbuild_runnerを通してコード生成するパッケージはどのように作るのかについて紹介します。

サンプルではbuild_runnerと、buildのAPIをラップしてコード生成しやすくするパッケージであるsource_genを使って簡単なコードを生成してみます。

作成するサンプル

@Greeter()
class TestClass {}

というようにクラスにアノテーションを付加してbuild_runnerを実行すると以下のようなファイルを生成するジェネレータを作ってみます。

extension TestClassExt on TestClass {
  String greet() {
    return 'hello TestClass';
  }
}

パラメータを指定すると「hello」を置き換えたり、末尾に言葉を追加したりできるようにもしてみます。

ディレクトリ構成

以下のような構成で作成していきます。

example、gensample、gensample_generatorで構成しています。

  • gensampleで「@Greeter()」のようなアノテーションを定義します。

  • gensample_generatorがbuild_runnerから駆動されてコードを生成するモジュールになります。

  • 上記で定義したアノテーションを使ってみた例をexampleに書きます。

コード生成を伴うパッケージは大抵このような構成になっており、利用時はpubspec.yamlでアノテーション定義をdependenciesに、ジェネレータ部分をdev_dependenciesに記述します。 例えばfreezedを利用するときは以下のように定義します。

dependencies:
  freezed_annotation:

dev_dependencies:
  freezed:
  build_runner:

ジェネレータとbuild_runnerは開発時にしか利用しないのでdev_dependenciesへの記述になります。

アノテーションの定義

まずはアノテーションそのものを定義していきます。gensampleの下にpubspec.yamlを用意します。

name: gensample
descrpition: a generator sample

environment:
  sdk: ">=2.14.0 <3.0.0"

dev_dependencies:
  test: ^1.20.1

同じ階層に「lib」ディレクトリを作成して「gensample.dart」を作成します。 さらに「lib」の下に「src」ディレクトリを作成して「greeter.dart」を作成します。 それぞれ以下のようになります。

library gensample;

export 'src/greeter.dart';
class Greeter {
  const Greeter({this.message = 'hello', this.ps});

  final String message;
  final String? ps;
}

今回はシンプルな例ですので、アノテーションの定義はこれだけです。

ジェネレータの実装

次にジェネレータを作成します。 まずは先程と同様にgensample_generatorの下にpubspec.yamlを作成します。

name: gensample_generator
description: a sample generator
version: 1.0.0

environment:
  sdk: ">=2.14.0 <3.0.0"

dependencies:
  analyzer:
  build:
  build_config:
  source_gen:
  gensample:
    path: ../gensample/

dev_dependencies:
  test: ^1.20.1

ジェネレータでは「build.yaml」でコード生成するための情報を与える必要があります。 以下のように作成しておきます。

targets:
  $default:
    builders:
      gensample_generator|gensample:
        enabled: true

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

build.yamlで指定している内容についてはbuild_configのREADMEを参照してください。

「builder_factories」にはBuilderを返す関数を記述します。ここで記述した関数が返すBuilderを使ってコード生成が進みます。 ここでは「genSample」という名前で定義しました。 「lib」ディレクトリを作成し、直下に「builder.dart」を作成して「genSample」を記述します。

import 'package:build/build.dart';
import 'package:gensample_generator/src/gensample_generator.dart';
import 'package:source_gen/source_gen.dart';

Builder genSample(BuilderOptions options) =>
    SharedPartBuilder([GenSampleGenerator()], 'gen_sample');

返すBuilderインスタンスはSharedPartBuilderのほかにPartBuilder、LibraryBuilderがあります。 part of でファイルを分割する場合はSharedPartBuilderかPartBuilderになりますが、PartBuilderは非推奨のようです(ちなみにfreezedはPartBuilderを使っています)。 ここではSharedPartBuilderを返しています。パラメータにはジェネレータ(これから定義します)と一意の識別子を渡しています。

libの下に「src」ディレクトリを作成して、その下に「gensample_generator.dart」を作成します。 ここにジェネレータ(GenSampleGenerator)を定義していきましょう。

import 'package:analyzer/dart/element/element.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:gensample/gensample.dart';
import 'package:source_gen/source_gen.dart';

class GenSampleGenerator extends GeneratorForAnnotation<Greeter> {
  @override
  Stream<String> generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) async* {
    final annotation = const TypeChecker.fromRuntime(Greeter)
        .firstAnnotationOf(element, throwOnUnresolved: false);
    final message = annotation?.getField('message')?.toStringValue() ?? '';
    final ps = annotation?.getField('ps')?.toStringValue() ?? '';

    yield '''
    extension ${element.name}Ext on ${element.name} {
      String greet() {
        return '${message} ${element.name}${ps.isNotEmpty ? ',${ps}' : ''}';
      }
    }
    ''';
  }
}

アノテーションを見つけるとgenerateForAnnotatedElementが呼ばれるのでそこで適切なコードを返してあげます。 今回定義したアノテーションのGreeterクラスではmessageとpsというフィールドを持っていますが、以下のようにして ジェネレータからアクセスしています。

final annotation = const TypeChecker.fromRuntime(Greeter)
    .firstAnnotationOf(element, throwOnUnresolved: false);
final message = annotation?.getField('message')?.toStringValue() ?? '';
final ps = annotation?.getField('ps')?.toStringValue() ?? '';

また、アノテーションが付与された要素(今回の例ではTestClassなど)はパラメータで渡されてくるelementで情報にアクセスできます。 このようにアノテーションの情報と定義された対象の要素の情報を組み合わせてコード生成していきます。

使ってみる

ではexampleの下にアノテーションを使ったコード生成を試してみます。 exampleの直下にpubsplec.yamlを作成します。

name: example
description: demo
version: 1.0.0

environment:
  sdk: ">=2.14.0 <3.0.0"

dependencies:
  gensample:
    path: ../gensample/

dev_dependencies:
  build_runner:
  gensample_generator:
    path: ../gensample_generator/

libディレクトリを作成してその下にアノテーションを使ったサンプルクラスを定義してみます。

import 'package:gensample/gensample.dart';

part "one.g.dart";

@Greeter()
class TestClass {}
import 'package:gensample/gensample.dart';

part 'two.g.dart';

@Greeter(message: 'こんにちは', ps: '牛乳買ってきて')
class Two {
  const Two(this.something);

  final String something;
}

oneはデフォルトで、twoはパラメータを指定してアノテーションを使いました。 exampleディレクトリに移動して flutter pub run build_runner build としてみましょう。 成功するとexample/libの下に「one.g.dart」と「two.g.dart」が生成されます。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'one.dart';

// **************************************************************************
// GenSampleGenerator
// **************************************************************************

extension TestClassExt on TestClass {
  String greet() {
    return 'hello TestClass';
  }
}
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'two.dart';

// **************************************************************************
// GenSampleGenerator
// **************************************************************************

extension SecondTodoExt on Two {
  String greet() {
    return 'こんにちは Two,牛乳買ってきて';
  }
}

このように生成できました。 とても簡単なサンプルですが、build_runnerを通じてコード生成するパッケージはこの応用です。 freezedのように共通の処理を手広く導入したいケースやauto_routeのように煩雑な処理をスッキリ見せるケースなどうまく使えばとても便利なしくみなので利用シーンを探してみてはどうでしょうか。


この記事が気に入ったらサポートをしてみませんか?