Fix resolving nested serialized types

This commit is contained in:
Simon Binder 2021-07-15 17:14:56 +02:00
parent 230a18abdc
commit cea5a9119c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
3 changed files with 24 additions and 196 deletions

View File

@ -8,7 +8,6 @@ import 'package:build/build.dart' as build show log;
import 'package:logging/logging.dart';
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/backends/backend.dart';
import 'package:moor_generator/src/backends/build/serialized_types.dart';
class BuildBackend extends Backend {
final MoorOptions options;
@ -29,10 +28,8 @@ class BuildBackend extends Backend {
class BuildBackendTask extends BackendTask {
final BuildStep step;
final BuildBackend backend;
final TypeDeserializer typeDeserializer;
BuildBackendTask(this.step, this.backend)
: typeDeserializer = TypeDeserializer(step);
BuildBackendTask(this.step, this.backend);
@override
Uri get entrypoint => step.inputId.uri;
@ -76,6 +73,7 @@ class BuildBackendTask extends BackendTask {
// prepare the result. See PreprocessBuilder for details
final preparedHelperFile =
_resolve(context).changeExtension('.dart_in_moor');
final temporaryFile = _resolve(context).changeExtension('.temp.dart');
if (!await step.canRead(preparedHelperFile)) {
throw CannotLoadTypeException('Generated helper file not found. '
@ -83,10 +81,17 @@ class BuildBackendTask extends BackendTask {
}
final content = await step.readAsString(preparedHelperFile);
final json = jsonDecode(content) as Map<String, dynamic>;
final serializedType = json[dartExpression] as Map<String, dynamic>;
final json =
(jsonDecode(content) as Map<String, dynamic>).cast<String, String>();
final fieldName = json[dartExpression];
if (fieldName == null) {
throw CannotLoadTypeException('Generated helper file does not contain '
'$dartExpression!');
}
return typeDeserializer
.deserialize(SerializedType.fromJson(serializedType));
final library = await step.resolver.libraryFor(temporaryFile);
final field = library.units.first.topLevelVariables
.firstWhere((element) => element.name == fieldName);
return field.type;
}
}

View File

@ -2,10 +2,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:moor_generator/src/backends/build/serialized_types.dart';
import 'package:moor_generator/src/utils/string_escaper.dart';
import 'package:sqlparser/sqlparser.dart';
@ -85,8 +82,11 @@ class PreprocessBuilder extends Builder {
final importedDartFiles =
seenFiles.where((asset) => asset.extension == '.dart');
final codeToField = <String, String>{};
// to analyze the expressions, generate a fake Dart file that declares each
// expression in a `var`, we can then read the static type.
// expression in a `var`, we can then read the static type when resolving
// file later.
final dartBuffer = StringBuffer();
for (final import in importedDartFiles) {
@ -95,38 +95,20 @@ class PreprocessBuilder extends Builder {
}
for (var i = 0; i < dartLexemes.length; i++) {
dartBuffer.write('var ${_nameForDartExpr(i)} = ${dartLexemes[i]};\n');
final name = _nameForDartExpr(i);
dartBuffer.write('var $name = ${dartLexemes[i]};\n');
codeToField[dartLexemes[i]] = name;
}
final tempDartAsset = input.changeExtension('.temp.dart');
// Await the file needed to resolve types.
await buildStep.writeAsString(tempDartAsset, dartBuffer.toString());
// we can now resolve the library we just wrote
final createdLibrary = await buildStep.resolver.libraryFor(tempDartAsset);
final serializer = TypeSerializer(buildStep.resolver);
final codeToType = <String, SerializedType>{};
for (var i = 0; i < dartLexemes.length; i++) {
final member =
_findVariableDefinition(_nameForDartExpr(i), createdLibrary);
final node = await buildStep.resolver.astNodeFor(member, resolve: true)
as VariableDeclaration;
final type = node.initializer.staticType;
codeToType[dartLexemes[i]] = await serializer.serialize(type);
}
// And the file mapping Dart expressions onto the variable names here
final outputAsset = input.changeExtension('.dart_in_moor');
await buildStep.writeAsString(outputAsset, jsonEncode(codeToType));
await buildStep.writeAsString(outputAsset, json.encode(codeToField));
}
String _nameForDartExpr(int i) => 'expr_$i';
TopLevelVariableElement _findVariableDefinition(
String name, LibraryElement element) {
return element.units
.expand((u) => u.topLevelVariables)
.firstWhere((e) => e.name == name);
}
}

View File

@ -1,159 +0,0 @@
//@dart=2.9
import 'dart:async';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:moor_generator/src/backends/backend.dart';
import 'package:source_gen/source_gen.dart';
/// A serialized version of a [DartType].
abstract class SerializedType {
factory SerializedType.fromJson(Map<String, dynamic> json) {
switch (json['type'] as String) {
case 'interface':
return _SerializedInterfaceType.fromJson(json);
}
throw ArgumentError('Unknown type kind: ${json['type']}');
}
SerializedType();
Map<String, dynamic> toJson();
}
// todo handle non-interface types, recursive types
class _SerializedInterfaceType extends SerializedType {
final Uri libraryUri;
final String className;
final List<SerializedType> typeArgs;
_SerializedInterfaceType(this.libraryUri, this.className, this.typeArgs);
factory _SerializedInterfaceType.fromJson(Map<String, dynamic> json) {
final serializedTypes = json['type_args'] as List;
return _SerializedInterfaceType(
Uri.parse(json['library'] as String),
json['class_name'] as String,
serializedTypes
.map((raw) => SerializedType.fromJson(raw as Map<String, dynamic>))
.toList(),
);
}
@override
Map<String, dynamic> toJson() {
return {
'type': 'interface',
'library': libraryUri.toString(),
'class_name': className,
'type_args': [for (var type in typeArgs) type.toJson()],
};
}
}
class TypeSerializer {
final Resolver resolver;
TypeSerializer(this.resolver);
Future<SerializedType> serialize(DartType type) async {
if (type is InterfaceType) {
final dartClass = type.element;
Uri uri;
if (dartClass.librarySource.uri.scheme == 'dart') {
uri = dartClass.librarySource.uri;
} else {
uri = (await resolver.assetIdForElement(dartClass)).uri;
}
final serializedArgs =
await Future.wait(type.typeArguments.map(serialize));
return _SerializedInterfaceType(
uri,
dartClass.name,
serializedArgs,
);
} else {
throw UnsupportedError(
"Couldn't serialize $type, we only support interface types");
}
}
}
class TypeDeserializer {
/// The [BuildStep] used to resolve
final BuildStep buildStep;
/// The analysis session used to read libraries from the Dart SDK which can't
/// be obtained via build apis.
AnalysisSession _lastSession;
TypeDeserializer(this.buildStep);
Future<DartType> deserialize(SerializedType type) async {
if (type is _SerializedInterfaceType) {
final library = await _libraryFromUri(type.libraryUri);
final args = await Future.wait(type.typeArgs.map(deserialize));
return LibraryReader(library).findType(type.className).instantiate(
typeArguments: args, nullabilitySuffix: NullabilitySuffix.star);
}
throw AssertionError('Unhandled type: $type');
}
Future<LibraryElement> _libraryFromUri(Uri uri) async {
if (uri.scheme == 'dart') {
var session = await _obtainSession();
// The session could be invalidated by other builders outside of our
// control. There's no better way than to continue fetching a new session
// in that case.
var attempts = 0;
const maxAttempts = 5;
// ignore: literal_only_boolean_expressions
while (true) {
try {
final info = await session.getLibraryByUri2(uri.toString());
if (info is LibraryElementResult) {
return info.element;
} else {
throw NotALibraryException(uri);
}
} on InconsistentAnalysisException {
_lastSession = null; // Invalidate session, then try again
session = await _obtainSession();
attempts++;
if (attempts == maxAttempts) rethrow;
}
}
} else {
final library = await buildStep.resolver.libraryFor(AssetId.resolve(uri));
_lastSession ??= library?.session;
return library;
}
}
FutureOr<AnalysisSession> _obtainSession() {
if (_lastSession != null) {
return _lastSession;
} else {
// resolve bogus library that's not going to change often. We can use the
// session from that library. Technically, this is non-hermetic, but the
// build runner will throw everything away after an SDK update so it
// should be safe
return _libraryFromUri(Uri.parse('package:moor/sqlite_keywords.dart'))
.then((_) => _lastSession);
}
}
}