Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reviver #679

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions _test_annotations/lib/test_annotations.dart
Original file line number Diff line number Diff line change
@@ -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<String, ComplexObject>? cMap;
final List<ComplexObject>? cList;
final Set<ComplexObject>? cSet;
const ComplexObject(
this.sObj, {
this.cEnum,
this.cMap,
this.cList,
this.cSet,
});
}

enum CustomEnum {
v1,
v2,
v3;
}
3 changes: 2 additions & 1 deletion source_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions source_gen/lib/source_gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
194 changes: 194 additions & 0 deletions source_gen/lib/src/constants/reviver.dart
Original file line number Diff line number Diff line change
@@ -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<T> {
/// const Delegator();
///
/// T call([dynamic]);
/// }
///
/// // Consumer library
/// /// My CustomDelegate callable to be used in a builder
/// class CustomDelegate implements Delegator<ReturnType> {
/// const CustomDelegate();
///
/// @override
/// ReturnType call(Map<String,String> 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<dynamic> get positionalValues => reader
.revive()
.positionalArguments
.map(
(value) => Reviver.fromDartObject(value).toInstance(),
)
.toList();

Map<Symbol, dynamic> 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<T>? toTypedList<T>(T t) => reader.listValue
.map((e) => Reviver.fromDartObject(e).toInstance() as T)
.toList() as List<T>?;

Map<KT, VT>? toTypedMap<KT, VT>(KT kt, VT vt) => reader.mapValue.map(
(key, value) => MapEntry(
Reviver.fromDartObject(key).toInstance() as KT,
Reviver.fromDartObject(value).toInstance() as VT,
),
) as Map<KT, VT>?;

Set<T>? toTypedSet<T>(T t) => reader.setValue
.map((e) => Reviver.fromDartObject(e).toInstance() as T)
.toSet() as Set<T>?;
}
Comment on lines +174 to +188
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to leverage these results in the following stack trace:

dart:collection                                       MapBase.map
package:source_gen/src/constants/reviver.dart 178:68  Reviver.toTypedMap
package:source_gen/src/constants/reviver.dart 93:16   Reviver.toInstance
package:source_gen/src/constants/reviver.dart 142:51  Reviver.namedValues.<fn>
dart:collection                                       MapBase.map
package:source_gen/src/constants/reviver.dart 139:74  Reviver.namedValues
package:source_gen/src/constants/reviver.dart 109:18  Reviver.toInstance
package:source_gen/src/constants/reviver.dart 135:50  Reviver.positionalValues.<fn>
dart:_internal                                        ListIterable.toList
package:source_gen/src/constants/reviver.dart 137:8   Reviver.positionalValues
package:source_gen/src/constants/reviver.dart 108:18  Reviver.toInstance
test/constants/reviver_test.dart 56:38                main.<fn>.<fn>.<fn>.<fn>

This feels wrong. The reason I'm doing this is because there is a limitation of not being able to do:

final t = Reviver.fromDartObject(reader.listValue.first)
              .toInstance()
              .runtimeType;

return reder.listValue.map((e) => Reviver.fromDartObject(e).toInstance() as t).toList();


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;
}
2 changes: 1 addition & 1 deletion source_gen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
91 changes: 91 additions & 0 deletions source_gen/test/constants/reviver_test.dart
Original file line number Diff line number Diff line change
@@ -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: <String,ComplexObject>{'1':ComplexObject(SimpleObject(1)),'2':ComplexObject(SimpleObject(2)),'fred':ComplexObject(SimpleObject(3))}, cList: <ComplexObject>[ComplexObject(SimpleObject(1))], cSet: <ComplexObject>{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<TestAnnotation>());
});

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<TestAnnotationWithComplexObject>());
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>[
ComplexObject(SimpleObject(1)),
]);

expect(instance.object.cMap, isNotNull);
expect(instance.object.cMap!, const <String, ComplexObject>{
'1': ComplexObject(SimpleObject(1)),
'2': ComplexObject(SimpleObject(2)),
'fred': ComplexObject(SimpleObject(3)),
});

expect(instance.object.cSet, isNotNull);
expect(instance.object.cSet!, const <ComplexObject>{
ComplexObject(SimpleObject(1)),
ComplexObject(SimpleObject(2)),
});
}
});
}
});
});
});
}