🌏

dart macros チートシート

2024/07/09に公開

環境

Flutter 3.22.0-36.0.pre.1
Dart 3.5.0 (build 3.5.0-154.0.dev)

interface class Macro

※Doc参照

Every macro interface is a subtype of a root Macro marker interface. There are interfaces for each kind of declaration macros can be applied to: class, function, etc. Then, for each of those, there is an interface for each macro phase: type, declaration, and definition. A single macro class can implement as many of these interfaces as it wants to. This allows a single macro to participate in multiple phases and to support being applied to multiple kinds of declarations.

  • すべてのMacro Interfaceroot Macro,marker interfaceサブタイプである
  • macro classを生成する場合に最低でも一つ、上記サブタイプを適用し、単一メソッド宣言 / 実装する必要がある
  • また、サブタイプは複数宣言することも可能

interface Macro sub type

interface Macroのサブタイプは複数存在している
主に以下が用意されている

  • library
  • Function
  • Variable
  • Class
  • Enum
  • EnumValue
  • Field
  • Method
  • Constructor
  • Mixin
  • Extension
  • ExtensionType
  • TypeAlias

ここに、各々以下が用意されている

  • TypesMacro
    • 新しい型宣言を追加したい場合
  • DeclarationsMacro
    • 新しい非型宣言を追加したい場合
  • DefinitionMacro
    • {サブタイプ}内の宣言の定義を提供する

libraryだったら以下のように定義されている

/// The interface for [Macro]s that can be applied to a library directive, and
/// want to contribute new type declarations to the library.
abstract interface class LibraryTypesMacro implements Macro {
  FutureOr<void> buildTypesForLibrary(Library library, TypeBuilder builder);
}

/// The interface for [Macro]s that can be applied to a library directive, and
/// want to contribute new non-type declarations to the library.
abstract interface class LibraryDeclarationsMacro implements Macro {
  FutureOr<void> buildDeclarationsForLibrary(
      Library library, DeclarationBuilder builder);
}

/// The interface for [Macro]s that can be applied to a library directive, and
/// want to provide definitions for declarations in the library.
abstract interface class LibraryDefinitionMacro implements Macro {
  FutureOr<void> buildDefinitionForLibrary(
      Library library, LibraryDefinitionBuilder builder);
}

argument

  • ここでいう引数とはinterface Macroで宣言されているメソッドの引数
    • 上記例のbuildDefinitionForLibraryLibrary library, LibraryDefinitionBuilder builderの部分

第一引数(Declaration argument)

The first argument to a builder method is an object describing the declaration it is applied to. This argument contains only essentially the parsed AST for the declaration itself, and does not include nested declarations.
For example, in ClassDeclarationsMacro, the introspection object is a ClassDeclaration. This gives you access to the name of the class and access to the immediate superclass, as well as any immediate mixins or interfaces, but not its members or entire class hierarchy.

  • 適応される宣言を記述できるオブジェクト
  • クラスの名前 / 直接のスーパークラス / 直接のミックスインやインターフェースにアクセスが可能
    • ただそのメンバーやクラス階層全体にはアクセスできない

class Sample implements ClassDeclarationsMacro {
  const Sample();

  
  Future<FutureOr<void>> buildDeclarationsForClass(
      ClassDeclaration classDeclaration, MemberDeclarationBuilder builder,
) async {
    // macroを適用したクラスのクラス名を取得
    final className = classDeclaration.identifier.name;
    // クラス内で宣言した変数 / 定数の一覧を取得
    final fields = await builder.fieldsOf(classDeclaration);
    // クラス内で宣言した変数 / 定数の型を取得
    final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
    // ...
    // ...
}

ClassDeclarationの親子関係

ClassDeclarationに限らず、interface Macroのサブタイプに応じて~Declarationが用意されている、が今回はClassDeclarationに焦点を置く

  • 階層
.
└── Annotatable / MacroTarget
    └── Declaration
        └── TypeDeclaration
            └── ParameterizedTypeDeclaration
                └── ClassDeclaration

基本となるベースはDeclarationとされている
Declarationから、library / identifierにアクセスが可能となり、フィールド情報やクラス情報などを取得参照できるようになる。

※その他は、サブタイプに紐づいた(marker interfaceや型パラメーターを持つ)interfaceとなっている

ClassDeclarationのfield

 /// このクラスに `external` 修飾子があるかどうか。
 bool get hasExternal;

 /// このクラスに `final` 修飾子があるかどうか。
 bool get hasFinal;

 /// このクラスに `interface` 修飾子があるかどうか。
 bool get hasInterface;

 /// このクラスに `mixin` 修飾子があるかどうか。
 bool get hasMixin;

 /// このクラスに `sealed` 修飾子があるかどうか。
 bool get hasSealed;

 /// `extends` 型注釈 (存在する場合)。
 NamedTypeAnnotation? get superclass;

