From a6caae9d8f0c2bca2582ff0241f1317c49b583b3 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 17 Sep 2022 00:16:53 +0200 Subject: [PATCH] Parse entrypoints in new analyzer --- drift/lib/src/drift_dev_helper.dart | 2 +- .../src/analysis/resolver/dart/accessor.dart | 182 ++++++++++++++++++ .../src/analysis/resolver/dart/helper.dart | 21 +- .../lib/src/analysis/resolver/discover.dart | 20 +- .../analysis/resolver/intermediate_state.dart | 11 ++ .../lib/src/analysis/resolver/resolver.dart | 23 +++ drift_dev/lib/src/analysis/results/dart.dart | 5 + .../lib/src/analysis/results/database.dart | 103 ++++++++++ .../lib/src/analysis/results/results.dart | 1 + drift_dev/lib/src/analysis/serializer.dart | 35 ++++ .../analysis/results/database.g.dart | 18 ++ 11 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 drift_dev/lib/src/analysis/resolver/dart/accessor.dart create mode 100644 drift_dev/lib/src/analysis/results/database.dart create mode 100644 drift_dev/lib/src/generated/analysis/results/database.g.dart diff --git a/drift/lib/src/drift_dev_helper.dart b/drift/lib/src/drift_dev_helper.dart index ddcc4aa7..619e6fa2 100644 --- a/drift/lib/src/drift_dev_helper.dart +++ b/drift/lib/src/drift_dev_helper.dart @@ -1,4 +1,4 @@ // This field is analyzed by drift_dev to easily obtain common types. export 'runtime/types/converters.dart' show TypeConverter, JsonTypeConverter; -export 'dsl/dsl.dart' show Table; +export 'dsl/dsl.dart' show Table, DriftDatabase, DriftAccessor; diff --git a/drift_dev/lib/src/analysis/resolver/dart/accessor.dart b/drift_dev/lib/src/analysis/resolver/dart/accessor.dart new file mode 100644 index 00000000..46001303 --- /dev/null +++ b/drift_dev/lib/src/analysis/resolver/dart/accessor.dart @@ -0,0 +1,182 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +import '../../driver/error.dart'; +import '../../results/results.dart'; +import '../intermediate_state.dart'; +import '../resolver.dart'; +import 'helper.dart'; + +class DartAccessorResolver + extends LocalElementResolver { + DartAccessorResolver( + super.file, super.discovered, super.resolver, super.state); + + @override + Future resolve() async { + final tables = []; + final views = []; + final includes = []; + final queries = []; + + final annotation = discovered.annotation; + final element = discovered.dartElement; + + final rawTables = annotation.getField('tables')!.toListValue()!; + for (final tableType in rawTables) { + final dartType = tableType.toTypeValue(); + + if (dartType is! InterfaceType) { + reportError( + DriftAnalysisError.forDartElement( + element, + 'Could not read table from ' + '`${dartType?.getDisplayString(withNullability: true)}`, it needs ' + 'to reference a table class.', + ), + ); + continue; + } + + final table = await resolveDartReferenceOrReportError( + dartType.element2, + (msg) => DriftAnalysisError.forDartElement(element, msg)); + if (table != null) { + tables.add(table); + } + } + + final rawViews = annotation.getField('views')!.toListValue()!; + for (final viewType in rawViews) { + final dartType = viewType.toTypeValue(); + + if (dartType is! InterfaceType) { + reportError( + DriftAnalysisError.forDartElement( + element, + 'Could not read view from ' + '`${dartType?.getDisplayString(withNullability: true)}`, it needs ' + 'to reference a view class.', + ), + ); + continue; + } + + final view = await resolveDartReferenceOrReportError( + dartType.element2, + (msg) => DriftAnalysisError.forDartElement(element, msg)); + if (view != null) { + views.add(view); + } + } + + for (final include in annotation.getField('include')!.toSetValue()!) { + final value = include.toStringValue()!; + final import = Uri.tryParse(value); + + if (import == null) { + reportError( + DriftAnalysisError.forDartElement( + element, '`$value` is not a valid URI to include'), + ); + } else { + includes.add(import); + } + } + + final rawQueries = annotation.getField('queries')!.toMapValue()!; + rawQueries.forEach((key, value) { + final keyStr = key!.toStringValue()!; + final valueStr = value!.toStringValue()!; + + queries.add(QueryOnAccessor(keyStr, valueStr)); + }); + + final declaration = DriftDeclaration.dartElement(element); + if (discovered.isDatabase) { + final accessorTypes = []; + final rawDaos = annotation.getField('daos')!.toListValue()!; + for (final value in rawDaos) { + final type = value.toTypeValue()!; + + if (type is! InterfaceType) { + reportError( + DriftAnalysisError.forDartElement( + element, + 'Could not read referenced DAO from ' + '`$type?.getDisplayString(withNullability: true)}`, it needs ' + 'to reference an accessor class.', + ), + ); + continue; + } + + accessorTypes.add(AnnotatedDartCode.type(type)); + } + + return DriftDatabase( + id: discovered.ownId, + declaration: declaration, + declaredTables: tables, + declaredViews: views, + declaredIncludes: includes, + declaredQueries: queries, + schemaVersion: await _readSchemaVersion(), + accessorTypes: accessorTypes, + ); + } else { + final dbType = element.allSupertypes + .firstWhereOrNull((i) => i.element2.name == 'DatabaseAccessor'); + + // inherits from DatabaseAccessor, we want to know which T + + final dbImpl = dbType?.typeArguments.single ?? + element.library.typeProvider.dynamicType; + if (dbImpl.isDynamic) { + reportError(DriftAnalysisError.forDartElement( + element, + 'This class must inherit from DatabaseAccessor, where T is an ' + 'actual type of a database.', + )); + } + + return DatabaseAccessor( + id: discovered.ownId, + declaration: declaration, + declaredTables: tables, + declaredViews: views, + declaredIncludes: includes, + declaredQueries: queries, + databaseClass: AnnotatedDartCode.type(dbImpl), + ); + } + } + + Future _readSchemaVersion() async { + final element = + discovered.dartElement.thisType.getGetter('schemaVersion')?.variable; + if (element == null) return null; + + try { + if (element.isSynthetic) { + // Getter, read from `=>` body if possible. + final expr = returnExpressionOfMethod(await resolver.driver.backend + .loadElementDeclaration(element.getter!) as MethodDeclaration); + if (expr is IntegerLiteral) { + return expr.value; + } + } else { + final astField = await resolver.driver.backend + .loadElementDeclaration(element) as VariableDeclaration; + if (astField.initializer is IntegerLiteral) { + return (astField.initializer as IntegerLiteral).value; + } + } + } catch (e, s) { + resolver.driver.backend.log + .warning('Could not read schemaVersion from $element', e, s); + } + return null; + } +} diff --git a/drift_dev/lib/src/analysis/resolver/dart/helper.dart b/drift_dev/lib/src/analysis/resolver/dart/helper.dart index 22b40387..ba37d00f 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/helper.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/helper.dart @@ -9,6 +9,8 @@ import '../../driver/driver.dart'; class KnownDriftTypes { final ClassElement tableElement; final InterfaceType tableType; + final InterfaceType driftDatabase; + final InterfaceType driftAccessor; final InterfaceElement typeConverter; final InterfaceElement jsonTypeConverter; @@ -17,6 +19,8 @@ class KnownDriftTypes { this.tableType, this.typeConverter, this.jsonTypeConverter, + this.driftDatabase, + this.driftAccessor, ); /// Constructs the set of known drift types from a helper library, which is @@ -24,15 +28,16 @@ class KnownDriftTypes { factory KnownDriftTypes._fromLibrary(LibraryElement helper) { final exportNamespace = helper.exportNamespace; final tableElement = exportNamespace.get('Table') as ClassElement; + final dbElement = exportNamespace.get('DriftDatabase') as ClassElement; + final daoElement = exportNamespace.get('DriftAccessor') as ClassElement; return KnownDriftTypes._( tableElement, - tableElement.instantiate( - typeArguments: const [], - nullabilitySuffix: NullabilitySuffix.none, - ), + tableElement.defaultInstantiation, exportNamespace.get('TypeConverter') as InterfaceElement, exportNamespace.get('JsonTypeConverter') as InterfaceElement, + dbElement.defaultInstantiation, + daoElement.defaultInstantiation, ); } @@ -65,8 +70,7 @@ class KnownDriftTypes { } } -Expression? returnExpressionOfMethod(MethodDeclaration method, - {bool reportErrorOnFailure = true}) { +Expression? returnExpressionOfMethod(MethodDeclaration method) { final body = method.body; if (body is! ExpressionFunctionBody) { @@ -131,6 +135,11 @@ extension IsFromDrift on Element { } } +extension on InterfaceElement { + InterfaceType get defaultInstantiation => instantiate( + typeArguments: const [], nullabilitySuffix: NullabilitySuffix.none); +} + extension TypeUtils on DartType { String? get nameIfInterfaceType { final $this = this; diff --git a/drift_dev/lib/src/analysis/resolver/discover.dart b/drift_dev/lib/src/analysis/resolver/discover.dart index a3d935ef..91c2a380 100644 --- a/drift_dev/lib/src/analysis/resolver/discover.dart +++ b/drift_dev/lib/src/analysis/resolver/discover.dart @@ -1,6 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart' as dart; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/visitor.dart'; +import 'package:drift/drift.dart'; import 'package:recase/recase.dart'; import 'package:source_gen/source_gen.dart'; import 'package:sqlparser/sqlparser.dart' hide AnalysisError; @@ -139,7 +140,8 @@ class DiscoverStep { class _FindDartElements extends RecursiveElementVisitor { final DiscoverStep _discoverStep; final LibraryElement _library; - final TypeChecker _isTable; + + final TypeChecker _isTable, _isDatabase, _isDao; final List> _pendingWork = []; @@ -148,7 +150,9 @@ class _FindDartElements extends RecursiveElementVisitor { _FindDartElements( this._discoverStep, this._library, KnownDriftTypes knownTypes) - : _isTable = TypeChecker.fromStatic(knownTypes.tableType); + : _isTable = TypeChecker.fromStatic(knownTypes.tableType), + _isDatabase = TypeChecker.fromStatic(knownTypes.driftDatabase), + _isDao = TypeChecker.fromStatic(knownTypes.driftAccessor); Future find() async { visitLibraryElement(_library); @@ -164,6 +168,18 @@ class _FindDartElements extends RecursiveElementVisitor { found.add(DiscoveredDartTable(id, element)); })); + } else { + // Check if this class declares a database or a database accessor. + + final firstDb = _isDatabase.firstAnnotationOf(element); + final firstDao = _isDao.firstAnnotationOf(element); + final id = _discoverStep._id(element.name); + + if (firstDb != null) { + found.add(DiscoveredBaseAccessor(id, element, firstDb, true)); + } else if (firstDao != null) { + found.add(DiscoveredBaseAccessor(id, element, firstDao, false)); + } } super.visitClassElement(element); diff --git a/drift_dev/lib/src/analysis/resolver/intermediate_state.dart b/drift_dev/lib/src/analysis/resolver/intermediate_state.dart index 26f7fa71..f89a5ba6 100644 --- a/drift_dev/lib/src/analysis/resolver/intermediate_state.dart +++ b/drift_dev/lib/src/analysis/resolver/intermediate_state.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:sqlparser/sqlparser.dart'; @@ -25,3 +26,13 @@ abstract class DiscoveredDartElement class DiscoveredDartTable extends DiscoveredDartElement { DiscoveredDartTable(super.ownId, super.dartElement); } + +class DiscoveredBaseAccessor extends DiscoveredDartElement { + final bool isDatabase; + final DartObject annotation; + + bool get isAccessor => !isDatabase; + + DiscoveredBaseAccessor( + super.ownId, super.dartElement, this.annotation, this.isDatabase); +} diff --git a/drift_dev/lib/src/analysis/resolver/resolver.dart b/drift_dev/lib/src/analysis/resolver/resolver.dart index e755a5c6..4a963b1b 100644 --- a/drift_dev/lib/src/analysis/resolver/resolver.dart +++ b/drift_dev/lib/src/analysis/resolver/resolver.dart @@ -6,8 +6,10 @@ import '../driver/error.dart'; import '../driver/state.dart'; import '../results/element.dart'; +import 'dart/accessor.dart' as dart_accessor; import 'dart/table.dart' as dart_table; import 'drift/index.dart' as drift_index; +import 'drift/query.dart' as drift_query; import 'drift/table.dart' as drift_table; import 'drift/trigger.dart' as drift_trigger; import 'drift/view.dart' as drift_view; @@ -35,6 +37,9 @@ class DriftResolver { } else if (discovered is DiscoveredDriftIndex) { resolver = drift_index.DriftIndexResolver( fileState, discovered, this, elementState); + } else if (discovered is DiscoveredDriftStatement) { + resolver = drift_query.DriftQueryResolver( + fileState, discovered, this, elementState); } else if (discovered is DiscoveredDriftTrigger) { resolver = drift_trigger.DriftTriggerResolver( fileState, discovered, this, elementState); @@ -44,6 +49,9 @@ class DriftResolver { } else if (discovered is DiscoveredDartTable) { resolver = dart_table.DartTableResolver( fileState, discovered, this, elementState); + } else if (discovered is DiscoveredBaseAccessor) { + resolver = dart_accessor.DartAccessorResolver( + fileState, discovered, this, elementState); } else { throw UnimplementedError('TODO: Handle $discovered'); } @@ -178,7 +186,22 @@ abstract class LocalElementResolver { DriftAnalysisError Function(String msg) createError, ) async { final result = await resolver.resolveReference(discovered.ownId, reference); + return _handleReferenceResult(result, createError); + } + Future resolveDartReferenceOrReportError( + Element reference, + DriftAnalysisError Function(String msg) createError, + ) async { + final result = + await resolver.resolveDartReference(discovered.ownId, reference); + return _handleReferenceResult(result, createError); + } + + E? _handleReferenceResult( + ResolveReferencedElementResult result, + DriftAnalysisError Function(String msg) createError, + ) { if (result is ResolvedReferenceFound) { final element = result.element; if (element is E) { diff --git a/drift_dev/lib/src/analysis/results/dart.dart b/drift_dev/lib/src/analysis/results/dart.dart index a7d54d3c..1cd31b77 100644 --- a/drift_dev/lib/src/analysis/results/dart.dart +++ b/drift_dev/lib/src/analysis/results/dart.dart @@ -157,6 +157,11 @@ class _AddFromDartType extends TypeVisitor { } } + @override + void visitRecordType(RecordType type) { + throw UnsupportedError('RecordType to Dart source code'); + } + @override void visitDynamicType(DynamicType type) { _builder.addText('dynamic'); diff --git a/drift_dev/lib/src/analysis/results/database.dart b/drift_dev/lib/src/analysis/results/database.dart new file mode 100644 index 00000000..93a68e70 --- /dev/null +++ b/drift_dev/lib/src/analysis/results/database.dart @@ -0,0 +1,103 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'dart.dart'; +import 'element.dart'; +import 'table.dart'; +import 'query.dart'; +import 'view.dart'; + +part '../../generated/analysis/results/database.g.dart'; + +/// Abstract base class for databases and DAO declarations. +abstract class BaseDriftAccessor extends DriftElement { + /// All tables that have been declared on this accessor directly. + /// + /// This contains the `tables` field from a `DriftDatabase` or `DriftAccessor` + /// annotation, but not tables that are declared in imported files. + final List declaredTables; + + /// All views that have been declared on this accessor directly. + /// + /// This contains the `views` field from a `DriftDatabase` or `DriftAccessor` + /// annotation, but not views that are declared in imported files. + final List declaredViews; + + /// The `includes` field from the annotation. + final List declaredIncludes; + + /// All queries declared directly in the annotation. + final List declaredQueries; + + BaseDriftAccessor({ + required DriftElementId id, + required DriftDeclaration declaration, + required this.declaredTables, + required this.declaredViews, + required this.declaredIncludes, + required this.declaredQueries, + }) : super(id, declaration); + + @override + Iterable get references => [ + // todo: Track dependencies on includes somehow + ...declaredTables, + ...declaredViews, + ]; +} + +/// A database, declared via a `DriftDatabase` annotation on a Dart class. +class DriftDatabase extends BaseDriftAccessor { + /// If the source database class overrides `schemaVersion` and returns a + /// simple integer literal, stores that version. + /// + /// This is optionally used by the migration tooling to store the schema in a + /// versioned file. + final int? schemaVersion; + + final List accessorTypes; + + DriftDatabase({ + required super.id, + required super.declaration, + required super.declaredTables, + required super.declaredViews, + required super.declaredIncludes, + required super.declaredQueries, + this.schemaVersion, + this.accessorTypes = const [], + }); +} + +/// A Dart class with a similar API to a database, providing a view over a +/// subset of tables. +class DatabaseAccessor extends BaseDriftAccessor { + /// The database class this dao belongs to. + final AnnotatedDartCode databaseClass; + + DatabaseAccessor({ + required super.id, + required super.declaration, + required super.declaredTables, + required super.declaredViews, + required super.declaredIncludes, + required super.declaredQueries, + required this.databaseClass, + }); +} + +/// A query defined on a [BaseDriftAccessor]. +/// +/// Similar to a [DefinedSqlQuery] defined in a `.drift` file, most of the SQL +/// analysis happens during code generation because intermediate state is hard +/// to serialize and there are little benefits of analyzing queries early. +@JsonSerializable() +class QueryOnAccessor { + final String name; + final String sql; + + QueryOnAccessor(this.name, this.sql); + + factory QueryOnAccessor.fromJson(Map json) => _$QueryOnAccessorFromJson(json); + + Map toJson() => _$QueryOnAccessorToJson(this); +} diff --git a/drift_dev/lib/src/analysis/results/results.dart b/drift_dev/lib/src/analysis/results/results.dart index 204e9d2c..c977dc0d 100644 --- a/drift_dev/lib/src/analysis/results/results.dart +++ b/drift_dev/lib/src/analysis/results/results.dart @@ -1,5 +1,6 @@ export 'column.dart'; export 'dart.dart'; +export 'database.dart'; export 'element.dart'; export 'index.dart'; export 'query.dart'; diff --git a/drift_dev/lib/src/analysis/serializer.dart b/drift_dev/lib/src/analysis/serializer.dart index cf60ee08..33eb43a5 100644 --- a/drift_dev/lib/src/analysis/serializer.dart +++ b/drift_dev/lib/src/analysis/serializer.dart @@ -88,6 +88,36 @@ class ElementSerializer { 'name_of_row_class': element.nameOfRowClass, 'source': serializedSource, }; + } else if (element is BaseDriftAccessor) { + String type; + + if (element is DriftDatabase) { + type = 'database'; + } else { + type = 'dao'; + } + + additionalInformation = { + 'type': type, + 'tables': [ + for (final table in element.declaredTables) + _serializeElementReference(table), + ], + 'views': [ + for (final view in element.declaredViews) + _serializeElementReference(view), + ], + 'includes': [ + for (final include in element.declaredIncludes) include.toString() + ], + 'queries': element.declaredQueries, + if (element is DatabaseAccessor) + 'database': element.databaseClass.toJson(), + if (element is DriftDatabase) ...{ + 'schema_version': element.schemaVersion, + 'daos': element.accessorTypes, + } + }; } else { throw UnimplementedError('Unknown element $element'); } @@ -207,6 +237,11 @@ class _DartTypeSerializer extends TypeVisitor> { }; } + @override + Map visitRecordType(RecordType type) { + throw UnsupportedError('Not yet supported: Record type serialization'); + } + @override Map visitNeverType(NeverType type) { return _simple('Never', type.nullabilitySuffix); diff --git a/drift_dev/lib/src/generated/analysis/results/database.g.dart b/drift_dev/lib/src/generated/analysis/results/database.g.dart new file mode 100644 index 00000000..d9d4a2a1 --- /dev/null +++ b/drift_dev/lib/src/generated/analysis/results/database.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../../../analysis/results/database.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +QueryOnAccessor _$QueryOnAccessorFromJson(Map json) => QueryOnAccessor( + json['name'] as String, + json['sql'] as String, + ); + +Map _$QueryOnAccessorToJson(QueryOnAccessor instance) => + { + 'name': instance.name, + 'sql': instance.sql, + };