Analyze body of CREATE TRIGGER statements

This commit is contained in:
Simon Binder 2020-01-01 21:19:10 +01:00
parent 75cbe74b53
commit cf6824af82
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 134 additions and 53 deletions

View File

@ -2,17 +2,21 @@ import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/errors.dart'; import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart'; import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/analyzer/runner/steps.dart'; import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/sqlparser.dart';
/// Handles `REFERENCES` clauses in tables by resolving their columns and /// Handles `REFERENCES` clauses in tables by resolving their columns and
/// reporting errors if they don't exist. Further, sets the /// reporting errors if they don't exist. Further, sets the
/// [MoorTable.references] field for tables declared in moor. /// [MoorTable.references] field for tables declared in moor.
class EntityHandler { class EntityHandler extends BaseAnalyzer {
final AnalyzeMoorStep step;
final ParsedMoorFile file; final ParsedMoorFile file;
final List<MoorTable> availableTables;
EntityHandler(this.step, this.file, this.availableTables); AnalyzeMoorStep get moorStep => step as AnalyzeMoorStep;
EntityHandler(
AnalyzeMoorStep step, this.file, List<MoorTable> availableTables)
: super(availableTables, step);
final Map<CreateTriggerStatement, MoorTrigger> _triggers = {}; final Map<CreateTriggerStatement, MoorTrigger> _triggers = {};
final Map<TableInducingStatement, MoorTable> _tables = {}; final Map<TableInducingStatement, MoorTable> _tables = {};
@ -31,8 +35,17 @@ class EntityHandler {
trigger.on = null; trigger.on = null;
final declaration = trigger.declaration as MoorTriggerDeclaration; final declaration = trigger.declaration as MoorTriggerDeclaration;
_triggers[declaration.node] = trigger; final node = declaration.node;
declaration.node.acceptWithoutArg(referenceResolver); _triggers[node] = trigger;
node.acceptWithoutArg(referenceResolver);
// triggers can have complex statements, so run the linter on them
final context = engine.analyzeNode(node, file.parseResult.sql);
context.errors.forEach(report);
final linter = Linter(context, mapper);
linter.reportLints();
reportLints(linter.lints, name: trigger.displayName);
} }
} }
@ -51,8 +64,7 @@ class _ReferenceResolvingVisitor extends RecursiveVisitor<void, void> {
_ReferenceResolvingVisitor(this.handler); _ReferenceResolvingVisitor(this.handler);
MoorTable _resolveTable(TableReference reference) { MoorTable _resolveTable(TableReference reference) {
return handler.availableTables.singleWhere( return handler.tables.singleWhere((t) => t.sqlName == reference.tableName,
(t) => t.sqlName == reference.tableName,
orElse: () => null); orElse: () => null);
} }

View File

@ -10,7 +10,7 @@ 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/runner/results.dart';
import 'package:moor_generator/src/analyzer/moor/inline_dart_resolver.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/moor/parser.dart';
import 'package:moor_generator/src/analyzer/sql_queries/sql_parser.dart'; import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
import 'package:moor_generator/src/analyzer/runner/task.dart'; import 'package:moor_generator/src/analyzer/runner/task.dart';
import 'package:moor_generator/src/model/sql_query.dart'; import 'package:moor_generator/src/model/sql_query.dart';

View File

@ -40,7 +40,8 @@ class AnalyzeDartStep extends AnalyzingStep {
.expand((f) => f.resolvedQueries); .expand((f) => f.resolvedQueries);
final availableTables = availableEntities.whereType<MoorTable>().toList(); final availableTables = availableEntities.whereType<MoorTable>().toList();
final parser = SqlParser(this, availableTables, accessor.declaredQueries); final parser =
SqlAnalyzer(this, availableTables, accessor.declaredQueries);
parser.parse(); parser.parse();
accessor accessor

View File

@ -13,7 +13,7 @@ class AnalyzeMoorStep extends AnalyzingStep {
.followedBy(parseResult.declaredTables) .followedBy(parseResult.declaredTables)
.toList(); .toList();
final parser = SqlParser(this, availableTables, parseResult.queries) final parser = SqlAnalyzer(this, availableTables, parseResult.queries)
..parse(); ..parse();
EntityHandler(this, parseResult, availableTables).handle(); EntityHandler(this, parseResult, availableTables).handle();

View File

@ -1,17 +1,23 @@
import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/sqlparser.dart';
import '../query_handler.dart'; import '../query_handler.dart';
import '../type_mapping.dart';
/// Provides additional hints that aren't implemented in the sqlparser because /// Provides additional hints that aren't implemented in the sqlparser because
/// they're specific to moor. /// they're specific to moor.
class Linter { class Linter {
final QueryHandler handler; final AnalysisContext context;
final TypeMapper mapper;
final List<AnalysisError> lints = []; final List<AnalysisError> lints = [];
Linter(this.handler); Linter(this.context, this.mapper);
Linter.forHandler(QueryHandler handler)
: context = handler.context,
mapper = handler.mapper;
void reportLints() { void reportLints() {
handler.context.root.acceptWithoutArg(_LintingVisitor(this)); context.root.acceptWithoutArg(_LintingVisitor(this));
} }
} }
@ -29,7 +35,7 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
// to know the column name (e.g. it's a Dart template without an AS), or // to know the column name (e.g. it's a Dart template without an AS), or
// if the type is unknown. // if the type is unknown.
final expression = e.expression; final expression = e.expression;
final resolveResult = linter.handler.context.typeOf(expression); final resolveResult = linter.context.typeOf(expression);
if (resolveResult.type == null) { if (resolveResult.type == null) {
linter.lints.add(AnalysisError( linter.lints.add(AnalysisError(
@ -86,8 +92,7 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
); );
// second, check that no required columns are left out // second, check that no required columns are left out
final specifiedTable = final specifiedTable = linter.mapper.tableToMoor(e.table.resolved as Table);
linter.handler.mapper.tableToMoor(e.table.resolved as Table);
final required = final required =
specifiedTable.columns.where((c) => c.requiredDuringInsert).toList(); specifiedTable.columns.where((c) => c.requiredDuringInsert).toList();

View File

@ -1,4 +1,5 @@
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:meta/meta.dart';
import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/errors.dart'; import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/runner/file_graph.dart'; import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
@ -8,27 +9,59 @@ import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn; import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
class SqlParser { abstract class BaseAnalyzer {
final List<MoorTable> tables; final List<MoorTable> tables;
final Step step; final Step step;
final List<DeclaredQuery> definedQueries;
final TypeMapper _mapper = TypeMapper(); @protected
final TypeMapper mapper = TypeMapper();
SqlEngine _engine; SqlEngine _engine;
BaseAnalyzer(this.tables, this.step);
@protected
SqlEngine get engine {
if (_engine == null) {
_engine = step.task.session.spawnEngine();
tables.map(mapper.extractStructure).forEach(_engine.registerTable);
}
return _engine;
}
@protected
void report(AnalysisError error, {String Function() msg, Severity severity}) {
if (step.file.type == FileType.moor) {
step.reportError(
ErrorInMoorFile.fromSqlParser(error, overrideSeverity: severity));
} else {
step.reportError(MoorError(
severity: severity,
message: msg(),
));
}
}
@protected
void reportLints(List<AnalysisError> lints, {String name}) {
for (final lint in lints) {
report(
lint,
msg: () => 'Lint for $name: $lint',
severity: Severity.warning,
);
}
}
}
class SqlAnalyzer extends BaseAnalyzer {
final List<DeclaredQuery> definedQueries;
final List<SqlQuery> foundQueries = []; final List<SqlQuery> foundQueries = [];
SqlParser(this.step, this.tables, this.definedQueries) { SqlAnalyzer(Step step, List<MoorTable> tables, this.definedQueries)
_engine = step.task.session.spawnEngine(); : super(tables, step);
}
void _spawnEngine() {
tables.map(_mapper.extractStructure).forEach(_engine.registerTable);
}
void parse() { void parse() {
_spawnEngine();
for (final query in definedQueries) { for (final query in definedQueries) {
final name = query.name; final name = query.name;
var declaredInMoor = false; var declaredInMoor = false;
@ -38,9 +71,9 @@ class SqlParser {
try { try {
if (query is DeclaredDartQuery) { if (query is DeclaredDartQuery) {
final sql = query.sql; final sql = query.sql;
context = _engine.analyze(sql); context = engine.analyze(sql);
} else if (query is DeclaredMoorQuery) { } else if (query is DeclaredMoorQuery) {
context = _engine.analyzeNode( context = engine.analyzeNode(
query.query, query.query,
query.file.parseResult.sql, query.file.parseResult.sql,
stmtOptions: _createOptions(query.astNode), stmtOptions: _createOptions(query.astNode),
@ -55,13 +88,13 @@ class SqlParser {
} }
for (final error in context.errors) { for (final error in context.errors) {
_report(error, report(error,
msg: () => 'The sql query $name is invalid: $error', msg: () => 'The sql query $name is invalid: $error',
severity: Severity.error); severity: Severity.error);
} }
try { try {
final query = QueryHandler(name, context, _mapper).handle() final query = QueryHandler(name, context, mapper).handle()
..declaredInMoorFile = declaredInMoor; ..declaredInMoorFile = declaredInMoor;
foundQueries.add(query); foundQueries.add(query);
} catch (e, s) { } catch (e, s) {
@ -72,29 +105,12 @@ class SqlParser {
// report lints // report lints
for (final query in foundQueries) { for (final query in foundQueries) {
for (final lint in query.lints) { reportLints(query.lints, name: query.name);
_report(lint,
msg: () => 'Lint for ${query.name}: $lint',
severity: Severity.warning);
}
}
}
void _report(AnalysisError error,
{String Function() msg, Severity severity}) {
if (step.file.type == FileType.moor) {
step.reportError(
ErrorInMoorFile.fromSqlParser(error, overrideSeverity: severity));
} else {
step.reportError(MoorError(
severity: severity,
message: msg(),
));
} }
} }
AnalyzeStatementOptions _createOptions(DeclaredStatement stmt) { AnalyzeStatementOptions _createOptions(DeclaredStatement stmt) {
final reader = _engine.schemaReader; final reader = engine.schemaReader;
final indexedHints = <int, ResolvedType>{}; final indexedHints = <int, ResolvedType>{};
final namedHints = <String, ResolvedType>{}; final namedHints = <String, ResolvedType>{};

View File

@ -30,7 +30,7 @@ class QueryHandler {
_verifyNoSkippedIndexes(); _verifyNoSkippedIndexes();
final query = _mapToMoor(); final query = _mapToMoor();
final linter = Linter(this); final linter = Linter.forHandler(this);
linter.reportLints(); linter.reportLints();
query.lints = linter.lints; query.lints = linter.lints;

View File

@ -1,8 +1,11 @@
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart'; import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../utils.dart';
void main() { void main() {
final engine = SqlEngine(useMoorExtensions: true); final engine = SqlEngine(useMoorExtensions: true);
final mapper = TypeMapper(); final mapper = TypeMapper();
@ -22,4 +25,48 @@ void main() {
expect(moorQuery.lints, expect(moorQuery.lints,
anyElement((AnalysisError q) => q.message.contains('Dart template'))); anyElement((AnalysisError q) => q.message.contains('Dart template')));
}); });
group('warns about insert column count mismatch', () {
test('in top-level queries', () async {
final state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
context VARCHAR
);
test: INSERT INTO foo VALUES (?)
''',
});
final file = await state.analyze('package:foo/a.moor');
expect(
file.errors.errors,
contains(const TypeMatcher<MoorError>().having(
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
);
});
test('in CREATE TRIGGER statements', () async {
final state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
context VARCHAR
);
CREATE TRIGGER my_trigger AFTER DELETE ON foo BEGIN
INSERT INTO foo VALUES (old.context);
END;
''',
});
final file = await state.analyze('package:foo/a.moor');
expect(
file.errors.errors,
contains(const TypeMatcher<MoorError>().having(
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
);
});
});
} }