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>
:
- 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? - Is it Ok to just instantiate my own
ConstantReader
? - 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? - This gets me the value like
1
forint
orstring value
forString
, 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 stringtrue
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?
Using a
TypeChecker
for annotations will also match subtypes, so that will work and is a decent solution for finding annotations of specific types.Absolutely! It's part of
source_gen
's public API and only a stateless wrapper around theDartObject
class from theanalyzer
either way.By loading the type from the
DartObject
and referring to the defining class:fieldAnnotation.type
InterfaceType
. AnInterfaceType
is the class used for types based on Dart classes (as opposed to a sayDynamicType
fordynamic
or aFunctionType
for functions). Since you're using aTypeChecker
to find matching annotations, you're guaranteed to only getInterfaceType
s so you might as well cast there.(fieldAnnotation.type as InterfaceType).accessors
to get all getters and setters defined for that type (note that fields implicitly define a getter as well)accessor.variable.name
to get the name of the field, which you can then look up in the constant reader. Skip accessors whereisSetter
is true (since you only care about getters) or whereisSynthetic
is false (since you want to get getters defined by fields only and don't care about explicitget
functions defined on a class).For everything except strings, you can just use
toString()
to get a valid literal. For strings, you could write something manually:Or use a package doing code-generation for you, like
literalString
frompackage:code_builder
.