Don't throw parsing errors for top-level statements

This commit is contained in:
Simon Binder 2020-06-24 17:08:13 +02:00
parent b4aeacdba3
commit ccea0a5d36
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 98 additions and 22 deletions

View File

@ -6,6 +6,7 @@
- Breaking: Removed `visitQueryable`. Use `defaultQueryable` instead. - Breaking: Removed `visitQueryable`. Use `defaultQueryable` instead.
- Support parsing and analyzing `CREATE VIEW` statements (see `SchemaFromCreateTable.readView`). - Support parsing and analyzing `CREATE VIEW` statements (see `SchemaFromCreateTable.readView`).
Thanks to [@mqus](https://github.com/mqus) for their contribution! Thanks to [@mqus](https://github.com/mqus) for their contribution!
- `SqlEngine.parse` will no longer throw when there's a parsing error (use `ParseResult.errors` instead).
## 0.9.0 ## 0.9.0

View File

@ -1,23 +1,31 @@
part of 'analysis.dart'; part of 'analysis.dart';
class AnalysisError { class AnalysisError {
final AstNode relevantNode; final SyntacticEntity source;
final String message; final String message;
final AnalysisErrorType type; final AnalysisErrorType type;
AnalysisError({@required this.type, this.message, this.relevantNode}); AnalysisError._internal(this.type, this.message, this.source);
AnalysisError({@required this.type, this.message, AstNode relevantNode})
: source = relevantNode;
@Deprecated('Use source instead')
AstNode get relevantNode => source as AstNode;
factory AnalysisError.fromParser(ParsingError error) {
return AnalysisError._internal(
AnalysisErrorType.synctactic,
error.message,
error.token,
);
}
/// The relevant portion of the source code that caused this error. Some AST /// The relevant portion of the source code that caused this error. Some AST
/// nodes don't have a span, in that case this error is going to be null. /// nodes don't have a span, in that case this error is going to have a null
FileSpan get span { /// span as well.
final first = relevantNode?.first?.span; FileSpan get span => source.span;
final last = relevantNode?.last?.span;
if (first != null && last != null) {
return first.expand(last);
}
return null;
}
@override @override
String toString() { String toString() {
@ -54,8 +62,6 @@ enum AnalysisErrorType {
referencedUnknownTable, referencedUnknownTable,
referencedUnknownColumn, referencedUnknownColumn,
ambiguousReference, ambiguousReference,
/// Note that most syntax errors are reported as [ParsingError]
synctactic, synctactic,
unknownFunction, unknownFunction,

View File

@ -37,6 +37,7 @@ part 'statements/create_trigger.dart';
part 'statements/create_view.dart'; part 'statements/create_view.dart';
part 'statements/delete.dart'; part 'statements/delete.dart';
part 'statements/insert.dart'; part 'statements/insert.dart';
part 'statements/invalid.dart';
part 'statements/select.dart'; part 'statements/select.dart';
part 'statements/statement.dart'; part 'statements/statement.dart';
part 'statements/update.dart'; part 'statements/update.dart';

View File

@ -0,0 +1,18 @@
part of '../ast.dart';
/// Used as a top-level parsing
class InvalidStatement extends Statement {
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitInvalidStatement(this, arg);
}
@override
Iterable<AstNode> get childNodes => const Iterable.empty();
@override
bool contentEquals(InvalidStatement other) => true;
@override
void transformChildren<A>(Transformer<A> transformer, A arg) {}
}

View File

@ -14,6 +14,7 @@ abstract class AstVisitor<A, R> {
R visitCreateTriggerStatement(CreateTriggerStatement e, A arg); R visitCreateTriggerStatement(CreateTriggerStatement e, A arg);
R visitCreateIndexStatement(CreateIndexStatement e, A arg); R visitCreateIndexStatement(CreateIndexStatement e, A arg);
R visitCreateViewStatement(CreateViewStatement e, A arg); R visitCreateViewStatement(CreateViewStatement e, A arg);
R visitInvalidStatement(InvalidStatement e, A arg);
R visitWithClause(WithClause e, A arg); R visitWithClause(WithClause e, A arg);
R visitUpsertClause(UpsertClause e, A arg); R visitUpsertClause(UpsertClause e, A arg);
@ -96,6 +97,11 @@ abstract class AstVisitor<A, R> {
class RecursiveVisitor<A, R> implements AstVisitor<A, R> { class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
// Statements // Statements
@override
R visitInvalidStatement(InvalidStatement e, A arg) {
return visitStatement(e, arg);
}
@override @override
R visitSelectStatement(SelectStatement e, A arg) { R visitSelectStatement(SelectStatement e, A arg) {
return visitBaseSelectStatement(e, arg); return visitBaseSelectStatement(e, arg);

View File

@ -110,7 +110,7 @@ class SqlEngine {
final tokensForParser = tokens.where((t) => !t.invisibleToParser).toList(); final tokensForParser = tokens.where((t) => !t.invisibleToParser).toList();
final parser = Parser(tokensForParser, useMoor: options.useMoorExtensions); final parser = Parser(tokensForParser, useMoor: options.useMoorExtensions);
final stmt = parser.statement(); final stmt = parser.safeStatement();
return ParseResult._(stmt, tokens, parser.errors, sql, null); return ParseResult._(stmt, tokens, parser.errors, sql, null);
} }
@ -143,7 +143,14 @@ class SqlEngine {
/// this statement only. /// this statement only.
AnalysisContext analyze(String sql, {AnalyzeStatementOptions stmtOptions}) { AnalysisContext analyze(String sql, {AnalyzeStatementOptions stmtOptions}) {
final result = parse(sql); final result = parse(sql);
return analyzeParsed(result, stmtOptions: stmtOptions); final analyzed = analyzeParsed(result, stmtOptions: stmtOptions);
// Add parsing errors that occured to the beginning since they are the most
// prominent problems.
analyzed.errors
.insertAll(0, result.errors.map((e) => AnalysisError.fromParser(e)));
return analyzed;
} }
/// Analyzes a parsed [result] statement. The [AnalysisContext] returned /// Analyzes a parsed [result] statement. The [AnalysisContext] returned

View File

@ -225,6 +225,12 @@ class Parser extends ParserBase
// todo remove this and don't be that lazy in moorFile() // todo remove this and don't be that lazy in moorFile()
var _lastStmtHadParsingError = false; var _lastStmtHadParsingError = false;
/// Parses a statement without throwing when there's a parsing error.
Statement /*?*/ safeStatement() {
return _parseAsStatement(statement, requireSemicolon: false) ??
InvalidStatement();
}
Statement statement() { Statement statement() {
final first = _peek; final first = _peek;
Statement stmt = _crud(); Statement stmt = _crud();
@ -369,14 +375,15 @@ class Parser extends ParserBase
/// Invokes [parser], sets the appropriate source span and attaches a /// Invokes [parser], sets the appropriate source span and attaches a
/// semicolon if one exists. /// semicolon if one exists.
T _parseAsStatement<T extends Statement>(T Function() parser) { T _parseAsStatement<T extends Statement>(T Function() parser,
{bool requireSemicolon = true}) {
_lastStmtHadParsingError = false; _lastStmtHadParsingError = false;
final first = _peek; final first = _peek;
T result; T result;
try { try {
result = parser(); result = parser();
if (result != null) { if (result != null && requireSemicolon) {
result.semicolon = _consume(TokenType.semicolon, result.semicolon = _consume(TokenType.semicolon,
'Expected a semicolon after the statement ended'); 'Expected a semicolon after the statement ended');
result.setSpan(first, _previous); result.setSpan(first, _previous);

View File

@ -0,0 +1,30 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
test('does not throw when parsing invalid statements', () {
final engine = SqlEngine();
ParseResult result;
try {
result = engine.parse('UDPATE foo SET bar = foo;');
} on ParsingError {
fail('Calling engine.parse threw an error');
}
expect(result.errors, isNotEmpty);
});
test('does not throw when analyzing invalid statements', () {
final engine = SqlEngine();
AnalysisContext result;
try {
result = engine.analyze('UDPATE foo SET bar = foo;');
} on ParsingError {
fail('Calling engine.parse threw an error');
}
expect(result.errors, isNotEmpty);
});
}

View File

@ -188,16 +188,16 @@ void main() {
test("can't have empty arguments in CREATE VIRTUAL TABLE", () { test("can't have empty arguments in CREATE VIRTUAL TABLE", () {
final engine = SqlEngine(); final engine = SqlEngine();
expect( expect(
() => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)'), engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)').errors,
throwsA( contains(
const TypeMatcher<ParsingError>() const TypeMatcher<ParsingError>()
.having((e) => e.token.lexeme, 'fails at closing bracket', ')'), .having((e) => e.token.lexeme, 'fails at closing bracket', ')'),
), ),
); );
expect( expect(
() => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)'), engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)').errors,
throwsA( contains(
const TypeMatcher<ParsingError>() const TypeMatcher<ParsingError>()
.having((e) => e.token.lexeme, 'fails at next comma', ','), .having((e) => e.token.lexeme, 'fails at next comma', ','),
), ),