mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Support generic row classes
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user