Simple tests for pre-analysis step

This commit is contained in:
Simon Binder 2022-08-30 22:50:07 +02:00
parent 2575f36e82
commit aae09c02b0
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 282 additions and 12 deletions

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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));

View File

@ -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'))
]);
});
});
});
}

View File

@ -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);
}