From 8819245685a2892dda30e446ac1f09719dcc55d2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 18 Nov 2019 13:59:10 +0100 Subject: [PATCH] Resolve Dart expressions in moor files --- moor/pubspec.yaml | 10 +- moor/test/data/tables/converter.dart | 23 +++ moor/test/data/tables/custom_tables.dart | 7 +- moor/test/data/tables/custom_tables.g.dart | 64 +++++++-- moor/test/data/tables/tables.moor | 5 +- moor/test/data/tables/todos.g.dart | 3 +- moor_generator/build.yaml | 16 ++- moor_generator/lib/moor_generator.dart | 7 + .../lib/src/analyzer/dart/column_parser.dart | 2 +- .../lib/src/analyzer/dart/table_parser.dart | 7 - .../analyzer/moor/create_table_reader.dart | 29 +++- .../analyzer/moor/inline_dart_resolver.dart | 45 ------ .../lib/src/analyzer/moor/parser.dart | 7 +- .../lib/src/analyzer/runner/steps.dart | 1 - .../src/analyzer/runner/steps/parse_moor.dart | 5 +- moor_generator/lib/src/backends/backend.dart | 8 +- .../lib/src/backends/build/build_backend.dart | 33 ++++- .../backends/build/preprocess_builder.dart | 101 +++++++++++++ .../src/backends/build/serialized_types.dart | 134 ++++++++++++++++++ .../plugin/backend/plugin_backend.dart | 6 - .../lib/src/model/specified_table.dart | 13 +- .../lib/src/model/used_type_converter.dart | 11 +- .../lib/src/writer/tables/table_writer.dart | 4 +- moor_generator/pubspec.yaml | 6 +- moor_generator/test/utils/test_backend.dart | 6 - 25 files changed, 434 insertions(+), 119 deletions(-) create mode 100644 moor/test/data/tables/converter.dart delete mode 100644 moor_generator/lib/src/analyzer/moor/inline_dart_resolver.dart create mode 100644 moor_generator/lib/src/backends/build/preprocess_builder.dart create mode 100644 moor_generator/lib/src/backends/build/serialized_types.dart diff --git a/moor/pubspec.yaml b/moor/pubspec.yaml index 7bf8cba3..287c04ff 100644 --- a/moor/pubspec.yaml +++ b/moor/pubspec.yaml @@ -27,8 +27,8 @@ dev_dependencies: test: ^1.9.0 mockito: ^4.1.0 -#dependency_overrides: -# moor_generator: -# path: ../moor_generator -# sqlparser: -# path: ../sqlparser \ No newline at end of file +dependency_overrides: + moor_generator: + path: ../moor_generator + sqlparser: + path: ../sqlparser \ No newline at end of file diff --git a/moor/test/data/tables/converter.dart b/moor/test/data/tables/converter.dart new file mode 100644 index 00000000..bebf07eb --- /dev/null +++ b/moor/test/data/tables/converter.dart @@ -0,0 +1,23 @@ +import 'package:moor/moor.dart'; + +enum SyncType { + locallyCreated, + locallyUpdated, + synchronized, +} + +class SyncTypeConverter extends TypeConverter { + const SyncTypeConverter(); + + @override + SyncType mapToDart(int fromDb) { + if (fromDb == null) return null; + + return SyncType.values[fromDb]; + } + + @override + int mapToSql(SyncType value) { + return value?.index; + } +} diff --git a/moor/test/data/tables/custom_tables.dart b/moor/test/data/tables/custom_tables.dart index 75bf5df5..d9b20370 100644 --- a/moor/test/data/tables/custom_tables.dart +++ b/moor/test/data/tables/custom_tables.dart @@ -1,10 +1,15 @@ import 'package:moor/moor.dart'; +import 'converter.dart'; + part 'custom_tables.g.dart'; @UseMoor( include: {'tables.moor'}, - queries: {'writeConfig': 'REPLACE INTO config VALUES (:key, :value)'}, + queries: { + 'writeConfig': 'REPLACE INTO config (config_key, config_value) ' + 'VALUES (:key, :value)' + }, ) class CustomTablesDb extends _$CustomTablesDb { CustomTablesDb(QueryExecutor e) : super(e) { diff --git a/moor/test/data/tables/custom_tables.g.dart b/moor/test/data/tables/custom_tables.g.dart index 3333c734..11f85df6 100644 --- a/moor/test/data/tables/custom_tables.g.dart +++ b/moor/test/data/tables/custom_tables.g.dart @@ -494,16 +494,20 @@ class WithConstraints extends Table class Config extends DataClass implements Insertable { final String configKey; final String configValue; - Config({@required this.configKey, this.configValue}); + final SyncType syncState; + Config({@required this.configKey, this.configValue, this.syncState}); factory Config.fromData(Map data, GeneratedDatabase db, {String prefix}) { final effectivePrefix = prefix ?? ''; final stringType = db.typeSystem.forDartType(); + final intType = db.typeSystem.forDartType(); return Config( configKey: stringType .mapFromDatabaseResponse(data['${effectivePrefix}config_key']), configValue: stringType .mapFromDatabaseResponse(data['${effectivePrefix}config_value']), + syncState: ConfigTable.$converter0.mapToDart(intType + .mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])), ); } factory Config.fromJson(Map json, @@ -511,6 +515,7 @@ class Config extends DataClass implements Insertable { return Config( configKey: serializer.fromJson(json['config_key']), configValue: serializer.fromJson(json['config_value']), + syncState: serializer.fromJson(json['sync_state']), ); } factory Config.fromJsonString(String encodedJson, @@ -523,6 +528,7 @@ class Config extends DataClass implements Insertable { return { 'config_key': serializer.toJson(configKey), 'config_value': serializer.toJson(configValue), + 'sync_state': serializer.toJson(syncState), }; } @@ -535,48 +541,62 @@ class Config extends DataClass implements Insertable { configValue: configValue == null && nullToAbsent ? const Value.absent() : Value(configValue), + syncState: syncState == null && nullToAbsent + ? const Value.absent() + : Value(syncState), ); } - Config copyWith({String configKey, String configValue}) => Config( + Config copyWith({String configKey, String configValue, SyncType syncState}) => + Config( configKey: configKey ?? this.configKey, configValue: configValue ?? this.configValue, + syncState: syncState ?? this.syncState, ); @override String toString() { return (StringBuffer('Config(') ..write('configKey: $configKey, ') - ..write('configValue: $configValue') + ..write('configValue: $configValue, ') + ..write('syncState: $syncState') ..write(')')) .toString(); } @override - int get hashCode => $mrjf($mrjc(configKey.hashCode, configValue.hashCode)); + int get hashCode => $mrjf($mrjc( + configKey.hashCode, $mrjc(configValue.hashCode, syncState.hashCode))); @override bool operator ==(other) => identical(this, other) || (other is Config && other.configKey == this.configKey && - other.configValue == this.configValue); + other.configValue == this.configValue && + other.syncState == this.syncState); } class ConfigCompanion extends UpdateCompanion { final Value configKey; final Value configValue; + final Value syncState; const ConfigCompanion({ this.configKey = const Value.absent(), this.configValue = const Value.absent(), + this.syncState = const Value.absent(), }); ConfigCompanion.insert({ @required String configKey, this.configValue = const Value.absent(), + this.syncState = const Value.absent(), }) : configKey = Value(configKey); ConfigCompanion copyWith( - {Value configKey, Value configValue}) { + {Value configKey, + Value configValue, + Value syncState}) { return ConfigCompanion( configKey: configKey ?? this.configKey, configValue: configValue ?? this.configValue, + syncState: syncState ?? this.syncState, ); } } @@ -603,8 +623,16 @@ class ConfigTable extends Table with TableInfo { $customConstraints: ''); } + final VerificationMeta _syncStateMeta = const VerificationMeta('syncState'); + GeneratedIntColumn _syncState; + GeneratedIntColumn get syncState => _syncState ??= _constructSyncState(); + GeneratedIntColumn _constructSyncState() { + return GeneratedIntColumn('sync_state', $tableName, true, + $customConstraints: ''); + } + @override - List get $columns => [configKey, configValue]; + List get $columns => [configKey, configValue, syncState]; @override ConfigTable get asDslTable => this; @override @@ -627,6 +655,7 @@ class ConfigTable extends Table with TableInfo { } else if (configValue.isRequired && isInserting) { context.missing(_configValueMeta); } + context.handle(_syncStateMeta, const VerificationResult.success()); return context; } @@ -647,6 +676,11 @@ class ConfigTable extends Table with TableInfo { if (d.configValue.present) { map['config_value'] = Variable(d.configValue.value); } + if (d.syncState.present) { + final converter = ConfigTable.$converter0; + map['sync_state'] = + Variable(converter.mapToSql(d.syncState.value)); + } return map; } @@ -655,6 +689,7 @@ class ConfigTable extends Table with TableInfo { return ConfigTable(_db, alias); } + static TypeConverter $converter0 = const SyncTypeConverter(); @override final bool dontWriteConstraints = true; } @@ -916,6 +951,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { return Config( configKey: row.readString('config_key'), configValue: row.readString('config_value'), + syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')), ); } @@ -955,6 +991,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { rowid: row.readInt('rowid'), configKey: row.readString('config_key'), configValue: row.readString('config_value'), + syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')), ); } @@ -968,7 +1005,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { Future writeConfig(String key, String value) { return customInsert( - 'REPLACE INTO config VALUES (:key, :value)', + 'REPLACE INTO config (config_key, config_value) VALUES (:key, :value)', variables: [Variable.withString(key), Variable.withString(value)], updates: {config}, ); @@ -983,19 +1020,24 @@ class ReadRowIdResult { final int rowid; final String configKey; final String configValue; + final SyncType syncState; ReadRowIdResult({ this.rowid, this.configKey, this.configValue, + this.syncState, }); @override - int get hashCode => $mrjf( - $mrjc(rowid.hashCode, $mrjc(configKey.hashCode, configValue.hashCode))); + int get hashCode => $mrjf($mrjc( + rowid.hashCode, + $mrjc(configKey.hashCode, + $mrjc(configValue.hashCode, syncState.hashCode)))); @override bool operator ==(other) => identical(this, other) || (other is ReadRowIdResult && other.rowid == this.rowid && other.configKey == this.configKey && - other.configValue == this.configValue); + other.configValue == this.configValue && + other.syncState == this.syncState); } diff --git a/moor/test/data/tables/tables.moor b/moor/test/data/tables/tables.moor index 972fecb8..7a454929 100644 --- a/moor/test/data/tables/tables.moor +++ b/moor/test/data/tables/tables.moor @@ -1,3 +1,5 @@ +import 'converter.dart'; + CREATE TABLE no_ids ( payload BLOB NOT NULL ) WITHOUT ROWID; @@ -17,7 +19,8 @@ CREATE TABLE with_constraints ( create table config ( config_key TEXT not null primary key, - config_value TEXT + config_value TEXT, + sync_state INTEGER MAPPED BY `const SyncTypeConverter()` ) AS "Config"; CREATE TABLE mytable ( diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 77075f17..f9cdc4d0 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -1151,7 +1151,8 @@ class $TableWithoutPKTable extends TableWithoutPK return $TableWithoutPKTable(_db, alias); } - static CustomConverter $converter0 = const CustomConverter(); + static TypeConverter $converter0 = + const CustomConverter(); } class PureDefault extends DataClass implements Insertable { diff --git a/moor_generator/build.yaml b/moor_generator/build.yaml index 89d07069..86332685 100644 --- a/moor_generator/build.yaml +++ b/moor_generator/build.yaml @@ -1,8 +1,22 @@ builders: + preparing_builder: + import: "package:moor_generator/moor_generator.dart" + builder_factories: ["preparingBuilder"] + build_extensions: {".moor": [".temp.dart", ".dart_in_moor"]} + build_to: cache + auto_apply: dependents + applies_builders: ["moor_generator|moor_cleanup"] + moor_generator: import: "package:moor_generator/moor_generator.dart" builder_factories: ["moorBuilder"] build_extensions: {".dart": [".moor.g.part"]} auto_apply: dependents build_to: cache - applies_builders: ["source_gen|combining_builder"] \ No newline at end of file + required_inputs: [".dart_in_moor"] + applies_builders: ["source_gen|combining_builder"] + +post_process_builders: + moor_cleanup: + import: "package:moor_generator/moor_generator.dart" + builder_factory: "moorCleanup" \ No newline at end of file diff --git a/moor_generator/lib/moor_generator.dart b/moor_generator/lib/moor_generator.dart index e90d6c14..b927d698 100644 --- a/moor_generator/lib/moor_generator.dart +++ b/moor_generator/lib/moor_generator.dart @@ -1,4 +1,11 @@ import 'package:build/build.dart'; import 'package:moor_generator/src/backends/build/moor_builder.dart'; +import 'package:moor_generator/src/backends/build/preprocess_builder.dart'; + +Builder preparingBuilder(BuilderOptions options) => PreprocessBuilder(); Builder moorBuilder(BuilderOptions options) => MoorBuilder(options); + +PostProcessBuilder moorCleanup(BuilderOptions options) { + return const FileDeletingBuilder(['.temp.dart']); +} diff --git a/moor_generator/lib/src/analyzer/dart/column_parser.dart b/moor_generator/lib/src/analyzer/dart/column_parser.dart index b5e8f056..f666f17d 100644 --- a/moor_generator/lib/src/analyzer/dart/column_parser.dart +++ b/moor_generator/lib/src/analyzer/dart/column_parser.dart @@ -174,7 +174,7 @@ class ColumnParser { UsedTypeConverter converter; if (createdTypeConverter != null && typeConverterRuntime != null) { converter = UsedTypeConverter( - expression: createdTypeConverter, + expression: createdTypeConverter.toSource(), mappedType: typeConverterRuntime, sqlType: columnType); } diff --git a/moor_generator/lib/src/analyzer/dart/table_parser.dart b/moor_generator/lib/src/analyzer/dart/table_parser.dart index 8e1ab4cd..f960d472 100644 --- a/moor_generator/lib/src/analyzer/dart/table_parser.dart +++ b/moor_generator/lib/src/analyzer/dart/table_parser.dart @@ -21,13 +21,6 @@ class TableParser { ); table.declaration = TableDeclaration(table, base.step.file, element, null); - var index = 0; - for (var converter in table.converters) { - converter - ..index = index++ - ..table = table; - } - return table; } 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 c307b972..b55950e6 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/src/analyzer/runner/steps.dart'; import 'package:moor_generator/src/analyzer/sql_queries/meta/declarations.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; @@ -12,11 +13,11 @@ import 'package:sqlparser/sqlparser.dart'; class CreateTableReader { /// The AST of this `CREATE TABLE` statement. final CreateTableStatement stmt; - final Step step; + final ParseMoorStep step; CreateTableReader(this.stmt, this.step); - SpecifiedTable extractTable(TypeMapper mapper) { + Future extractTable(TypeMapper mapper) async { final table = SchemaFromCreateTable(moorExtensions: true).read(stmt); final foundColumns = {}; @@ -50,8 +51,9 @@ class CreateTableReader { } if (constraint is MappedBy) { - converter = _readTypeConverter(constraint); - // don't write MAPPED BY constraints when creating the table + converter = await _readTypeConverter(moorType, constraint); + // don't write MAPPED BY constraints when creating the table, they're + // a convenience feature by the compiler continue; } if (constraint is JsonKey) { @@ -121,8 +123,21 @@ class CreateTableReader { TableDeclaration(specifiedTable, step.file, null, table.definition); } - UsedTypeConverter _readTypeConverter(MappedBy mapper) { - // todo we need to somehow parse the dart expression and check types - return null; + Future _readTypeConverter( + ColumnType sqlType, MappedBy mapper) async { + final code = mapper.mapper.dartCode; + final type = await step.task.backend.resolveTypeOf(step.file.uri, code); + + // todo report lint for any of those cases or when resolveTypeOf throws + if (type is! InterfaceType) { + return null; + } + + final interfaceType = type as InterfaceType; + // TypeConverter declares a "D mapToDart(S fromDb);". We need to know D + final typeInDart = interfaceType.getMethod('mapToDart').returnType; + + return UsedTypeConverter( + expression: code, mappedType: typeInDart, sqlType: sqlType); } } diff --git a/moor_generator/lib/src/analyzer/moor/inline_dart_resolver.dart b/moor_generator/lib/src/analyzer/moor/inline_dart_resolver.dart deleted file mode 100644 index 49a30e62..00000000 --- a/moor_generator/lib/src/analyzer/moor/inline_dart_resolver.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:moor_generator/src/analyzer/runner/steps.dart'; - -/// Resolves the type of Dart expressions given as a string. The -/// [importStatements] are used to discover types. -/// -/// The way this works is that we create a fake file for the analyzer. That file -/// has the following content: -/// ``` -/// import 'package:moor/moor.dart'; // always imported -/// // all import statements -/// -/// var expr = $expression; -/// ``` -/// -/// We can then obtain the type of an expression by reading the inferred type -/// of the top-level `expr` variable in that source. -class InlineDartResolver { - final List importStatements = []; - final ParseMoorStep step; - - InlineDartResolver(this.step); - - Future resolveDartTypeOf(String expression) async { - final template = _createDartTemplate(expression); - final unit = await step.task.backend.parseSource(template); - - final declaration = unit.declarations.single as TopLevelVariableDeclaration; - return declaration.variables.variables.single.initializer.staticType; - } - - String _createDartTemplate(String expression) { - final fakeDart = StringBuffer(); - - fakeDart.write("import 'package:moor/moor.dart';\n"); - for (var import in importStatements) { - fakeDart.write("import '$import';\n"); - } - - fakeDart.write('var expr = $expression;\n'); - - return fakeDart.toString(); - } -} diff --git a/moor_generator/lib/src/analyzer/moor/parser.dart b/moor_generator/lib/src/analyzer/moor/parser.dart index 8ac459d4..0245b65d 100644 --- a/moor_generator/lib/src/analyzer/moor/parser.dart +++ b/moor_generator/lib/src/analyzer/moor/parser.dart @@ -11,7 +11,7 @@ class MoorParser { MoorParser(this.step); - Future parseAndAnalyze() { + Future parseAndAnalyze() async { final result = SqlEngine(useMoorExtensions: true).parseMoorFile(step.content); final parsedFile = result.rootNode as MoorFile; @@ -23,7 +23,6 @@ class MoorParser { for (var parsedStmt in parsedFile.statements) { if (parsedStmt is ImportStatement) { final importStmt = parsedStmt; - step.inlineDartResolver.importStatements.add(importStmt.importedFile); importStatements.add(importStmt); } else if (parsedStmt is CreateTableStatement) { createdReaders.add(CreateTableReader(parsedStmt, step)); @@ -43,7 +42,7 @@ class MoorParser { final createdTables = []; final tableDeclarations = {}; for (var reader in createdReaders) { - final table = reader.extractTable(step.mapper); + final table = await reader.extractTable(step.mapper); createdTables.add(table); tableDeclarations[reader.stmt] = table; } @@ -59,6 +58,6 @@ class MoorParser { decl.file = analyzedFile; } - return Future.value(analyzedFile); + return analyzedFile; } } diff --git a/moor_generator/lib/src/analyzer/runner/steps.dart b/moor_generator/lib/src/analyzer/runner/steps.dart index 44ae604b..c00f82fa 100644 --- a/moor_generator/lib/src/analyzer/runner/steps.dart +++ b/moor_generator/lib/src/analyzer/runner/steps.dart @@ -7,7 +7,6 @@ import 'package:moor_generator/src/analyzer/errors.dart'; import 'package:moor_generator/src/analyzer/moor/table_handler.dart'; import 'package:moor_generator/src/analyzer/runner/file_graph.dart'; import 'package:moor_generator/src/analyzer/runner/results.dart'; -import 'package:moor_generator/src/analyzer/moor/inline_dart_resolver.dart'; import 'package:moor_generator/src/analyzer/moor/parser.dart'; import 'package:moor_generator/src/analyzer/sql_queries/sql_parser.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; diff --git a/moor_generator/lib/src/analyzer/runner/steps/parse_moor.dart b/moor_generator/lib/src/analyzer/runner/steps/parse_moor.dart index 58ff2032..923eaf84 100644 --- a/moor_generator/lib/src/analyzer/runner/steps/parse_moor.dart +++ b/moor_generator/lib/src/analyzer/runner/steps/parse_moor.dart @@ -3,11 +3,8 @@ part of '../steps.dart'; class ParseMoorStep extends Step { final String content; final TypeMapper mapper = TypeMapper(); - /* late final */ InlineDartResolver inlineDartResolver; - ParseMoorStep(Task task, FoundFile file, this.content) : super(task, file) { - inlineDartResolver = InlineDartResolver(this); - } + ParseMoorStep(Task task, FoundFile file, this.content) : super(task, file); Future parseFile() { final parser = MoorParser(this); diff --git a/moor_generator/lib/src/backends/backend.dart b/moor_generator/lib/src/backends/backend.dart index ca74f3f7..3afbb4a6 100644 --- a/moor_generator/lib/src/backends/backend.dart +++ b/moor_generator/lib/src/backends/backend.dart @@ -1,5 +1,5 @@ -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:logging/logging.dart'; import 'package:moor_generator/src/analyzer/session.dart'; @@ -27,9 +27,13 @@ abstract class BackendTask { Logger get log; Future resolveDart(Uri uri); - Future parseSource(String dart); + Future readMoor(Uri uri); + Future resolveTypeOf(Uri context, String dartExpression) { + throw UnsupportedError('Resolving dart expressions not supported'); + } + /// Checks whether a file at [uri] exists. Future exists(Uri uri); diff --git a/moor_generator/lib/src/backends/build/build_backend.dart b/moor_generator/lib/src/backends/build/build_backend.dart index 09419fdc..43ebe0f0 100644 --- a/moor_generator/lib/src/backends/build/build_backend.dart +++ b/moor_generator/lib/src/backends/build/build_backend.dart @@ -1,10 +1,13 @@ -import 'package:analyzer/dart/ast/ast.dart'; +import 'dart:convert'; + import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart' hide log; import 'package:build/build.dart' as build show log; import 'package:logging/logging.dart'; import 'package:moor_generator/src/analyzer/runner/file_graph.dart'; import 'package:moor_generator/src/backends/backend.dart'; +import 'package:moor_generator/src/backends/build/serialized_types.dart'; class BuildBackend extends Backend { BuildBackendTask createTask(BuildStep step) { @@ -21,8 +24,10 @@ class BuildBackend extends Backend { class BuildBackendTask extends BackendTask { final BuildStep step; final BuildBackend backend; + final TypeDeserializer typeDeserializer; - BuildBackendTask(this.step, this.backend); + BuildBackendTask(this.step, this.backend) + : typeDeserializer = TypeDeserializer(step); @override Uri get entrypoint => step.inputId.uri; @@ -41,11 +46,6 @@ class BuildBackendTask extends BackendTask { return step.resolver.libraryFor(_resolve(uri)); } - @override - Future parseSource(String dart) async { - return null; - } - @override Logger get log => build.log; @@ -54,6 +54,25 @@ class BuildBackendTask extends BackendTask { return step.canRead(_resolve(uri)); } + @override + Future resolveTypeOf(Uri context, String dartExpression) async { + // we try to detect all calls of resolveTypeOf in an earlier builder and + // prepare the result. See PreprocessBuilder for details + final preparedHelperFile = + _resolve(context).changeExtension('.dart_in_moor'); + + if (!await step.canRead(preparedHelperFile)) { + throw AssetNotFoundException(preparedHelperFile); + } + + final content = await step.readAsString(preparedHelperFile); + final json = jsonDecode(content) as Map; + final serializedType = json[dartExpression] as Map; + + return typeDeserializer + .deserialize(SerializedType.fromJson(serializedType)); + } + Future finish(FoundFile inputFile) async { // the result could be cached if it was calculated in a previous build step. // we need to can canRead so that the build package can calculate the diff --git a/moor_generator/lib/src/backends/build/preprocess_builder.dart b/moor_generator/lib/src/backends/build/preprocess_builder.dart new file mode 100644 index 00000000..c9474b65 --- /dev/null +++ b/moor_generator/lib/src/backends/build/preprocess_builder.dart @@ -0,0 +1,101 @@ +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'; + +/// A support builder that runs before the main moor_generator to parse and +/// resolve inline Dart resources in a moor file. +/// +/// We use this builder to extract and analyze inline Dart expressions from moor +/// files, which are mainly used for type converters. For instance, let's say +/// we had a moor file like this: +/// ``` +/// -- called input.moor +/// import 'package:my_package/converter.dart'; +/// +/// CREATE TABLE users ( +/// preferences TEXT MAPPED BY `const PreferencesConverter()` +/// ); +/// ``` +/// For that file, the [PreprocessBuilder] would generate a `.dart_in_moor` file +/// which contains information about the static type of all expressions in the +/// moor file. The main generator can then read the `.dart_in_moor` file to +/// resolve those expressions. +class PreprocessBuilder extends Builder { + @override + final Map> buildExtensions = const { + '.moor': ['.temp.dart', '.dart_in_moor'], + }; + + @override + FutureOr build(BuildStep buildStep) async { + final input = buildStep.inputId; + final moorFileContent = await buildStep.readAsString(input); + final engine = SqlEngine(useMoorExtensions: true); + + final parsed = engine.parseMoorFile(moorFileContent); + + final dartLexemes = parsed.tokens + .whereType() + .map((token) => token.dartCode) + .toList(); + + if (dartLexemes.isEmpty) return; // nothing to do, no Dart in this moor file + + final importedFiles = parsed.rootNode.allDescendants + .whereType() + .map((stmt) => stmt.importedFile) + .where((import) => import.endsWith('.dart')); + + // to analyze the expressions, generate a fake Dart file that declares each + // expression in a `var`, we can then read the static type. + + final dartBuffer = StringBuffer(); + for (final import in importedFiles) { + dartBuffer.write('import ${asDartLiteral(import)};\n'); + } + + for (var i = 0; i < dartLexemes.length; i++) { + dartBuffer.write('var ${_nameForDartExpr(i)} = ${dartLexemes[i]};\n'); + } + + final tempDartAsset = input.changeExtension('.temp.dart'); + await buildStep.writeAsString(tempDartAsset, dartBuffer.toString()); + + // we can now resolve the library we just wrote + + final createdLibrary = await buildStep.resolver.libraryFor(tempDartAsset); + final resolveResult = await createdLibrary.session + .getResolvedLibraryByElement(createdLibrary); + + final serializer = TypeSerializer(buildStep.resolver); + final codeToType = {}; + + for (var i = 0; i < dartLexemes.length; i++) { + final member = + _findVariableDefinition(_nameForDartExpr(i), createdLibrary); + final node = resolveResult.getElementDeclaration(member).node + as VariableDeclaration; + + final type = node.initializer.staticType; + codeToType[dartLexemes[i]] = await serializer.serialize(type); + } + + final outputAsset = input.changeExtension('.dart_in_moor'); + await buildStep.writeAsString(outputAsset, jsonEncode(codeToType)); + } + + String _nameForDartExpr(int i) => 'expr_$i'; + + TopLevelVariableElement _findVariableDefinition( + String name, LibraryElement element) { + return element.units + .expand((u) => u.topLevelVariables) + .firstWhere((e) => e.name == name); + } +} diff --git a/moor_generator/lib/src/backends/build/serialized_types.dart b/moor_generator/lib/src/backends/build/serialized_types.dart new file mode 100644 index 00000000..62196429 --- /dev/null +++ b/moor_generator/lib/src/backends/build/serialized_types.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +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:source_gen/source_gen.dart'; + +/// A serialized version of a [DartType]. +abstract class SerializedType { + factory SerializedType.fromJson(Map json) { + switch (json['type'] as String) { + case 'interface': + return _SerializedInterfaceType.fromJson(json); + } + throw ArgumentError('Unknown type kind: ${json['type']}'); + } + + SerializedType(); + + Map toJson(); +} + +// todo handle non-interface types, recursive types + +class _SerializedInterfaceType extends SerializedType { + final Uri libraryUri; + final String className; + final List typeArgs; + + _SerializedInterfaceType(this.libraryUri, this.className, this.typeArgs); + + factory _SerializedInterfaceType.fromJson(Map 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)) + .toList(), + ); + } + + @override + Map 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 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 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 _libraryFromUri(Uri uri) async { + if (uri.scheme == 'dart') { + final session = await _obtainSession(); + return session.getLibraryByUri(uri.toString()); + } else { + final library = + await buildStep.resolver.libraryFor(AssetId.resolve(uri.toString())); + _lastSession ??= library?.session; + return library; + } + } + + FutureOr _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 udpate so it + // should be safe + return _libraryFromUri(Uri.parse('package:moor/sqlite_keywords.dart')) + .then((_) => _lastSession); + } + } +} diff --git a/moor_generator/lib/src/backends/plugin/backend/plugin_backend.dart b/moor_generator/lib/src/backends/plugin/backend/plugin_backend.dart index 4a075fb2..b63946ef 100644 --- a/moor_generator/lib/src/backends/plugin/backend/plugin_backend.dart +++ b/moor_generator/lib/src/backends/plugin/backend/plugin_backend.dart @@ -1,4 +1,3 @@ -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:logging/logging.dart'; import 'package:moor_generator/src/backends/backend.dart'; @@ -26,11 +25,6 @@ class PluginTask extends BackendTask { @override final Logger log = Logger.root; - @override - Future parseSource(String dart) { - return null; - } - @override Future readMoor(Uri uri) async { final path = driver.absolutePath(uri, base: entrypoint); diff --git a/moor_generator/lib/src/model/specified_table.dart b/moor_generator/lib/src/model/specified_table.dart index 5baa55d7..4896b0ea 100644 --- a/moor_generator/lib/src/model/specified_table.dart +++ b/moor_generator/lib/src/model/specified_table.dart @@ -86,12 +86,23 @@ class SpecifiedTable { this.overrideWithoutRowId, this.overrideTableConstraints, this.overrideDontWriteConstraints}) - : _overriddenName = overriddenName; + : _overriddenName = overriddenName { + _attachToConverters(); + } /// Finds all type converters used in this tables. Iterable get converters => columns.map((c) => c.typeConverter).where((t) => t != null); + void _attachToConverters() { + var index = 0; + for (var converter in converters) { + converter + ..index = index++ + ..table = this; + } + } + String get displayName { if (isFromSql) { return sqlName; diff --git a/moor_generator/lib/src/model/used_type_converter.dart b/moor_generator/lib/src/model/used_type_converter.dart index 95827d74..9faffd88 100644 --- a/moor_generator/lib/src/model/used_type_converter.dart +++ b/moor_generator/lib/src/model/used_type_converter.dart @@ -1,4 +1,3 @@ -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:meta/meta.dart'; import 'package:moor_generator/src/model/specified_column.dart'; @@ -9,10 +8,10 @@ class UsedTypeConverter { int index; SpecifiedTable table; - /// The [Expression] that will construct the type converter at runtime. The + /// The expression that will construct the type converter at runtime. The /// type converter constructed will map a [mappedType] to the [sqlType] and /// vice-versa. - final Expression expression; + final String expression; /// The type that will be present at runtime. final DartType mappedType; @@ -20,7 +19,11 @@ class UsedTypeConverter { /// The type that will be written to the database. final ColumnType sqlType; - DartType get typeOfConverter => expression.staticType; + /// A suitable typename to store an instance of the type converter used here. + String get displayNameOfConverter { + final sqlDartType = dartTypeNames[sqlType]; + return 'TypeConverter<${mappedType.displayName}, $sqlDartType>'; + } /// Type converters are stored as static fields in the table that created /// them. This will be the field name for this converter. diff --git a/moor_generator/lib/src/writer/tables/table_writer.dart b/moor_generator/lib/src/writer/tables/table_writer.dart index 3c10165a..106fba1b 100644 --- a/moor_generator/lib/src/writer/tables/table_writer.dart +++ b/moor_generator/lib/src/writer/tables/table_writer.dart @@ -75,8 +75,8 @@ class TableWriter { void _writeConvertersAsStaticFields() { for (var converter in table.converters) { - final typeName = converter.typeOfConverter.displayName; - final code = converter.expression.toSource(); + final typeName = converter.displayNameOfConverter; + final code = converter.expression; _buffer..write('static $typeName ${converter.fieldName} = $code;'); } } diff --git a/moor_generator/pubspec.yaml b/moor_generator/pubspec.yaml index 814cf7ff..3ddab5d3 100644 --- a/moor_generator/pubspec.yaml +++ b/moor_generator/pubspec.yaml @@ -9,7 +9,7 @@ authors: maintainer: Simon Binder (@simolus3) environment: - sdk: '>=2.2.0 <3.0.0' + sdk: '>=2.3.0 <3.0.0' dependencies: collection: ^1.14.0 @@ -24,13 +24,15 @@ dependencies: # Dart analysis analyzer: '>=0.36.4 <0.40.0' - analyzer_plugin: '>=0.1.0 <0.3.0' + analyzer_plugin: # '>=0.1.0 <0.3.0' TODO: remove dependency after https://github.com/dart-lang/sdk/pull/39417 + path: /home/simon/programming/dart-sdk/pkg/analyzer_plugin source_span: ^1.5.5 # Build system build: ^1.1.0 build_config: '>=0.3.1 <1.0.0' source_gen: ^0.9.4 + build_resolvers: '>=1.3.0' # we don't use its api, but let's require 1.3.0 to resolve arbitrary assets dev_dependencies: test: ^1.6.0 diff --git a/moor_generator/test/utils/test_backend.dart b/moor_generator/test/utils/test_backend.dart index 62de782c..829f8366 100644 --- a/moor_generator/test/utils/test_backend.dart +++ b/moor_generator/test/utils/test_backend.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:build_test/build_test.dart'; @@ -68,11 +67,6 @@ class _TestBackendTask extends BackendTask { return await backend._resolver.libraryFor(AssetId.resolve(path.toString())); } - @override - Future parseSource(String dart) { - return null; - } - @override Future exists(Uri uri) async { return backend.fakeContent.containsKey(AssetId.resolve(uri.toString()));