Как сгенерировать код Dart для аннотаций в полях?

Я пишу генератор кода для Dart, используя build_runner, но мой конструктор не вызывается для аннотаций в полях, хотя он работает для аннотаций в классах.

Можно ли также вызвать генератор аннотаций в полях (или в любом другом месте)?

Например, конструктор вызывается для следующего файла:

import 'package:my_annotation/my_annotation.dart';

part 'example.g.dart';

@MyAnnotation()
class Fruit {
  int number;
}

Но не для этого:

import 'package:my_annotation/my_annotation.dart';

part 'example.g.dart';

class Fruit {
  @MyAnnotation()
  int number;
}

Вот определение аннотации:

class MyAnnotation {
  const MyAnnotation();
}

И так определяется генератор. На данный момент он просто прерывается при каждом вызове, вызывая вывод сообщения об ошибке.

library my_annotation_generator;

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:my_annotation/my_annotation.dart';
import 'package:source_gen/source_gen.dart';

Builder generateAnnotation(BuilderOptions options) =>
    SharedPartBuilder([MyAnnotationGenerator()], 'my_annotation');

class MyAnnotationGenerator extends GeneratorForAnnotation<MyAnnotation> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, _) {
    throw CodeGenError('Generating code for annotation is not implemented yet.');
}

Вот build.yaml конфигурация:

targets:
  $default:
    builders:
      my_annotation_generator|my_annotation:
        enabled: true

builders:
  my_annotation:
    target: ":my_annotation_generator"
    import: "package:my_annotation/my_annotation.dart"
    builder_factories: ["generateAnnotation"]
    build_extensions: { ".dart": [".my_annotation.g.part"] }
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

3 ответа

Решение

Встроенный GeneratorForAnnotation использует LibraryElementс annotatedWith(...), который проверяет только аннотации верхнего уровня. Чтобы также обнаруживать аннотации в полях, вам нужно написать что-нибудь особенное.

Вот Generator Я написал для своего проекта:

abstract class GeneratorForAnnotatedField<AnnotationType> extends Generator {
  /// Returns the annotation of type [AnnotationType] of the given [element],
  /// or [null] if it doesn't have any.
  DartObject getAnnotation(Element element) {
    final annotations =
        TypeChecker.fromRuntime(AnnotationType).annotationsOf(element);
    if (annotations.isEmpty) {
      return null;
    }
    if (annotations.length > 1) {
      throw Exception(
          "You tried to add multiple @$AnnotationType() annotations to the "
          "same element (${element.name}), but that's not possible.");
    }
    return annotations.single;
  }

  @override
  String generate(LibraryReader library, BuildStep buildStep) {
    final values = <String>{};

    for (final element in library.allElements) {
      if (element is ClassElement && !element.isEnum) {
        for (final field in element.fields) {
          final annotation = getAnnotation(field);
          if (annotation != null) {
            values.add(generateForAnnotatedField(
              field,
              ConstantReader(annotation),
            ));
          }
        }
      }
    }

    return values.join('\n\n');
  }

  String generateForAnnotatedField(
      FieldElement field, ConstantReader annotation);
}

По крайней мере, исходя из моего опыта, вашему файлу example.dart потребуется хотя бы одна аннотация над определением класса для анализа с помощью GeneratorForAnnotation.

example.dart:

import 'package:my_annotation/my_annotation.dart';

part 'example.g.dart';

@MyAnnotation()
class Fruit {
 @MyFieldAnnotation()
 int number;
}

Чтобы получить доступ к аннотациям над полями класса или методами класса, вы можете использовать посетителя для "посещения" каждого дочернего элемента и извлечения информации из исходного кода. Например, чтобы получить информацию о полях класса, вы можете переопределить метод visitFieldElement, а затем получить доступ к любым аннотациям с помощью getter: element.metadata.

builder.dart:

import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:source_gen/source_gen.dart';
import 'package:my_annotation/my_annotation.dart';

class MyAnnotationGenerator extends 
GeneratorForAnnotation<MyAnnotation> {
  @override
  FutureOr<String> generateForAnnotatedElement(
      Element element, 
      ConstantReader annotation,   
      BuildStep buildStep,){
    return _generateSource(element);
  }

  String _generateSource(Element element) {
    var visitor = ModelVisitor();
    element.visitChildren(visitor);
    return '''
      // ${visitor.className}
      // ${visitor.fields}
      // ${visitor.metaData}
    ''';
  }
}

class ModelVisitor extends SimpleElementVisitor {
  DartType className;
  Map<String, DartType> fields = {};
  Map<String, dynamic> metaData = {};

  @override
  visitConstructorElement(ConstructorElement element) {
    className = element.type.returnType;
  }

  @override
  visitFieldElement(FieldElement element) {
    fields[element.name] = element.type;
    metaData[element.name] = element.metadata;
  }
}

Примечание. В этом примере _generateSource возвращает закомментированный оператор. Без комментариев вам нужно будет вернуть правильно сформированный исходный код дротика, в противном случае построитель завершит работу с ошибкой.

Для получения дополнительной информации см.: Создание исходного кода и написание собственного пакета (The Boring Flutter Development Show, Ep. 22) https://www.youtube.com/watch?v=mYDFOdl-aWM&t=459s

У меня была очень похожая проблема, когда я пытался настроить таргетинг на определенные методы в моих аннотированных классах. Вдохновленный вашими ответами, я немного изменил аннотацию класса. model_visitorчтобы проверить аннотацию метода перед выбором элементов.

      class ClassAnnotationModelVisitor extends SimpleElementVisitor<dynamic> {
  String className;

  Map<String, String> methods = <String, String>{};
  Map<String, String> parameters = <String, String>{};

  @override
  dynamic visitConstructorElement(ConstructorElement element) {
    final elementReturnType = element.type.returnType.toString();
    className = elementReturnType.replaceFirst('*', '');
  }

  @override
  dynamic visitMethodElement(MethodElement element) {
    if (methodHasAnnotation(MethodAnnotation, element)) {
      final functionReturnType = element.type.returnType.toString();
      methods[element.name] = functionReturnType.replaceFirst('*', '');
      parameters[element.name] = element.parameters.map((e) => e.name).join(' ,');
    }
  }

  bool methodHasAnnotation(Type annotationType, MethodElement element) {
    final annotations = TypeChecker.fromRuntime(annotationType).annotationsOf(element);
    return !annotations.isEmpty;
  }
}

Затем я могу использовать основные GeneratorForAnnotationclass и генерировать для class и methodArray.

Другие вопросы по тегам