From d932b16d4cbc2cb70f4778c9d64146d53e90988d Mon Sep 17 00:00:00 2001 From: Nexushunter Date: Mon, 18 Sep 2023 20:59:49 -0400 Subject: [PATCH 1/4] feat: Add a reviver that hydrates ConstantReader objects --- _test_annotations/lib/test_annotations.dart | 21 ++++ source_gen/lib/source_gen.dart | 1 + source_gen/lib/src/constants/reviver.dart | 128 ++++++++++++++++++++ source_gen/test/constants/reviver_test.dart | 73 +++++++++++ 4 files changed, 223 insertions(+) create mode 100644 source_gen/lib/src/constants/reviver.dart create mode 100644 source_gen/test/constants/reviver_test.dart diff --git a/_test_annotations/lib/test_annotations.dart b/_test_annotations/lib/test_annotations.dart index 237c2975..d4e9ca17 100644 --- a/_test_annotations/lib/test_annotations.dart +++ b/_test_annotations/lib/test_annotations.dart @@ -1,3 +1,24 @@ 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 SimpleObject? sObj2; + const ComplexObject(this.sObj, {this.sObj2 = null}); +} 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..173fd54e --- /dev/null +++ b/source_gen/lib/src/constants/reviver.dart @@ -0,0 +1,128 @@ +import 'dart:mirrors'; + +import 'package:analyzer/dart/constant/value.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.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.isList) { + return reader.listValue + .map((e) => Reviver.fromDartObject(e).toInstance()) + .toList(growable: false); + } else if (reader.isSet) { + return reader.setValue + .map((e) => Reviver.fromDartObject(e).toInstance()) + .toSet(); + } else if (reader.isString) { + return reader.stringValue; + } else if (reader.isMap) { + return reader.mapValue.map( + (key, value) => MapEntry( + Reviver.fromDartObject(key).toInstance(), + Reviver.fromDartObject(value).toInstance(), + ), + ); + } else if (reader.isLiteral) { + return reader.literalValue; + } else if (reader.isType) { + return reader.typeValue; + } else if (reader.isSymbol) { + return reader.symbolValue; + } else { + // TODO: Handle enum. + + final revivable = reader.revive(); + + // Grab the library from the system + final libraryMirror = currentMirrorSystem() + .libraries + .entries + .firstWhere( + (element) => + element.key.pathSegments.first == + revivable.source.pathSegments.first, + ) + .value; + + final decl = + libraryMirror.declarations[Symbol(revivable.source.fragment)]; + if (decl == null) { + // TODO: Throw instead? + return null; + } + + final positionalArguments = revivable.positionalArguments + .map((e) => Reviver.fromDartObject(e).toInstance()) + .toList(growable: false); + + final namedArguments = revivable.namedArguments.map( + (key, value) => MapEntry( + Symbol(key), + Reviver.fromDartObject(value).toInstance(), + ), + ); + + return (decl as ClassMirror) + .newInstance( + Symbol(revivable.accessor), + positionalArguments, + namedArguments, + ) + .reflectee; + } + } +} diff --git a/source_gen/test/constants/reviver_test.dart b/source_gen/test/constants/reviver_test.dart new file mode 100644 index 00000000..9160f0b4 --- /dev/null +++ b/source_gen/test/constants/reviver_test.dart @@ -0,0 +1,73 @@ +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), sObj2: 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); + expect( + instance.object.sObj2, + s == 'Positional' ? isNull : isNotNull, + ); + if (s == 'PositionalAndNamed') expect(instance.object.sObj2!.i, 2); + }); + } + }); + }); + }); +} From d21f8e51362a1e5b44382ee2350026f5fe3989a3 Mon Sep 17 00:00:00 2001 From: Nexushunter Date: Mon, 18 Sep 2023 21:05:49 -0400 Subject: [PATCH 2/4] fix: Add wip fragments --- source_gen/CHANGELOG.md | 3 ++- source_gen/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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/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 From c89dae7fc051c67b91ab23b18aa05f1c8ac4ecc4 Mon Sep 17 00:00:00 2001 From: Nexushunter Date: Fri, 22 Sep 2023 20:19:04 -0400 Subject: [PATCH 3/4] fix: Handle enums --- _test_annotations/lib/test_annotations.dart | 9 ++++- source_gen/lib/src/constants/reviver.dart | 41 +++++++++++++-------- source_gen/test/constants/reviver_test.dart | 13 ++++--- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/_test_annotations/lib/test_annotations.dart b/_test_annotations/lib/test_annotations.dart index d4e9ca17..223e7690 100644 --- a/_test_annotations/lib/test_annotations.dart +++ b/_test_annotations/lib/test_annotations.dart @@ -20,5 +20,12 @@ class SimpleObject { class ComplexObject { final SimpleObject sObj; final SimpleObject? sObj2; - const ComplexObject(this.sObj, {this.sObj2 = null}); + final CustomEnum? cEnum; + const ComplexObject(this.sObj, {this.sObj2 = null, this.cEnum = null}); +} + +enum CustomEnum { + v1, + v2, + v3; } diff --git a/source_gen/lib/src/constants/reviver.dart b/source_gen/lib/src/constants/reviver.dart index 173fd54e..a425f90e 100644 --- a/source_gen/lib/src/constants/reviver.dart +++ b/source_gen/lib/src/constants/reviver.dart @@ -2,6 +2,7 @@ import 'dart:mirrors'; import 'package:analyzer/dart/constant/value.dart'; +import '../../source_gen.dart'; import 'reader.dart'; /// Revives a [ConstantReader] to an instance in memory using Dart mirrors. @@ -83,26 +84,36 @@ class Reviver { } else if (reader.isSymbol) { return reader.symbolValue; } else { - // TODO: Handle enum. + final isEnum = reader.instanceOf(const TypeChecker.fromRuntime(Enum)); 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 = currentMirrorSystem() - .libraries - .entries - .firstWhere( - (element) => - element.key.pathSegments.first == - revivable.source.pathSegments.first, - ) - .value; + final libraryMirror = entries[revivable.source.pathSegments.first]; + if (libraryMirror == null || libraryMirror.simpleName == Symbol.empty) { + throw Exception('Library missing'); + } - final decl = - libraryMirror.declarations[Symbol(revivable.source.fragment)]; + // Determine the declaration being requested. Split on . when an enum is passed in. + var declKey = Symbol(revivable.source.fragment); + if (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) { - // TODO: Throw instead? - return null; + throw Exception('Declaration missing'); + } + + // Handle the enum case + if (decl.isEnum) { + final values = decl.getField(const Symbol('values')).reflectee as List; + return values[reader.objectValue.getField('index')!.toIntValue()!]; } final positionalArguments = revivable.positionalArguments @@ -116,7 +127,7 @@ class Reviver { ), ); - return (decl as ClassMirror) + return decl .newInstance( Symbol(revivable.accessor), positionalArguments, diff --git a/source_gen/test/constants/reviver_test.dart b/source_gen/test/constants/reviver_test.dart index 9160f0b4..5f58ec33 100644 --- a/source_gen/test/constants/reviver_test.dart +++ b/source_gen/test/constants/reviver_test.dart @@ -19,7 +19,7 @@ class TestClassSimple {} @TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1))) class TestClassComplexPositional {} -@TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1), sObj2: SimpleObject(2))) +@TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1), cEnum: CustomEnum.v2)) class TestClassComplexPositionalAndNamed {} '''; test('with simple objects', () async { @@ -60,11 +60,12 @@ class TestClassComplexPositionalAndNamed {} expect(instance.object, isNotNull); expect(instance.object.sObj.i, 1); - expect( - instance.object.sObj2, - s == 'Positional' ? isNull : isNotNull, - ); - if (s == 'PositionalAndNamed') expect(instance.object.sObj2!.i, 2); + expect(instance.object.sObj2, isNull); + + if (s == 'PositionalAndNamed') { + expect(instance.object.cEnum, isNotNull); + expect(instance.object.cEnum, CustomEnum.v2); + } }); } }); From 1f12636841afa37be1d87234e5de11a2bbe33f66 Mon Sep 17 00:00:00 2001 From: Nexushunter Date: Sun, 24 Sep 2023 14:05:54 -0400 Subject: [PATCH 4/4] fix: Apply transformations to type objects --- _test_annotations/lib/test_annotations.dart | 15 +- source_gen/lib/src/constants/reviver.dart | 183 +++++++++++++------- source_gen/test/constants/reviver_test.dart | 21 ++- 3 files changed, 151 insertions(+), 68 deletions(-) diff --git a/_test_annotations/lib/test_annotations.dart b/_test_annotations/lib/test_annotations.dart index 223e7690..448b122d 100644 --- a/_test_annotations/lib/test_annotations.dart +++ b/_test_annotations/lib/test_annotations.dart @@ -1,3 +1,6 @@ +/// A sample library for the Annotations used during testing. +library _test_annotations; + class TestAnnotation { const TestAnnotation(); } @@ -19,9 +22,17 @@ class SimpleObject { class ComplexObject { final SimpleObject sObj; - final SimpleObject? sObj2; final CustomEnum? cEnum; - const ComplexObject(this.sObj, {this.sObj2 = null, this.cEnum = null}); + final Map? cMap; + final List? cList; + final Set? cSet; + const ComplexObject( + this.sObj, { + this.cEnum, + this.cMap, + this.cList, + this.cSet, + }); } enum CustomEnum { diff --git a/source_gen/lib/src/constants/reviver.dart b/source_gen/lib/src/constants/reviver.dart index a425f90e..16cee2a1 100644 --- a/source_gen/lib/src/constants/reviver.dart +++ b/source_gen/lib/src/constants/reviver.dart @@ -2,7 +2,7 @@ import 'dart:mirrors'; import 'package:analyzer/dart/constant/value.dart'; -import '../../source_gen.dart'; +import '../type_checker.dart'; import 'reader.dart'; /// Revives a [ConstantReader] to an instance in memory using Dart mirrors. @@ -52,6 +52,69 @@ class Reviver { /// 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) { @@ -60,80 +123,72 @@ class Reviver { return reader.doubleValue; } else if (reader.isInt) { return reader.intValue; - } else if (reader.isList) { - return reader.listValue - .map((e) => Reviver.fromDartObject(e).toInstance()) - .toList(growable: false); - } else if (reader.isSet) { - return reader.setValue - .map((e) => Reviver.fromDartObject(e).toInstance()) - .toSet(); } else if (reader.isString) { return reader.stringValue; - } else if (reader.isMap) { - return reader.mapValue.map( - (key, value) => MapEntry( - Reviver.fromDartObject(key).toInstance(), - Reviver.fromDartObject(value).toInstance(), - ), - ); - } else if (reader.isLiteral) { - return reader.literalValue; - } else if (reader.isType) { - return reader.typeValue; - } else if (reader.isSymbol) { - return reader.symbolValue; - } else { - final isEnum = reader.instanceOf(const TypeChecker.fromRuntime(Enum)); + } + } - final revivable = reader.revive(); + List get positionalValues => reader + .revive() + .positionalArguments + .map( + (value) => Reviver.fromDartObject(value).toInstance(), + ) + .toList(); - // Flatten the list of libraries - final entries = Map.fromEntries(currentMirrorSystem().libraries.entries) - .map((key, value) => MapEntry(key.pathSegments.first, value)); + Map get namedValues => reader.revive().namedArguments.map( + (key, value) { + final k = Symbol(key); + final v = Reviver.fromDartObject(value).toInstance(); + return MapEntry(k, v); + }, + ); - // Grab the library from the system - final libraryMirror = entries[revivable.source.pathSegments.first]; - if (libraryMirror == null || libraryMirror.simpleName == Symbol.empty) { - throw Exception('Library missing'); - } + ClassMirror get classMirror { + final revivable = reader.revive(); - // Determine the declaration being requested. Split on . when an enum is passed in. - var declKey = Symbol(revivable.source.fragment); - if (isEnum) { - // The accessor when the entry is an enum is the ClassName.value - declKey = Symbol(revivable.accessor.split('.')[0]); - } + // Flatten the list of libraries + final entries = Map.fromEntries(currentMirrorSystem().libraries.entries) + .map((key, value) => MapEntry(key.pathSegments.first, value)); - final decl = libraryMirror.declarations[declKey] as ClassMirror?; - if (decl == null) { - throw Exception('Declaration missing'); - } + // Grab the library from the system + final libraryMirror = entries[revivable.source.pathSegments.first]; + if (libraryMirror == null || libraryMirror.simpleName == Symbol.empty) { + throw Exception('Library missing'); + } - // Handle the enum case - if (decl.isEnum) { - final values = decl.getField(const Symbol('values')).reflectee as List; - return values[reader.objectValue.getField('index')!.toIntValue()!]; - } + // 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; + } - final positionalArguments = revivable.positionalArguments - .map((e) => Reviver.fromDartObject(e).toInstance()) - .toList(growable: false); + List? toTypedList(T t) => reader.listValue + .map((e) => Reviver.fromDartObject(e).toInstance() as T) + .toList() as List?; - final namedArguments = revivable.namedArguments.map( + Map? toTypedMap(KT kt, VT vt) => reader.mapValue.map( (key, value) => MapEntry( - Symbol(key), - Reviver.fromDartObject(value).toInstance(), + Reviver.fromDartObject(key).toInstance() as KT, + Reviver.fromDartObject(value).toInstance() as VT, ), - ); + ) as Map?; - return decl - .newInstance( - Symbol(revivable.accessor), - positionalArguments, - namedArguments, - ) - .reflectee; - } - } + 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/test/constants/reviver_test.dart b/source_gen/test/constants/reviver_test.dart index 5f58ec33..7d27cee2 100644 --- a/source_gen/test/constants/reviver_test.dart +++ b/source_gen/test/constants/reviver_test.dart @@ -19,7 +19,7 @@ class TestClassSimple {} @TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1))) class TestClassComplexPositional {} -@TestAnnotationWithComplexObject(ComplexObject(SimpleObject(1), cEnum: CustomEnum.v2)) +@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 { @@ -60,11 +60,28 @@ class TestClassComplexPositionalAndNamed {} expect(instance.object, isNotNull); expect(instance.object.sObj.i, 1); - expect(instance.object.sObj2, isNull); 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)), + }); } }); }