How to get annotation field names in when generating code with source_gen

484 views Asked by At

I am trying to implement a generator based on source_gen. It will process classes annotated with ClassAnnotation and act on fields annotated with various annotations, all of which are subtypes of a 'marker' interface FieldAnnotationMarker. Here is an example of 2 such annotations:

@immutable
@Target({TargetKind.classType})
class ClassAnnotation {
  const ClassAnnotation();
}

abstract class FieldAnnotationMarker {}

@immutable
@Target({TargetKind.field})
class FieldAnnotationA implements FieldAnnotationMarker {
  final String fooString;
  final int barInt;

  const FieldAnnotationA({
    required this.fooString,
    required this.barInt,
  });
}

@immutable
@Target({TargetKind.field})
class FieldAnnotationB implements FieldAnnotationMarker {
  final bool bazBool;

  const FieldAnnotationB({
    required this.bazBool,
  });
}

Here is an example annotated class:

@immutable
@ClassAnnotation()
class Person {
  @FieldAnnotationA(fooString: 'foo', barInt: 17)
  @FieldAnnotationB(bazBool: true)
  final int age;

  const Person({
    required this.age,
  });
}

What I need to be able to generate my code is the following information:

  • Names of all fields annotated with any subtype of FieldAnnotationMarker (here: age).
  • For each field: a list of the annotations (I need the type, its field names and values in the input source file).

For example, I would be able to generate code like this:

import 'person.dart';

void function(Person person) {
  // person.age
  final ageAnnotations = [
    const FieldAnnotationA(fooString: 'foo', barInt: 17),
    const FieldAnnotationB(bazBool: true),
  ];

  for (final annotation in ageAnnotations) {
    final processor = annotationProcessors[annotation.runtimeType]!; // annotationProcessors will be imported, is a Map<Type, AnnotationProcessor>
    processor.process(person.age, annotation); // Each processor gets field value and the annotation and knows what to do with this information.
  }

  // Other annotated fields, if exist, according to the pattern.
}

Here is my code so far - I can collect the metadata but do so doing some very sketchy things, and it is not very comfortable to use:

class MyGenerator extends GeneratorForAnnotation<ClassAnnotation> {
  @override
  String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    final fieldVisitor = _FieldVisitor();
    element.visitChildren(fieldVisitor);

    print(fieldVisitor.fields); // Will generate code based on collected data.

    return null;
  }
}

class _FieldVisitor extends SimpleElementVisitor<void> {
  final Map<String, List<_Annotation>> fields = {};

  @override
  void visitFieldElement(FieldElement element) {
    final annotations = _fieldAnnotationTypeChecker.annotationsOf(element).map( // <<< 1
      (fieldAnnotation) {
        final typeName = fieldAnnotation.type!.element!.name!;
        final annotationReader = ConstantReader(fieldAnnotation); // <<< 2
        final impl = fieldAnnotation as DartObjectImpl; // <<< 3
        final fields = impl.fields!.map(
          (fieldName, field) {
            return MapEntry(
              fieldName,
              annotationReader.read(fieldName).literalValue, // <<< 4
            );
          },
        );

        return _Annotation(typeName, fields);
      },
    ).toList(growable: false);

    fields[element.name] = annotations;
  }
}

const _fieldAnnotationTypeChecker =
    TypeChecker.fromRuntime(FieldAnnotationMarker);

@immutable
class _Annotation {
  final String typeName;
  final Map<String, Object?> fields;

  const _Annotation(this.typeName, this.fields);
}

Here are questions to the lines marked with <<< <number>:

  1. Here I want to fetch annotations that are subtypes of FieldAnnotationMarker - this seems to work for my basic tests but I'm not sure if this is the right way?
  2. Is it Ok to just instantiate my own ConstantReader?
  3. I don't want to hardcode the concrete types into the generator (users are able to register custom annotations and their processors) so I need some way of getting all the fields and values from the annotations dynamically. I can see all I need in the debugger but in code the only way I found to get annotations fields is this cast to DartObjectImpl which I detest and which also forces me to import the internal 'analyzer/src'. How can I get the field names in a proper way?
  4. This gets me the value like 1 for int or string value for String, but I will need to generate code from this, so I will need to write the string value in quotes eventually. For this I need to transform the value based on the type. Is there a way to get the literals as they appear in code, e.g. the string true for bool, 1 for ints and 'string value' (with the quotes) for strings? This would allow me to just paste this value directly when generating code.

Ideally, I could just get a whole string containing e.g. FieldAnnotationA(fooString: 'foo', barInt: 17) instead of having to collect the parts (type name, field names, values) and then join all of it together again - is this possible?

1

There are 1 answers

2
simolus3 On BEST ANSWER

Here I want to fetch annotations that are subtypes of FieldAnnotationMarker - this seems to work for my basic tests but I'm not sure if this is the right way?

Using a TypeChecker for annotations will also match subtypes, so that will work and is a decent solution for finding annotations of specific types.

Is it Ok to just instantiate my own ConstantReader?

Absolutely! It's part of source_gen's public API and only a stateless wrapper around the DartObject class from the analyzer either way.

How can I get the field names in a proper way?

By loading the type from the DartObject and referring to the defining class:

  1. Get fieldAnnotation.type
  2. Check that it's an InterfaceType. An InterfaceType is the class used for types based on Dart classes (as opposed to a say DynamicType for dynamic or a FunctionType for functions). Since you're using a TypeChecker to find matching annotations, you're guaranteed to only get InterfaceTypes so you might as well cast there.
  3. Use (fieldAnnotation.type as InterfaceType).accessors to get all getters and setters defined for that type (note that fields implicitly define a getter as well)
  4. For each accessor, you can use accessor.variable.name to get the name of the field, which you can then look up in the constant reader. Skip accessors where isSetter is true (since you only care about getters) or where isSynthetic is false (since you want to get getters defined by fields only and don't care about explicit get functions defined on a class).

Is there a way to get the literals as they appear in code, e.g. the string true for bool, 1 for ints and 'string value' (with the quotes) for strings?

For everything except strings, you can just use toString() to get a valid literal. For strings, you could write something manually:

String asDartLiteral(String value) {
  final escaped = escapeForDart(value);
  return "'$escaped'";
}

String escapeForDart(String value) {
  return value
      .replaceAll('\\', '\\\\')
      .replaceAll("'", "\\'")
      .replaceAll('\$', '\\\$')
      .replaceAll('\r', '\\r')
      .replaceAll('\n', '\\n');
}

Or use a package doing code-generation for you, like literalString from package:code_builder.