Start with some refactoring in the generator

The idea is that we have a setup that let's us use multiple backends (build, analyzer plugin, standalone) with maximum code sharing.
This commit is contained in:
Simon Binder 2019-08-30 23:09:22 +02:00
parent 3cb00a4b31
commit 23fca61961
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
12 changed files with 384 additions and 16 deletions

View File

@ -8,7 +8,6 @@
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock

View File

@ -1,17 +1,4 @@
import 'package:build/build.dart';
import 'package:moor_generator/src/dao_generator.dart';
import 'package:moor_generator/src/state/options.dart';
import 'package:source_gen/source_gen.dart';
import 'package:moor_generator/src/moor_generator.dart';
import 'package:moor_generator/src/backends/build/moor_builder.dart';
Builder moorBuilder(BuilderOptions options) {
final parsedOptions = MoorOptions.fromBuilder(options.config);
return SharedPartBuilder(
[
MoorGenerator(parsedOptions),
DaoGenerator(parsedOptions),
],
'moor',
);
}
Builder moorBuilder(BuilderOptions options) => MoorBuilder(options);

View File

@ -0,0 +1,45 @@
part of 'parser.dart';
const String startInt = 'integer';
const String startString = 'text';
const String startBool = 'boolean';
const String startDateTime = 'dateTime';
const String startBlob = 'blob';
const String startReal = 'real';
const Set<String> starters = {
startInt,
startString,
startBool,
startDateTime,
startBlob,
startReal,
};
const String _methodNamed = 'named';
const String _methodReferences = 'references';
const String _methodAutoIncrement = 'autoIncrement';
const String _methodWithLength = 'withLength';
const String _methodNullable = 'nullable';
const String _methodCustomConstraint = 'customConstraint';
const String _methodDefault = 'withDefault';
const String _methodMap = 'map';
/// Parses a single column defined in a Dart table. These columns are a chain
/// or [MethodInvocation]s. An example getter might look like this:
/// ```dart
/// IntColumn get id => integer().autoIncrement()();
/// ```
/// The last call `()` is a [FunctionExpressionInvocation], the entries for
/// before that (in this case `autoIncrement()` and `integer()` are a)
/// [MethodInvocation]. We work our way through that syntax until we hit a
/// method that starts the chain (contained in [starters]). By visiting all
/// the invocations on our way, we can extract the constraint for the column
/// (e.g. its name, whether it has auto increment, is a primary key and so on).
class ColumnParser {
final MoorDartParser base;
ColumnParser(this.base);
SpecifiedColumn parse(MethodDeclaration getter, MethodElement element) {}
}

View File

@ -0,0 +1,40 @@
import 'package:analyzer/dart/analysis/results.dart';
import 'package:meta/meta.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/session.dart';
import 'package:moor_generator/src/model/specified_column.dart';
part 'column_parser.dart';
class MoorDartParser {
final DartTask task;
MoorDartParser(this.task);
@visibleForTesting
Expression returnExpressionOfMethod(MethodDeclaration method) {
final body = method.body;
if (!(body is ExpressionFunctionBody)) {
task.reportError(ErrorInDartCode(
affectedElement: method.declaredElement,
severity: Severity.criticalError,
message:
'This method must have an expression body (user => instead of {return ...})',
));
return null;
}
return (method.body as ExpressionFunctionBody).expression;
}
Future<ElementDeclarationResult> loadElementDeclaration(
Element element) async {
final resolvedLibrary = await element.library.session
.getResolvedLibraryByElement(element.library);
return resolvedLibrary.getElementDeclaration(element);
}
}

View File

@ -0,0 +1,44 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:collection/collection.dart';
/// Base class for errors that can be presented to an user.
class MoorError {
final Severity severity;
MoorError(this.severity);
}
class ErrorInDartCode extends MoorError {
final String message;
final Element affectedElement;
ErrorInDartCode(
{this.message,
this.affectedElement,
Severity severity = Severity.warning})
: super(severity);
}
class ErrorSink {
final List<MoorError> _errors = [];
UnmodifiableListView<MoorError> get errors => UnmodifiableListView(_errors);
void report(MoorError error) {
_errors.add(error);
}
}
enum Severity {
/// A severe error. We might not be able to generate correct or consistent
/// code when errors with these severity are present.
criticalError,
/// An error. The generated code won't have major problems, but might cause
/// runtime errors. For instance, this is used when we get sql that has
/// semantic errors.
error,
warning,
info,
hint
}

View File

@ -0,0 +1,23 @@
import 'package:analyzer/dart/element/element.dart';
/// Inputs coming from an external system (such as the analyzer, the build
/// package, etc.) that will be further analyzed by moor.
abstract class Input {
final String path;
Input(this.path);
}
/// Input for Dart files that have already been analyzed.
class DartInput extends Input {
final LibraryElement library;
DartInput(String path, this.library) : super(path);
}
/// Input for a `.moor` file
class MoorInput extends Input {
final String content;
MoorInput(String path, this.content) : super(path);
}

View File

@ -0,0 +1,27 @@
import 'package:meta/meta.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/model/specified_dao.dart';
import 'package:moor_generator/src/model/specified_database.dart';
import 'package:moor_generator/src/model/specified_table.dart';
abstract class ParsedFile {}
class ParsedDartFile extends ParsedFile {
final LibraryElement library;
final List<SpecifiedTable> declaredTables;
final List<SpecifiedDao> declaredDaos;
final List<SpecifiedDatabase> declaredDatabases;
ParsedDartFile(
{@required this.library,
this.declaredTables = const [],
this.declaredDaos = const [],
this.declaredDatabases = const []});
}
class ParsedMoorFile extends ParsedFile {
final List<SpecifiedTable> declaredTables;
ParsedMoorFile(this.declaredTables);
}

