mirror of https://github.com/AMT-Cheif/drift.git
Simple tests for pre-analysis step
This commit is contained in:
parent
2575f36e82
commit
aae09c02b0
|
@ -4,6 +4,8 @@ import 'package:logging/logging.dart';
|
|||
abstract class DriftBackend {
|
||||
Logger get log;
|
||||
|
||||
Uri resolveUri(Uri base, String uriString);
|
||||
|
||||
Future<String> readAsString(Uri uri);
|
||||
Future<LibraryElement> readDart(Uri uri);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
import '../../analyzer/options.dart';
|
||||
import '../backend.dart';
|
||||
import '../resolver/discover.dart';
|
||||
import 'cache.dart';
|
||||
import 'error.dart';
|
||||
import 'state.dart';
|
||||
|
||||
class DriftAnalysisDriver {
|
||||
final DriftBackend backend;
|
||||
|
@ -24,11 +27,40 @@ class DriftAnalysisDriver {
|
|||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<FileState> prepareFileForAnalysis(Uri uri) async {
|
||||
var known = cache.knownFiles[uri] ?? cache.notifyFileChanged(uri);
|
||||
|
||||
if (known.discovery == null) {
|
||||
await DiscoverStep(this, cache.notifyFileChanged(uri)).discover();
|
||||
|
||||
// To analyze a drift file, we also need to be able to analyze imports.
|
||||
final state = known.discovery;
|
||||
if (state is DiscoveredDriftFile) {
|
||||
for (final import in state.imports) {
|
||||
final file = await prepareFileForAnalysis(import.importedUri);
|
||||
|
||||
if (file.discovery?.isValidImport != true) {
|
||||
known.errorsDuringDiscovery.add(
|
||||
DriftAnalysisError.inDriftFile(
|
||||
import.ast,
|
||||
'The imported file, `${import.importedUri}`, does not exist or '
|
||||
"can't be imported.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return known;
|
||||
}
|
||||
|
||||
Future<void> fullyAnalyze(Uri uri) async {
|
||||
var known = cache.knownFiles[uri];
|
||||
|
||||
if (known == null || known.discovery == null) {
|
||||
await DiscoverStep(this, cache.notifyFileChanged(uri)).discover();
|
||||
await prepareFileForAnalysis(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,25 @@
|
|||
class AnalysisError {}
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||
|
||||
class DriftAnalysisError {
|
||||
final SourceSpan? span;
|
||||
final String message;
|
||||
|
||||
DriftAnalysisError(this.span, this.message);
|
||||
|
||||
factory DriftAnalysisError.inDriftFile(
|
||||
sql.SyntacticEntity sql, String message) {
|
||||
return DriftAnalysisError(sql.span, message);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final span = this.span;
|
||||
|
||||
if (span != null) {
|
||||
return span.message(message);
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ class FileState {
|
|||
DiscoveredFileState? discovery;
|
||||
AnalyzedFile? results;
|
||||
|
||||
final List<AnalysisError> errorsDuringDiscovery = [];
|
||||
final List<AnalysisError> errorsDuringAnalysis = [];
|
||||
final List<DriftAnalysisError> errorsDuringDiscovery = [];
|
||||
final List<DriftAnalysisError> errorsDuringAnalysis = [];
|
||||
|
||||
FileState(this.ownUri);
|
||||
|
||||
|
@ -23,18 +23,43 @@ class FileState {
|
|||
abstract class DiscoveredFileState {
|
||||
final List<DiscoveredElement> locallyDefinedElements;
|
||||
|
||||
bool get isValidImport => false;
|
||||
|
||||
Iterable<Uri> get importDependencies => const [];
|
||||
|
||||
DiscoveredFileState(this.locallyDefinedElements);
|
||||
}
|
||||
|
||||
class DiscoveredDriftFile extends DiscoveredFileState {
|
||||
final DriftFile ast;
|
||||
final List<DriftFileImport> imports;
|
||||
|
||||
DiscoveredDriftFile(this.ast, super.locallyDefinedElements);
|
||||
@override
|
||||
bool get isValidImport => true;
|
||||
|
||||
@override
|
||||
Iterable<Uri> get importDependencies => imports.map((e) => e.importedUri);
|
||||
|
||||
DiscoveredDriftFile({
|
||||
required this.ast,
|
||||
required this.imports,
|
||||
required List<DiscoveredElement> locallyDefinedElements,
|
||||
}) : super(locallyDefinedElements);
|
||||
}
|
||||
|
||||
class DriftFileImport {
|
||||
final ImportStatement ast;
|
||||
final Uri importedUri;
|
||||
|
||||
DriftFileImport(this.ast, this.importedUri);
|
||||
}
|
||||
|
||||
class DiscoveredDartLibrary extends DiscoveredFileState {
|
||||
final LibraryElement library;
|
||||
|
||||
@override
|
||||
bool get isValidImport => true;
|
||||
|
||||
DiscoveredDartLibrary(this.library, super.locallyDefinedElements);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' hide AnalysisError;
|
||||
|
||||
import '../driver/driver.dart';
|
||||
import '../driver/error.dart';
|
||||
import '../driver/state.dart';
|
||||
import '../results/element.dart';
|
||||
import 'intermediate_state.dart';
|
||||
|
@ -41,12 +42,22 @@ class DiscoverStep {
|
|||
break;
|
||||
}
|
||||
|
||||
// todo: Handle parse errors
|
||||
final parsed = engine.parseDriftFile(contents);
|
||||
for (final error in parsed.errors) {
|
||||
_file.errorsDuringDiscovery
|
||||
.add(DriftAnalysisError(error.token.span, error.message));
|
||||
}
|
||||
|
||||
final ast = parsed.rootNode as DriftFile;
|
||||
final imports = <DriftFileImport>[];
|
||||
|
||||
for (final node in ast.childNodes) {
|
||||
if (node is TableInducingStatement) {
|
||||
if (node is ImportStatement) {
|
||||
final uri =
|
||||
_driver.backend.resolveUri(_file.ownUri, node.importedFile);
|
||||
|
||||
imports.add(DriftFileImport(node, uri));
|
||||
} else if (node is TableInducingStatement) {
|
||||
pendingElements
|
||||
.add(DiscoveredDriftTable(_id(node.createdName), node));
|
||||
} else if (node is CreateViewStatement) {
|
||||
|
@ -55,6 +66,11 @@ class DiscoverStep {
|
|||
}
|
||||
}
|
||||
|
||||
_file.discovery = DiscoveredDriftFile(
|
||||
ast: parsed.rootNode as DriftFile,
|
||||
imports: imports,
|
||||
locallyDefinedElements: pendingElements,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' show url;
|
||||
|
||||
@sealed
|
||||
class DriftElementId {
|
||||
final Uri libraryUri;
|
||||
final String name;
|
||||
|
@ -8,6 +10,21 @@ class DriftElementId {
|
|||
|
||||
bool get isDefinedInDart => url.extension(libraryUri.path) == '.dart';
|
||||
bool get isDefinedInDrift => url.extension(libraryUri.path) == '.drift';
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(DriftElementId, libraryUri, name);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DriftElementId &&
|
||||
other.libraryUri == libraryUri &&
|
||||
other.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DriftElementId($libraryUri, $name)';
|
||||
}
|
||||
}
|
||||
|
||||
class DriftDeclaration {
|
||||
|
|
|
@ -13,6 +13,12 @@ class DriftBuildBackend extends DriftBackend {
|
|||
@override
|
||||
Logger get log => build.log;
|
||||
|
||||
@override
|
||||
Uri resolveUri(Uri base, String uriString) {
|
||||
return AssetId.resolve(Uri.parse(uriString), from: AssetId.resolve(base))
|
||||
.uri;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readAsString(Uri uri) {
|
||||
return _buildStep.readAsString(AssetId.resolve(uri));
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:drift_dev/src/analysis/driver/state.dart';
|
||||
import 'package:drift_dev/src/analysis/resolver/intermediate_state.dart';
|
||||
import 'package:drift_dev/src/analysis/results/element.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../test_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('drift files', () {
|
||||
test('finds local elements', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/main.drift': '''
|
||||
CREATE TABLE foo (bar INTEGER);
|
||||
|
||||
CREATE VIEW my_view AS SELECT whatever FROM unknown_table;
|
||||
''',
|
||||
});
|
||||
|
||||
final uri = Uri.parse('package:a/main.drift');
|
||||
final state = await backend.driver.prepareFileForAnalysis(uri);
|
||||
final discovered = state.discovery;
|
||||
|
||||
DriftElementId id(String name) => DriftElementId(uri, name);
|
||||
|
||||
expect(state, hasNoErrors);
|
||||
expect(
|
||||
discovered,
|
||||
isA<DiscoveredDriftFile>()
|
||||
.having((e) => e.imports, 'imports', isEmpty)
|
||||
.having(
|
||||
(e) => e.locallyDefinedElements,
|
||||
'locallyDefinedElements',
|
||||
[
|
||||
isA<DiscoveredDriftTable>()
|
||||
.having((t) => t.ownId, 'ownId', id('foo')),
|
||||
isA<DiscoveredDriftView>()
|
||||
.having((v) => v.ownId, 'ownId', id('my_view')),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('reports syntax errors', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/main.drift': '''
|
||||
CREATE TABLE valid_1 (bar INTEGER);
|
||||
|
||||
CREATE TABLE EXISTS syntax_error ();
|
||||
|
||||
CREATE TABLE valid_2 (bar INTEGER);
|
||||
''',
|
||||
});
|
||||
|
||||
final state = await backend.driver
|
||||
.prepareFileForAnalysis(Uri.parse('package:a/main.drift'));
|
||||
expect(state.errorsDuringDiscovery, [
|
||||
isDriftError(contains('Expected a table name')),
|
||||
]);
|
||||
expect(state.errorsDuringAnalysis, isEmpty);
|
||||
|
||||
// The syntax error should only affect the single statement
|
||||
expect(
|
||||
state.discovery,
|
||||
isA<DiscoveredDriftFile>().having((e) => e.locallyDefinedElements,
|
||||
'locallyDefinedElements', hasLength(2)));
|
||||
});
|
||||
|
||||
group('imports', () {
|
||||
test('are resolved', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/a.drift': "import 'b.drift';",
|
||||
'a|lib/b.drift': "CREATE TABLE foo (bar INTEGER);",
|
||||
});
|
||||
|
||||
final state = await backend.driver
|
||||
.prepareFileForAnalysis(Uri.parse('package:a/a.drift'));
|
||||
expect(state, hasNoErrors);
|
||||
expect(
|
||||
state.discovery,
|
||||
isA<DiscoveredDriftFile>().having(
|
||||
(e) => e.importDependencies,
|
||||
'importDependencies',
|
||||
[Uri.parse('package:a/b.drift')],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
backend.driver.cache.knownFiles[Uri.parse('package:a/b.drift')],
|
||||
isNotNull,
|
||||
reason: 'Import should have been prepared as well',
|
||||
);
|
||||
});
|
||||
|
||||
test('can handle circular imports', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/a.drift': "import 'a.drift'; import 'b.drift';",
|
||||
'a|lib/b.drift': "import 'a.drift';",
|
||||
});
|
||||
|
||||
final state = await backend.driver
|
||||
.prepareFileForAnalysis(Uri.parse('package:a/a.drift'));
|
||||
expect(state, hasNoErrors);
|
||||
});
|
||||
|
||||
test('emits warning on invalid import', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/a.drift': "import 'b.drift';",
|
||||
});
|
||||
|
||||
final state = await backend.driver
|
||||
.prepareFileForAnalysis(Uri.parse('package:a/a.drift'));
|
||||
expect(state.errorsDuringDiscovery, [
|
||||
isDriftError(contains(
|
||||
'The imported file, `package:a/b.drift`, does not exist'))
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,19 +1,30 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:drift_dev/src/analysis/backend.dart';
|
||||
import 'package:drift_dev/src/analysis/driver/driver.dart';
|
||||
import 'package:drift_dev/src/analysis/driver/error.dart';
|
||||
import 'package:drift_dev/src/analysis/driver/state.dart';
|
||||
import 'package:drift_dev/src/analyzer/options.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/expect.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
|
||||
class TestBackend extends DriftBackend {
|
||||
final Map<String, String> sourceContents;
|
||||
|
||||
TestBackend(Map<String, String> sourceContents)
|
||||
late final DriftAnalysisDriver driver;
|
||||
|
||||
TestBackend(Map<String, String> sourceContents, DriftOptions options)
|
||||
: sourceContents = {
|
||||
for (final entry in sourceContents.entries)
|
||||
AssetId.parse(entry.key).uri.toString(): entry.value,
|
||||
};
|
||||
} {
|
||||
driver = DriftAnalysisDriver(this, options);
|
||||
}
|
||||
|
||||
factory TestBackend.inTest(Map<String, String> sourceContents) {
|
||||
final backend = TestBackend(sourceContents);
|
||||
factory TestBackend.inTest(Map<String, String> sourceContents,
|
||||
{DriftOptions options = const DriftOptions.defaults()}) {
|
||||
final backend = TestBackend(sourceContents, options);
|
||||
addTearDown(backend.dispose);
|
||||
|
||||
return backend;
|
||||
|
@ -22,11 +33,30 @@ class TestBackend extends DriftBackend {
|
|||
@override
|
||||
Logger get log => Logger.root;
|
||||
|
||||
@override
|
||||
Uri resolveUri(Uri base, String uriString) {
|
||||
return base.resolve(uriString);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readAsString(Uri uri) async {
|
||||
return sourceContents[uri.toString()] ??
|
||||
(throw StateError('No source for $uri'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibraryElement> readDart(Uri uri) {
|
||||
// TODO: implement readDart
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
Matcher get hasNoErrors => isA<FileState>()
|
||||
.having((e) => e.errorsDuringDiscovery, 'errorsDuringDiscovery', isEmpty)
|
||||
.having((e) => e.errorsDuringAnalysis, 'errorsDuringAnalysis', isEmpty);
|
||||
|
||||
Matcher isDriftError(dynamic message) {
|
||||
return isA<DriftAnalysisError>().having((e) => e.message, 'message', message);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue