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:
FieldAnnotationMarker
(here: age
).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>
:
FieldAnnotationMarker
- this seems to work for my basic tests but I'm not sure if this is the right way?ConstantReader
?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?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?
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:
fieldAnnotation.type
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 InterfaceType
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 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
forint
s 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
.