 /// すべての `implements` 型注釈。
 Iterable<NamedTypeAnnotation> get interfaces;

 /// すべての `with` 型注釈。
 Iterable<NamedTypeAnnotation> get mixins;

第二引数(Builder argument)

The second argument is an instance of a builder class. It exposes both methods to contribute new code to the program, as well as phase specific introspection capabilities.
In ClassDeclarationsMacro, the builder is a ClassDeclarationBuilder. Its primary method is declareInClass, which the macro can call to add a new member to the class. It also implements the ClassIntrospector interface, which allows you to get the members of the class, as well as its entire class hierarchy.

  • プログラムに新しいコードを追加するメソッドと、フェーズ固有のイントロスペクション機能の両方を公開する
    • 要は、Declaration argumentで取得したオブジェクトの詳細情報を取得するメソッドなどを提供してくれている
      • ※先ほどの例のfieldsOfのようなもの
    • builder~~PhaseIntrospectorを継承したオブジェクトとなっているため、そこに準拠した機能を使用することができる

fieldsOfを例に紐解くと

  /// The fields available for [type].
  ///
  /// This may be incomplete if additional declaration macros are going to run
  /// on [type].
  Future<List<FieldDeclaration>> fieldsOf(covariant TypeDeclaration type);

このTypeDeclarationというのがmarker Interfaceのこと。
marker Interfaceは第一引数のオブジェクトが継承している大元のinterfaceとなり、
builderインスタンスのメソッド(fieldsOfなど)を使用することにより、フィールド情報を参照することが可能となる。

MemberDeclarationBuilderの親子関係

~Declarationと同様サブタイプに応じて~DeclarationBuilderが用意されている

.
└── TypePhaseIntrospector
    └── Builder / DeclarationPhaseIntrospector
        └── DeclarationBuilder
            └── MemberDeclarationBuilder

基本となるベースはDeclarationPhaseIntrospectorとなっている
DeclarationPhaseIntrospectorの他にもTypePhaseIntrospector / DefinitionPhaseIntrospectorも用意されており、interface Macroに応じて使用するinterfaceが異なっている

DeclarationPhaseIntrospectorのfield

/// 指定された [type] アノテーションの新しい [StaticType] をインスタンス化します。
///
/// [type] が [RawTypeAnnotationCode] の場合、生の [Identifier] は許可されていないため、より具体的なサブタイプを使用する必要がある
Future<StaticType> resolve(TypeAnnotationCode type);

/// 引数[enuum] で使用可能な値
/// - enum情報を取得することができる
Future<List<EnumValueDeclaration>> valuesOf(covariant EnumDeclaration enuum);

/// 引数[type] で使用できるフィールド
/// - フィールド情報を取得することができる
Future<List<FieldDeclaration>> fieldsOf(covariant TypeDeclaration type);

/// 引数[type] で使用できるメソッド。
/// - メソッド情報を取得することができる
Future<List<MethodDeclaration>> methodsOf(covariant TypeDeclaration type);

/// 引数[type] で使用できるコンストラクター
/// - constructor情報を取得することができる
Future<List<ConstructorDeclaration>>constructorsOf(
covariant TypeDeclaration type);

/// [library] で宣言されているすべての型の [TypeDeclaration]が取得参照できる
Future<List<TypeDeclaration>> typesOf(covariant Library library);

/// 引数[identifier] を [TypeDeclaration] に解決 / 変換
///
/// [identifier] が [TypeDeclaration] に解決 / 変換されない場合は、
/// [MacroImplementationException] がスローされる
Future<TypeDeclaration> typeDeclarationOf(covariant Identifier identifier);

TypeAnnotation

型参照インターフェース

  /// null(?)が含まれているかどうか
  bool get isNullable;

  /// sealed classである[code]オブジェクトを取得する
  TypeAnnotationCode get code;

TypeAnnotationCode

Codeを継承したインターフェース。
サブタイプでは、TypeAnnotationCodeを継承。

// 型注釈の null 可能バージョンを表すコード
// `underlyingType`でnull可能な方を取得する
NullableTypeAnnotationCode
// 名前付き型の参照を表すコード
// `name`で型名を取得
NamedTypeAnnotationCode
// 関数名を取得するコード
FunctionTypeAnnotationCode
// レコード型名を取得するコード
RecordTypeAnnotationCode
// 省略された型注釈のコード
OmittedTypeAnnotationCode

Library

ライブラリのイントロスペクション情報

  // ライブラリの言語バージョン
  LanguageVersion get languageVersion;

  // ライブラリを識別するURI
  Uri get uri;

LanguageVersion

  // メジャーバージョン
  int get major;
  // マイナーバージョン
  int get minor;

参考

https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md
https://github.com/dart-lang/language/tree/main/working/macros/example
https://dart.dev/language/macros

Discussion