diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index a0034530..179fc6de 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -6,6 +6,7 @@ - Breaking: Removed `visitQueryable`. Use `defaultQueryable` instead. - Support parsing and analyzing `CREATE VIEW` statements (see `SchemaFromCreateTable.readView`). 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 diff --git a/sqlparser/lib/src/analysis/error.dart b/sqlparser/lib/src/analysis/error.dart index 48b378d3..e600fd0b 100644 --- a/sqlparser/lib/src/analysis/error.dart +++ b/sqlparser/lib/src/analysis/error.dart @@ -1,23 +1,31 @@ part of 'analysis.dart'; class AnalysisError { - final AstNode relevantNode; + final SyntacticEntity source; + final String message; 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 - /// nodes don't have a span, in that case this error is going to be null. - FileSpan get span { - final first = relevantNode?.first?.span; - final last = relevantNode?.last?.span; - - if (first != null && last != null) { - return first.expand(last); - } - return null; - } + /// nodes don't have a span, in that case this error is going to have a null + /// span as well. + FileSpan get span => source.span; @override String toString() { @@ -54,8 +62,6 @@ enum AnalysisErrorType { referencedUnknownTable, referencedUnknownColumn, ambiguousReference, - - /// Note that most syntax errors are reported as [ParsingError] synctactic, unknownFunction, diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 06562df1..55def297 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -37,6 +37,7 @@ part 'statements/create_trigger.dart'; part 'statements/create_view.dart'; part 'statements/delete.dart'; part 'statements/insert.dart'; +part 'statements/invalid.dart'; part 'statements/select.dart'; part 'statements/statement.dart'; part 'statements/update.dart'; diff --git a/sqlparser/lib/src/ast/statements/invalid.dart b/sqlparser/lib/src/ast/statements/invalid.dart new file mode 100644 index 00000000..5a43b579 --- /dev/null +++ b/sqlparser/lib/src/ast/statements/invalid.dart @@ -0,0 +1,18 @@ +part of '../ast.dart'; + +/// Used as a top-level parsing +class InvalidStatement extends Statement { + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitInvalidStatement(this, arg); + } + + @override + Iterable get childNodes => const Iterable.empty(); + + @override + bool contentEquals(InvalidStatement other) => true; + + @override + void transformChildren(Transformer transformer, A arg) {} +} diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index bb36874a..5bb4653b 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -14,6 +14,7 @@ abstract class AstVisitor { R visitCreateTriggerStatement(CreateTriggerStatement e, A arg); R visitCreateIndexStatement(CreateIndexStatement e, A arg); R visitCreateViewStatement(CreateViewStatement e, A arg); + R visitInvalidStatement(InvalidStatement e, A arg); R visitWithClause(WithClause e, A arg); R visitUpsertClause(UpsertClause e, A arg); @@ -96,6 +97,11 @@ abstract class AstVisitor { class RecursiveVisitor implements AstVisitor { // Statements + @override + R visitInvalidStatement(InvalidStatement e, A arg) { + return visitStatement(e, arg); + } + @override R visitSelectStatement(SelectStatement e, A arg) { return visitBaseSelectStatement(e, arg); diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index 65a95f3b..17f71a6a 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -110,7 +110,7 @@ class SqlEngine { final tokensForParser = tokens.where((t) => !t.invisibleToParser).toList(); final parser = Parser(tokensForParser, useMoor: options.useMoorExtensions); - final stmt = parser.statement(); + final stmt = parser.safeStatement(); return ParseResult._(stmt, tokens, parser.errors, sql, null); } @@ -143,7 +143,14 @@ class SqlEngine { /// this statement only. AnalysisContext analyze(String sql, {AnalyzeStatementOptions stmtOptions}) { 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 diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index b75d6102..2a7d81ba 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -225,6 +225,12 @@ class Parser extends ParserBase // todo remove this and don't be that lazy in moorFile() var _lastStmtHadParsingError = false; + /// Parses a statement without throwing when there's a parsing error. + Statement /*?*/ safeStatement() { + return _parseAsStatement(statement, requireSemicolon: false) ?? + InvalidStatement(); + } + Statement statement() { final first = _peek; Statement stmt = _crud(); @@ -369,14 +375,15 @@ class Parser extends ParserBase /// Invokes [parser], sets the appropriate source span and attaches a /// semicolon if one exists. - T _parseAsStatement(T Function() parser) { + T _parseAsStatement(T Function() parser, + {bool requireSemicolon = true}) { _lastStmtHadParsingError = false; final first = _peek; T result; try { result = parser(); - if (result != null) { + if (result != null && requireSemicolon) { result.semicolon = _consume(TokenType.semicolon, 'Expected a semicolon after the statement ended'); result.setSpan(first, _previous); diff --git a/sqlparser/test/engine/sql_engine_test.dart b/sqlparser/test/engine/sql_engine_test.dart new file mode 100644 index 00000000..779095a7 --- /dev/null +++ b/sqlparser/test/engine/sql_engine_test.dart @@ -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); + }); +} diff --git a/sqlparser/test/parser/create_table_test.dart b/sqlparser/test/parser/create_table_test.dart index 435d3c90..8ed36390 100644 --- a/sqlparser/test/parser/create_table_test.dart +++ b/sqlparser/test/parser/create_table_test.dart @@ -188,16 +188,16 @@ void main() { test("can't have empty arguments in CREATE VIRTUAL TABLE", () { final engine = SqlEngine(); expect( - () => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)'), - throwsA( + engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)').errors, + contains( const TypeMatcher() .having((e) => e.token.lexeme, 'fails at closing bracket', ')'), ), ); expect( - () => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)'), - throwsA( + engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)').errors, + contains( const TypeMatcher() .having((e) => e.token.lexeme, 'fails at next comma', ','), ),