View File

@ -0,0 +1,57 @@
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/results.dart';
import 'package:moor_generator/src/backends/backend.dart';
/// Will store cached data about files that have already been analyzed.
class MoorSession {
MoorSession();
Future<DartTask> startDartTask(BackendTask backendTask) async {
final library = await backendTask.resolveDart(backendTask.entrypoint);
return DartTask(this, backendTask, library);
}
}
/// Used to parse and analyze a single file.
abstract class FileTask<R extends ParsedFile> {
final BackendTask backendTask;
final MoorSession session;
final ErrorSink errors = ErrorSink();
FileTask(this.backendTask, this.session);
void reportError(MoorError error) => errors.report(error);
FutureOr<R> compute();
}
/// Session used to parse a Dart file and extract table information.
class DartTask extends FileTask<ParsedDartFile> {
final LibraryElement library;
DartTask(MoorSession session, BackendTask task, this.library)
: super(task, session);
@override
FutureOr<ParsedDartFile> compute() {
// TODO: implement compute
return null;
}
}
class MoorTask extends FileTask<ParsedMoorFile> {
final List<String> content;
MoorTask(BackendTask task, MoorSession session, this.content)
: super(task, session);
@override
FutureOr<ParsedMoorFile> compute() {
// TODO: implement compute
return null;
}
}

View File

@ -0,0 +1,19 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/analyzer/session.dart';
/// A backend for the moor generator.
///
/// Currently, we only have a backend based on the build package, but we can
/// extend this to a backend for an analyzer plugin or a standalone tool.
abstract class Backend {
final MoorSession session = MoorSession();
}
/// Used to analyze a single file via ([entrypoint]). The other methods can be
/// used to read imports used by the other files.
abstract class BackendTask {
String get entrypoint;
Future<LibraryElement> resolveDart(String path);
Future<String> readMoor(String path);
}

View File

@ -0,0 +1,21 @@
import 'package:build/build.dart';
import 'package:moor_generator/src/dao_generator.dart';
import 'package:moor_generator/src/moor_generator.dart';
import 'package:moor_generator/src/state/options.dart';
import 'package:source_gen/source_gen.dart';
class MoorBuilder extends SharedPartBuilder {
factory MoorBuilder(BuilderOptions options) {
final parsedOptions = MoorOptions.fromBuilder(options.config);
final generators = [
MoorGenerator(parsedOptions),
DaoGenerator(parsedOptions),
];
return MoorBuilder._(generators, 'moor');
}
MoorBuilder._(List<Generator> generators, String name)
: super(generators, name);
}

View File

@ -0,0 +1,47 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/analyzer/dart/parser.dart';
import 'package:test/test.dart';
import '../../utils/test_backend.dart';
void main() {
test('return expression of methods', () async {
final backend = TestBackend({
'test_lib|main.dart': r'''
class Test {
String get getter => 'foo';
String function() => 'bar';
String invalid() {
return 'baz';
}
}
'''
});
final backendTask = backend.startTask('test_lib|main.dart');
final dartTask = await backend.session.startDartTask(backendTask);
final parser = MoorDartParser(dartTask);
Future<MethodDeclaration> _loadDeclaration(Element element) async {
final declaration = await parser.loadElementDeclaration(element);
return declaration.node as MethodDeclaration;
}
void _verifyReturnExpressionMatches(Element element, String source) async {
final node = await _loadDeclaration(element);
expect(parser.returnExpressionOfMethod(node).toSource(), source);
}
final testClass = dartTask.library.getType('Test');
_verifyReturnExpressionMatches(testClass.getGetter('getter'), "'foo'");
_verifyReturnExpressionMatches(testClass.getMethod('function'), "'bar'");
final invalidDecl = await _loadDeclaration(testClass.getMethod('invalid'));
expect(parser.returnExpressionOfMethod(invalidDecl), isNull);
expect(dartTask.errors.errors, isNotEmpty);
backend.finish();
});
}

View File

@ -0,0 +1,59 @@
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:moor_generator/src/backends/backend.dart';
class TestBackend extends Backend {
final Map<String, String> fakeContent;
Resolver _resolver;
final Completer _initCompleter = Completer();
final Completer _finish = Completer();
/// Future that completes when this backend is ready, which happens when all
/// input files have been parsed and analyzed by the Dart analyzer.
Future get _ready => _initCompleter.future;
TestBackend(this.fakeContent) {
_init();
}
void _init() {
resolveSources(fakeContent, (r) {
_resolver = r;
_initCompleter.complete();
return _finish.future;
});
}
BackendTask startTask(String path) {
return _TestBackendTask(this, path);
}
void finish() {
_finish.complete();
}
}
class _TestBackendTask extends BackendTask {
final TestBackend backend;
@override
final String entrypoint;
_TestBackendTask(this.backend, this.entrypoint);
@override
Future<String> readMoor(String path) async {
await backend._ready;
return backend.fakeContent[path];
}
@override
Future<LibraryElement> resolveDart(String path) async {
await backend._ready;
return await backend._resolver.libraryFor(AssetId.parse(path));
}
}