From 61b6dd403db53c98002d911813d93a8f43fae584 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 26 Jul 2021 19:48:26 +0200 Subject: [PATCH] Support generic row classes --- .../lib/src/analyzer/custom_row_class.dart | 31 +++++++- .../lib/src/analyzer/dart/table_parser.dart | 6 +- .../analyzer/moor/create_table_reader.dart | 2 +- .../src/analyzer/moor/find_dart_class.dart | 13 +++- moor_generator/lib/src/model/base_entity.dart | 7 +- .../dart/custom_row_classes_test.dart | 71 ++++++++++++++++++- .../moor/custom_row_classes_test.dart | 69 ++++++++++++++++++ moor_generator/test/analyzer/utils.dart | 8 +-- moor_generator/test/utils/test_backend.dart | 7 +- 9 files changed, 193 insertions(+), 21 deletions(-) diff --git a/moor_generator/lib/src/analyzer/custom_row_class.dart b/moor_generator/lib/src/analyzer/custom_row_class.dart index c3849c82f..000297d12 100644 --- a/moor_generator/lib/src/analyzer/custom_row_class.dart +++ b/moor_generator/lib/src/analyzer/custom_row_class.dart @@ -5,9 +5,33 @@ import 'package:analyzer/dart/element/type_provider.dart'; import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/src/analyzer/errors.dart'; +class FoundDartClass { + final ClassElement classElement; + + /// The instantiation of the [classElement], if the found type was a generic + /// typedef. + final List? instantiation; + + FoundDartClass(this.classElement, this.instantiation); +} + ExistingRowClass? validateExistingClass(Iterable columns, - ClassElement desiredClass, String constructor, ErrorSink errors) { - final ctor = desiredClass.getNamedConstructor(constructor); + FoundDartClass dartClass, String constructor, ErrorSink errors) { + final desiredClass = dartClass.classElement; + ConstructorElement? ctor; + + if (dartClass.instantiation != null) { + final instantiation = desiredClass.instantiate( + typeArguments: dartClass.instantiation!, + nullabilitySuffix: NullabilitySuffix.none, + ); + + // If we have an instantation, search the constructor on the type because it + // will report the right parameter types if they're generic. + ctor = instantiation.lookUpConstructor(constructor, desiredClass.library); + } else { + ctor = desiredClass.getNamedConstructor(constructor); + } if (ctor == null) { final msg = constructor == '' @@ -39,7 +63,8 @@ ExistingRowClass? validateExistingClass(Iterable columns, } } - return ExistingRowClass(desiredClass, ctor, columnsToParameter); + return ExistingRowClass(desiredClass, ctor, columnsToParameter, + typeInstantiation: dartClass.instantiation ?? const []); } void _checkType(ParameterElement element, MoorColumn column, ErrorSink errors) { diff --git a/moor_generator/lib/src/analyzer/dart/table_parser.dart b/moor_generator/lib/src/analyzer/dart/table_parser.dart index 1fa09df33..594444088 100644 --- a/moor_generator/lib/src/analyzer/dart/table_parser.dart +++ b/moor_generator/lib/src/analyzer/dart/table_parser.dart @@ -68,7 +68,7 @@ class TableParser { } String name; - ClassElement? existingClass; + FoundDartClass? existingClass; String? constructorInExistingClass; if (dataClassName != null) { @@ -83,8 +83,8 @@ class TableParser { useRowClass.getField('constructor')!.toStringValue()!; if (type is InterfaceType) { - existingClass = type.element; - name = existingClass.name; + existingClass = FoundDartClass(type.element, type.typeArguments); + name = type.element.name; } else { base.step.reportError(ErrorInDartCode( message: 'The @UseRowClass annotation must be used with a class', diff --git a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart index cb3bf06cc..cd6782b85 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -270,7 +270,7 @@ class CreateTableReader { Future _readDartType(String typeIdentifier) async { final foundClass = await findDartClass(step, imports, typeIdentifier); - return foundClass?.instantiate( + return foundClass?.classElement.instantiate( typeArguments: const [], nullabilitySuffix: NullabilitySuffix.none, ); diff --git a/moor_generator/lib/src/analyzer/moor/find_dart_class.dart b/moor_generator/lib/src/analyzer/moor/find_dart_class.dart index d84a1f7c6..e075b24fb 100644 --- a/moor_generator/lib/src/analyzer/moor/find_dart_class.dart +++ b/moor_generator/lib/src/analyzer/moor/find_dart_class.dart @@ -1,9 +1,13 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/src/analyzer/runner/steps.dart'; import 'package:moor_generator/src/backends/backend.dart'; import 'package:sqlparser/sqlparser.dart'; -Future findDartClass( +import '../custom_row_class.dart'; + +/// Resolves a Dart class or generalized typedef pointing towards a Dart class. +Future findDartClass( Step step, List imports, String identifier) async { final dartImports = imports .map((import) => import.importedFile) @@ -20,7 +24,12 @@ Future findDartClass( final foundElement = library.exportNamespace.get(identifier); if (foundElement is ClassElement) { - return foundElement; + return FoundDartClass(foundElement, null); + } else if (foundElement is TypeAliasElement) { + final innerType = foundElement.aliasedType; + if (innerType is InterfaceType) { + return FoundDartClass(innerType.element, innerType.typeArguments); + } } } diff --git a/moor_generator/lib/src/model/base_entity.dart b/moor_generator/lib/src/model/base_entity.dart index 8eee58c07..1cd087417 100644 --- a/moor_generator/lib/src/model/base_entity.dart +++ b/moor_generator/lib/src/model/base_entity.dart @@ -1,4 +1,5 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/moor_generator.dart'; /// Some schema entity found. @@ -54,8 +55,12 @@ abstract class MoorEntityWithResultSet extends MoorSchemaEntity { /// written by users. class ExistingRowClass { final ClassElement targetClass; + + /// The Dart types that should be used to instantiate the [targetClass]. + final List typeInstantiation; final ConstructorElement constructor; final Map mapping; - ExistingRowClass(this.targetClass, this.constructor, this.mapping); + ExistingRowClass(this.targetClass, this.constructor, this.mapping, + {this.typeInstantiation = const []}); } diff --git a/moor_generator/test/analyzer/dart/custom_row_classes_test.dart b/moor_generator/test/analyzer/dart/custom_row_classes_test.dart index 35e25e337..056fee098 100644 --- a/moor_generator/test/analyzer/dart/custom_row_classes_test.dart +++ b/moor_generator/test/analyzer/dart/custom_row_classes_test.dart @@ -1,12 +1,14 @@ -// @dart=2.9 @Tags(['analyzer']) +import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/src/analyzer/errors.dart'; +import 'package:moor_generator/src/analyzer/runner/results.dart'; +import 'package:moor_generator/src/model/base_entity.dart'; import 'package:test/test.dart'; import '../utils.dart'; void main() { - TestState state; + late TestState state; setUpAll(() { state = TestState.withContent({ @@ -72,6 +74,28 @@ class TableClass extends Table { TextColumn get x => text().map(const MyConverter())(); } ''', + 'a|lib/generic.dart': ''' +//@dart=2.13 +import 'package:moor/moor.dart'; + +typedef StringRow = GenericRow; +typedef IntRow = GenericRow; + +class GenericRow { + final T value; + GenericRow(this.value); +} + +@UseRowClass(StringRow) +class StringTable extends Table { + TextColumn get value => text()(); +} + +@UseRowClass(IntRow) +class IntTable extends Table { + IntColumn get value => integer()(); +} + ''' }); }); @@ -128,4 +152,47 @@ class TableClass extends Table { ); }); }); + + test('supports generic row classes', () async { + final file = await state.analyze('package:a/generic.dart'); + expect(file.errors.errors, isEmpty); + + final tables = (file.currentResult as ParsedDartFile).declaredTables; + final stringTable = tables.firstWhere((e) => e.dslName == 'StringTable'); + final intTable = tables.firstWhere((e) => e.dslName == 'IntTable'); + + expect( + stringTable.existingRowClass, + isA() + .having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow') + .having( + (e) => e.typeInstantiation, + 'typeInstantiation', + allOf( + hasLength(1), + anyElement( + isA().having( + (e) => e.isDartCoreString, 'isDartCoreString', isTrue), + ), + ), + ), + ); + + expect( + intTable.existingRowClass, + isA() + .having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow') + .having( + (e) => e.typeInstantiation, + 'typeInstantiation', + allOf( + hasLength(1), + anyElement( + isA() + .having((e) => e.isDartCoreInt, 'isDartCoreInt', isTrue), + ), + ), + ), + ); + }); } diff --git a/moor_generator/test/analyzer/moor/custom_row_classes_test.dart b/moor_generator/test/analyzer/moor/custom_row_classes_test.dart index 1963c7ceb..75978c25a 100644 --- a/moor_generator/test/analyzer/moor/custom_row_classes_test.dart +++ b/moor_generator/test/analyzer/moor/custom_row_classes_test.dart @@ -1,4 +1,5 @@ // @dart=2.9 +import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/src/analyzer/runner/results.dart'; import 'package:test/scaffolding.dart'; @@ -57,4 +58,72 @@ class ExistingForView { expect(existingView.dartTypeName, 'ExistingForView'); expect(existingView.existingRowClass.targetClass.name, 'ExistingForView'); }); + + test('can use generic row classes', () async { + final state = TestState.withContent({ + 'a|lib/generic.dart': ''' +//@dart=2.13 +typedef StringRow = GenericRow; +typedef IntRow = GenericRow; + +class GenericRow { + final T value; + GenericRow(this.value); +} + ''', + 'a|lib/generic.moor': ''' +import 'generic.dart'; + +CREATE TABLE moor_strings ( + value TEXT NOT NULL +) WITH StringRow; + +CREATE TABLE moor_ints ( + value INT NOT NULL +) WITH IntRow; + ''', + }); + addTearDown(state.close); + + final file = await state.analyze('package:a/generic.moor'); + expect(file.errors.errors, isEmpty); + + final tables = (file.currentResult as ParsedMoorFile).declaredTables; + final strings = tables.singleWhere((e) => e.sqlName == 'moor_strings'); + final ints = tables.singleWhere((e) => e.sqlName == 'moor_ints'); + + expect( + strings.existingRowClass, + isA() + .having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow') + .having( + (e) => e.typeInstantiation, + 'typeInstantiation', + allOf( + hasLength(1), + anyElement( + isA().having( + (e) => e.isDartCoreString, 'isDartCoreString', isTrue), + ), + ), + ), + ); + + expect( + ints.existingRowClass, + isA() + .having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow') + .having( + (e) => e.typeInstantiation, + 'typeInstantiation', + allOf( + hasLength(1), + anyElement( + isA() + .having((e) => e.isDartCoreInt, 'isDartCoreInt', isTrue), + ), + ), + ), + ); + }); } diff --git a/moor_generator/test/analyzer/utils.dart b/moor_generator/test/analyzer/utils.dart index 190d92771..3a737b278 100644 --- a/moor_generator/test/analyzer/utils.dart +++ b/moor_generator/test/analyzer/utils.dart @@ -1,4 +1,3 @@ -//@dart=2.9 import 'package:build/build.dart'; import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/src/analyzer/options.dart'; @@ -16,7 +15,7 @@ class TestState { TestState(this.backend, this.session); factory TestState.withContent(Map content, - {MoorOptions options, bool enableAnalyzer = true}) { + {MoorOptions? options, bool enableAnalyzer = true}) { final backend = TestBackend( { for (final entry in content.entries) @@ -62,13 +61,12 @@ class _HasInferredColumnTypes extends CustomMatcher { : super('Select query with inferred columns', 'columns', expected); @override - Object featureValueOf(dynamic actual) { + Object? featureValueOf(dynamic actual) { if (actual is! SqlSelectQuery) { return actual; } - final query = actual as SqlSelectQuery; - final resultSet = query.resultSet; + final resultSet = actual.resultSet; return {for (final column in resultSet.columns) column.name: column.type}; } } diff --git a/moor_generator/test/utils/test_backend.dart b/moor_generator/test/utils/test_backend.dart index 639f5f7c0..de6adc0b7 100644 --- a/moor_generator/test/utils/test_backend.dart +++ b/moor_generator/test/utils/test_backend.dart @@ -1,4 +1,3 @@ -//@dart=2.9 import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; @@ -9,7 +8,7 @@ import 'package:moor_generator/src/backends/backend.dart'; class TestBackend extends Backend { final Map fakeContent; - Resolver _resolver; + late Resolver _resolver; final Completer _initCompleter = Completer(); final Completer _finish = Completer(); @@ -56,14 +55,14 @@ class _TestBackendTask extends BackendTask { final Uri entrypoint; @override - Logger get log => null; + Logger get log => Logger.root; _TestBackendTask(this.backend, this.entrypoint); @override Future readMoor(Uri path) async { await backend._ready; - return backend.fakeContent[AssetId.resolve(path)]; + return backend.fakeContent[AssetId.resolve(path)]!; } @override