Support generic row classes

This commit is contained in:
Simon Binder
2021-07-26 19:48:26 +02:00
parent 3bf91ed6c3
commit 61b6dd403d
9 changed files with 193 additions and 21 deletions
@@ -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<DartType>? instantiation;
FoundDartClass(this.classElement, this.instantiation);
}
ExistingRowClass? validateExistingClass(Iterable<MoorColumn> 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<MoorColumn> columns,
}
}
return ExistingRowClass(desiredClass, ctor, columnsToParameter);
return ExistingRowClass(desiredClass, ctor, columnsToParameter,
typeInstantiation: dartClass.instantiation ?? const []);
}
void _checkType(ParameterElement element, MoorColumn column, ErrorSink errors) {
@@ -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',
@@ -270,7 +270,7 @@ class CreateTableReader {
Future<DartType?> _readDartType(String typeIdentifier) async {
final foundClass = await findDartClass(step, imports, typeIdentifier);
return foundClass?.instantiate(
return foundClass?.classElement.instantiate(
typeArguments: const [],
nullabilitySuffix: NullabilitySuffix.none,
);
@@ -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<ClassElement?> findDartClass(
import '../custom_row_class.dart';
/// Resolves a Dart class or generalized typedef pointing towards a Dart class.
Future<FoundDartClass?> findDartClass(
Step step, List<ImportStatement> imports, String identifier) async {
final dartImports = imports
.map((import) => import.importedFile)
@@ -20,7 +24,12 @@ Future<ClassElement?> 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);
}
}
}
@@ -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<DartType> typeInstantiation;
final ConstructorElement constructor;
final Map<MoorColumn, ParameterElement> mapping;
ExistingRowClass(this.targetClass, this.constructor, this.mapping);
ExistingRowClass(this.targetClass, this.constructor, this.mapping,
{this.typeInstantiation = const []});
}
@@ -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<String>;
typedef IntRow = GenericRow<int>;
class GenericRow<T> {
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<ExistingRowClass>()
.having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow')
.having(
(e) => e.typeInstantiation,
'typeInstantiation',
allOf(
hasLength(1),
anyElement(
isA<DartType>().having(
(e) => e.isDartCoreString, 'isDartCoreString', isTrue),
),
),
),
);
expect(
intTable.existingRowClass,
isA<ExistingRowClass>()
.having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow')
.having(
(e) => e.typeInstantiation,
'typeInstantiation',
allOf(
hasLength(1),
anyElement(
isA<DartType>()
.having((e) => e.isDartCoreInt, 'isDartCoreInt', isTrue),
),
),
),
);
});
}
@@ -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<String>;
typedef IntRow = GenericRow<int>;
class GenericRow<T> {
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<ExistingRowClass>()
.having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow')
.having(
(e) => e.typeInstantiation,
'typeInstantiation',
allOf(
hasLength(1),
anyElement(
isA<DartType>().having(
(e) => e.isDartCoreString, 'isDartCoreString', isTrue),
),
),
),
);
expect(
ints.existingRowClass,
isA<ExistingRowClass>()
.having((e) => e.targetClass.name, 'targetClass.name', 'GenericRow')
.having(
(e) => e.typeInstantiation,
'typeInstantiation',
allOf(
hasLength(1),
anyElement(
isA<DartType>()
.having((e) => e.isDartCoreInt, 'isDartCoreInt', isTrue),
),
),
),
);
});
}
+3 -5
View File
@@ -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<String, String> 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};
}
}
+3 -4
View File
@@ -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<AssetId, String> 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<String> readMoor(Uri path) async {
await backend._ready;
return backend.fakeContent[AssetId.resolve(path)];
return backend.fakeContent[AssetId.resolve(path)]!;
}
@override