diff --git a/_test_annotations/lib/test_annotations.dart b/_test_annotations/lib/test_annotations.dart index 237c2975..448b122d 100644 --- a/_test_annotations/lib/test_annotations.dart +++ b/_test_annotations/lib/test_annotations.dart @@ -1,3 +1,42 @@ +/// A sample library for the Annotations used during testing. +library _test_annotations; + class TestAnnotation { const TestAnnotation(); } + +class TestAnnotationWithComplexObject { + final ComplexObject object; + const TestAnnotationWithComplexObject(this.object); +} + +class TestAnnotationWithSimpleObject { + final SimpleObject obj; + const TestAnnotationWithSimpleObject(this.obj); +} + +class SimpleObject { + final int i; + const SimpleObject(this.i); +} + +class ComplexObject { + final SimpleObject sObj; + final CustomEnum? cEnum; + final Map? cMap; + final List? cList; + final Set? cSet; + const ComplexObject( + this.sObj, { + this.cEnum, + this.cMap, + this.cList, + this.cSet, + }); +} + +enum CustomEnum { + v1, + v2, + v3; +} diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md index df22f35d..a54998c1 100644 --- a/source_gen/CHANGELOG.md +++ b/source_gen/CHANGELOG.md @@ -1,4 +1,5 @@ -## 1.4.1-wip +## 1.5.0-wip +- Add a `Reviver` class that hydrates a `ConstantReader` or `DartObject?` ## 1.4.0 diff --git a/source_gen/lib/source_gen.dart b/source_gen/lib/source_gen.dart index b695732e..dcc9f676 100644 --- a/source_gen/lib/source_gen.dart +++ b/source_gen/lib/source_gen.dart @@ -6,6 +6,7 @@ export 'src/builder.dart' show LibraryBuilder, PartBuilder, SharedPartBuilder, defaultFileHeader; export 'src/constants/reader.dart' show ConstantReader; export 'src/constants/revive.dart' show Revivable; +export 'src/constants/reviver.dart' show Reviver; export 'src/generator.dart' show Generator, InvalidGenerationSourceError; export 'src/generator_for_annotation.dart' show GeneratorForAnnotation; export 'src/library.dart' show AnnotatedElement, LibraryReader; diff --git a/source_gen/lib/src/constants/reviver.dart b/source_gen/lib/src/constants/reviver.dart new file mode 100644 index 00000000..16cee2a1 --- /dev/null +++ b/source_gen/lib/src/constants/reviver.dart @@ -0,0 +1,194 @@ +import 'dart:mirrors'; + +import 'package:analyzer/dart/constant/value.dart'; + +import '../type_checker.dart'; +import 'reader.dart'; + +/// Revives a [ConstantReader] to an instance in memory using Dart mirrors. +/// +/// Converts a serialized [DartObject] and transforms it into a fully qualified +/// instance of the object to be consumed. +/// +/// An intended usage of this is to provide those creating Generators a simpler +/// way to initialize their annotations as in memory instances. This allows for +/// cleaner and smaller implementations that don't have an underlying knowledge +/// of the [ConstantReader]. This simplfies cases like the following: +/// +/// ```dart +/// // Defined within a builder library and exposed for consumers to extend. +/// /// This [Delegator] delegates some complex processing. +/// abstract class Delegator { +/// const Delegator(); +/// +/// T call([dynamic]); +/// } +/// +/// // Consumer library +/// /// My CustomDelegate callable to be used in a builder +/// class CustomDelegate implements Delegator { +/// const CustomDelegate(); +/// +/// @override +/// ReturnType call(Map args) async { +/// // My implementation details. +/// } +/// } +/// ``` +/// +/// Where a library exposes an interface that the user is to implement by +/// the library doesn't need to know all of the implementation details. +class Reviver { + final ConstantReader reader; + const Reviver(this.reader); + Reviver.fromDartObject(DartObject? object) : this(ConstantReader(object)); + + /// Recurively build the instance and return it. + /// + /// This may return null when the declaration doesn't exist within the + /// system or the [reader] is null. + /// + /// In the event the reader is a primative type it returns that value. + /// Collections are iterated and revived. + /// Otherwise a fully qualified instance is returned. + dynamic toInstance() { + if (reader.isPrimative) { + return primativeValue; + } else if (reader.isCollection) { + if (reader.isList) { + // ignore: omit_local_variable_types + Type t = dynamic; + if (reader.listValue.isNotEmpty) { + // ignore: avoid_dynamic_calls + t = Reviver.fromDartObject(reader.listValue.first) + .toInstance() + .runtimeType; + } + return toTypedList(t); + } else if (reader.isSet) { + // ignore: omit_local_variable_types + Type t = dynamic; + if (reader.setValue.isNotEmpty) { + // ignore: avoid_dynamic_calls + t = Reviver.fromDartObject(reader.setValue.first) + .toInstance() + .runtimeType; + } + return toTypedSet(t); + } else { + // ignore: omit_local_variable_types + Type kt = dynamic; + // ignore: omit_local_variable_types + Type vt = dynamic; + if (reader.mapValue.isNotEmpty) { + // ignore: avoid_dynamic_calls + kt = Reviver.fromDartObject(reader.mapValue.keys.first) + .toInstance() + .runtimeType; + // ignore: avoid_dynamic_calls + vt = Reviver.fromDartObject(reader.mapValue.values.first) + .toInstance() + .runtimeType; + } + return toTypedMap(kt, vt); + } + } else if (reader.isLiteral) { + return reader.literalValue; + } else if (reader.isType) { + return reader.typeValue; + } else if (reader.isSymbol) { + return reader.symbolValue; + } else { + final decl = classMirror; + if (decl.isEnum) { + final values = decl.getField(const Symbol('values')).reflectee as List; + return values[reader.objectValue.getField('index')!.toIntValue()!]; + } + + final pv = positionalValues; + final nv = namedValues; + + return decl + .newInstance(Symbol(reader.revive().accessor), pv, nv) + .reflectee; + } + } + + dynamic get primativeValue { + if (reader.isNull) { + return null; + } else if (reader.isBool) { + return reader.boolValue; + } else if (reader.isDouble) { + return reader.doubleValue; + } else if (reader.isInt) { + return reader.intValue; + } else if (reader.isString) { + return reader.stringValue; + } + } + + List get positionalValues => reader + .revive() + .positionalArguments + .map( + (value) => Reviver.fromDartObject(value).toInstance(), + ) + .toList(); + + Map get namedValues => reader.revive().namedArguments.map( + (key, value) { + final k = Symbol(key); + final v = Reviver.fromDartObject(value).toInstance(); + return MapEntry(k, v); + }, + ); + + ClassMirror get classMirror { + final revivable = reader.revive(); + + // Flatten the list of libraries + final entries = Map.fromEntries(currentMirrorSystem().libraries.entries) + .map((key, value) => MapEntry(key.pathSegments.first, value)); + + // Grab the library from the system + final libraryMirror = entries[revivable.source.pathSegments.first]; + if (libraryMirror == null || libraryMirror.simpleName == Symbol.empty) { + throw Exception('Library missing'); + } + + // Determine the declaration being requested. Split on . when an enum is passed in. + var declKey = Symbol(revivable.source.fragment); + if (reader.isEnum) { + // The accessor when the entry is an enum is the ClassName.value + declKey = Symbol(revivable.accessor.split('.')[0]); + } + + final decl = libraryMirror.declarations[declKey] as ClassMirror?; + if (decl == null) { + throw Exception('Declaration missing'); + } + return decl; + } + + List? toTypedList(T t) => reader.listValue + .map((e) => Reviver.fromDartObject(e).toInstance() as T) + .toList() as List?; + + Map? toTypedMap(KT kt, VT vt) => reader.mapValue.map( + (key, value) => MapEntry( + Reviver.fromDartObject(key).toInstance() as KT, + Reviver.fromDartObject(value).toInstance() as VT, + ), + ) as Map?; + + Set? toTypedSet(T t) => reader.setValue + .map((e) => Reviver.fromDartObject(e).toInstance() as T) + .toSet() as Set?; +} + +extension IsChecks on ConstantReader { + bool get isCollection => isList || isMap || isSet; + bool get isEnum => instanceOf(const TypeChecker.fromRuntime(Enum)); + bool get isPrimative => isBool || isDouble || isInt || isString || isNull; +} diff --git a/source_gen/pubspec.yaml b/source_gen/pubspec.yaml index d46881c4..82e5444d 100644 --- a/source_gen/pubspec.yaml +++ b/source_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: source_gen -version: 1.4.1-wip +version: 1.5.0-wip description: >- Source code generation builders and utilities for the Dart build system repository: https://github.com/dart-lang/source_gen/tree/master/source_gen diff --git a/source_gen/test/constants/reviver_test.dart b/source_gen/test/constants/reviver_test.dart new file mode 100644 index 00000000..7d27cee2 --- /dev/null +++ b/source_gen/test/constants/reviver_test.dart @@ -0,0 +1,91 @@ +import 'package:_test_annotations/test_annotations.dart'; +import 'package:build_test/build_test.dart'; +import 'package:source_gen/src/constants/reader.dart'; +import 'package:source_gen/src/constants/reviver.dart'; +import 'package:test/test.dart'; + +void main() { + group('Reviver', () { + group('revives classes', () { + group('returns qualified class', () { + const declSrc = r''' +library test_lib; + +import 'package:_test_annotations/test_annotations.dart'; + +@TestAnnotation() +class TestClassSimple {} + +@TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1))) +class TestClassComplexPositional {} + +@TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1), cEnum: CustomEnum.v2, cMap: {'1':ComplexObject(SimpleObject(1)),'2':ComplexObject(SimpleObject(2)),'fred':ComplexObject(SimpleObject(3))}, cList: [ComplexObject(SimpleObject(1))], cSet: {ComplexObject(SimpleObject(1)),ComplexObject(SimpleObject(2))})) +class TestClassComplexPositionalAndNamed {} +'''; + test('with simple objects', () async { + final reader = (await resolveSource( + declSrc, + (resolver) async => (await resolver.findLibraryByName('test_lib'))!, + )) + .getClass('TestClassSimple')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .toList() + .first; + + final reviver = Reviver(reader); + final instance = reviver.toInstance(); + expect(instance, isNotNull); + expect(instance, isA()); + }); + + for (final s in ['Positional', 'PositionalAndNamed']) { + test('with complex objects: $s', () async { + final reader = (await resolveSource( + declSrc, + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!, + )) + .getClass('TestClassComplex$s')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .toList() + .first; + + final reviver = Reviver(reader); + final instance = reviver.toInstance(); + expect(instance, isNotNull); + expect(instance, isA()); + instance as TestAnnotationWithComplexObject; + + expect(instance.object, isNotNull); + expect(instance.object.sObj.i, 1); + + if (s == 'PositionalAndNamed') { + expect(instance.object.cEnum, isNotNull); + expect(instance.object.cEnum, CustomEnum.v2); + + expect(instance.object.cList, isNotNull); + expect(instance.object.cList!, const [ + ComplexObject(SimpleObject(1)), + ]); + + expect(instance.object.cMap, isNotNull); + expect(instance.object.cMap!, const { + '1': ComplexObject(SimpleObject(1)), + '2': ComplexObject(SimpleObject(2)), + 'fred': ComplexObject(SimpleObject(3)), + }); + + expect(instance.object.cSet, isNotNull); + expect(instance.object.cSet!, const { + ComplexObject(SimpleObject(1)), + ComplexObject(SimpleObject(2)), + }); + } + }); + } + }); + }); + }); +}