mirror of
https://github.com/MontFerret/ferret.git
synced 2025-08-15 20:02:56 +02:00
Refactor error handling and diagnostics: enhance error messages for syntax errors, improve handling of extraneous input, and streamline error listener logic for better clarity and maintainability.
This commit is contained in:
@@ -64,6 +64,9 @@ func (c *Compiler) Compile(src *file.Source) (program *vm.Program, err error) {
|
|||||||
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
|
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
|
||||||
return diagnostics.NewTrackingTokenStream(stream, tokenHistory)
|
return diagnostics.NewTrackingTokenStream(stream, tokenHistory)
|
||||||
})
|
})
|
||||||
|
// Remove all default error listeners
|
||||||
|
p.RemoveErrorListeners()
|
||||||
|
// Add custom error listener
|
||||||
p.AddErrorListener(diagnostics.NewErrorListener(src, l.Ctx.Errors, tokenHistory))
|
p.AddErrorListener(diagnostics.NewErrorListener(src, l.Ctx.Errors, tokenHistory))
|
||||||
p.Visit(l)
|
p.Visit(l)
|
||||||
|
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
package diagnostics
|
package diagnostics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/antlr4-go/antlr/v4"
|
"github.com/antlr4-go/antlr/v4"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/file"
|
"github.com/MontFerret/ferret/pkg/file"
|
||||||
@@ -38,8 +36,8 @@ func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interfac
|
|||||||
offending = tok
|
offending = tok
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.parseError(msg, offending); err != nil {
|
if !d.handler.HasErrorOnLine(line) {
|
||||||
if !d.handler.HasErrorOnLine(line) {
|
if err := d.parseError(msg, offending); err != nil {
|
||||||
d.handler.Add(err)
|
d.handler.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,53 +56,7 @@ func (d *ErrorListener) parseError(msg string, offending antlr.Token) *Compilati
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range []func(*CompilationError) bool{
|
AnalyzeSyntaxError(d.src, err, d.history.Last())
|
||||||
d.extraneousInput,
|
|
||||||
d.noViableAlternative,
|
|
||||||
d.mismatchedInput,
|
|
||||||
} {
|
|
||||||
if handler(err) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ErrorListener) extraneousInput(err *CompilationError) (matched bool) {
|
|
||||||
if !strings.Contains(err.Message, "extraneous input") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
last := d.history.Last()
|
|
||||||
|
|
||||||
if last == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
span := spanFromTokenSafe(last.Token(), d.src)
|
|
||||||
err.Spans = []ErrorSpan{
|
|
||||||
NewMainErrorSpan(span, "query must end with a value"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err.Message = "Expected a RETURN or FOR clause at end of query"
|
|
||||||
err.Hint = "All queries must return a value. Add a RETURN statement to complete the query."
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ErrorListener) noViableAlternative(err *CompilationError) bool {
|
|
||||||
if !strings.Contains(err.Message, "viable alternative at input") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnalyzeSyntaxError(d.src, err, d.history.Last())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ErrorListener) mismatchedInput(err *CompilationError) bool {
|
|
||||||
if !strings.Contains(err.Message, "mismatched input") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnalyzeSyntaxError(d.src, err, d.history.Last())
|
|
||||||
}
|
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package diagnostics
|
package diagnostics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/parser/fql"
|
|
||||||
"github.com/antlr4-go/antlr/v4"
|
"github.com/antlr4-go/antlr/v4"
|
||||||
|
|
||||||
|
"github.com/MontFerret/ferret/pkg/parser/fql"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/file"
|
"github.com/MontFerret/ferret/pkg/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,3 +113,34 @@ func is(node *TokenNode, expected string) bool {
|
|||||||
func has(msg string, substr string) bool {
|
func has(msg string, substr string) bool {
|
||||||
return strings.Contains(strings.ToLower(msg), strings.ToLower(substr))
|
return strings.Contains(strings.ToLower(msg), strings.ToLower(substr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isExtraneous(msg string) bool {
|
||||||
|
return has(msg, "extraneous input")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExtraneousInput(msg string) string {
|
||||||
|
re := regexp.MustCompile(`extraneous input\s+(?P<input>.+?)\s+expecting`)
|
||||||
|
match := re.FindStringSubmatch(msg)
|
||||||
|
return match[re.SubexpIndex("input")]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExtraneousInputAll(msg string) (string, []string) {
|
||||||
|
rx := regexp.MustCompile(`extraneous input\s+(?P<input>.+?)\s+expecting\s+\{(?P<expected>.+?)\}`)
|
||||||
|
matches := rx.FindStringSubmatch(msg)
|
||||||
|
|
||||||
|
if len(matches) != 3 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
input := strings.TrimSpace(matches[1])
|
||||||
|
expectedRaw := strings.TrimSpace(matches[2])
|
||||||
|
var expected []string
|
||||||
|
|
||||||
|
for _, part := range strings.Split(expectedRaw, ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
part = strings.Trim(part, "'")
|
||||||
|
expected = append(expected, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input, expected
|
||||||
|
}
|
||||||
|
@@ -7,6 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func matchMissingAssignmentValue(src *file.Source, err *CompilationError, offending *TokenNode) bool {
|
func matchMissingAssignmentValue(src *file.Source, err *CompilationError, offending *TokenNode) bool {
|
||||||
|
if isExtraneous(err.Message) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
prev := offending.Prev()
|
prev := offending.Prev()
|
||||||
|
|
||||||
if is(offending, "LET") || is(prev, "=") {
|
if is(offending, "LET") || is(prev, "=") {
|
||||||
|
@@ -64,5 +64,63 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is(prev, "FILTER") {
|
||||||
|
span := spanFromTokenSafe(prev.Token(), src)
|
||||||
|
span.Start = span.End
|
||||||
|
span.End = span.Start + 1
|
||||||
|
|
||||||
|
err.Message = "Expected condition after 'FILTER'"
|
||||||
|
err.Hint = "FILTER requires a boolean expression."
|
||||||
|
err.Spans = []ErrorSpan{
|
||||||
|
NewMainErrorSpan(span, "missing expression"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if is(prev, "LIMIT") {
|
||||||
|
span := spanFromTokenSafe(prev.Token(), src)
|
||||||
|
span.Start = span.End
|
||||||
|
span.End = span.Start + 1
|
||||||
|
|
||||||
|
err.Message = "Expected number after 'LIMIT'"
|
||||||
|
err.Hint = "LIMIT requires a numeric value."
|
||||||
|
err.Spans = []ErrorSpan{
|
||||||
|
NewMainErrorSpan(span, "missing expression"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExtraneous(err.Message) {
|
||||||
|
input := parseExtraneousInput(err.Message)
|
||||||
|
|
||||||
|
if input != "','" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps int
|
||||||
|
|
||||||
|
// We walk back two tokens to find if the keyword is LIMIT.
|
||||||
|
for ; steps < 2 && prev != nil; steps++ {
|
||||||
|
prev = prev.Prev()
|
||||||
|
}
|
||||||
|
|
||||||
|
if is(prev, "LIMIT") {
|
||||||
|
limitSpan := spanFromTokenSafe(prev.Token(), src)
|
||||||
|
span := spanFromTokenSafe(offending.Token(), src)
|
||||||
|
span.Start = limitSpan.End + 1
|
||||||
|
span.End += 4
|
||||||
|
|
||||||
|
err.Message = "Too many arguments provided to LIMIT clause"
|
||||||
|
err.Hint = "LIMIT accepts at most two arguments: offset and count."
|
||||||
|
err.Spans = []ErrorSpan{
|
||||||
|
NewMainErrorSpan(span, "unexpected third argument"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func matchMissingReturnValue(src *file.Source, err *CompilationError, offending *TokenNode) bool {
|
func matchMissingReturnValue(src *file.Source, err *CompilationError, offending *TokenNode) bool {
|
||||||
if !is(offending, "RETURN") {
|
extraneous := isExtraneous(err.Message)
|
||||||
|
|
||||||
|
if !is(offending, "RETURN") && !extraneous {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if extraneous {
|
||||||
|
span := spanFromTokenSafe(offending.Token(), src)
|
||||||
|
err.Spans = []ErrorSpan{
|
||||||
|
NewMainErrorSpan(span, "query must end with a value"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err.Message = "Expected a RETURN or FOR clause at end of query"
|
||||||
|
err.Hint = "All queries must return a value. Add a RETURN statement to complete the query."
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
span := spanFromTokenSafe(offending.Token(), src)
|
span := spanFromTokenSafe(offending.Token(), src)
|
||||||
err.Message = fmt.Sprintf("Expected expression after '%s'", offending)
|
err.Message = fmt.Sprintf("Expected expression after '%s'", offending)
|
||||||
err.Hint = "Did you forget to provide a value to return?"
|
err.Hint = "Did you forget to provide a value to return?"
|
||||||
|
@@ -40,6 +40,10 @@ func (p *Parser) AddErrorListener(listener antlr.ErrorListener) {
|
|||||||
p.tree.AddErrorListener(listener)
|
p.tree.AddErrorListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) RemoveErrorListeners() {
|
||||||
|
p.tree.RemoveErrorListeners()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) Visit(visitor fql.FqlParserVisitor) interface{} {
|
func (p *Parser) Visit(visitor fql.FqlParserVisitor) interface{} {
|
||||||
return visitor.VisitProgram(p.tree.Program().(*fql.ProgramContext))
|
return visitor.VisitProgram(p.tree.Program().(*fql.ProgramContext))
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package base
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/smarty/assertions"
|
"github.com/smarty/assertions"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/compiler"
|
"github.com/MontFerret/ferret/pkg/compiler"
|
||||||
@@ -110,7 +111,13 @@ func ShouldBeCompilationError(actual any, expected ...any) string {
|
|||||||
err, ok := actual.(*compiler.CompilationError)
|
err, ok := actual.(*compiler.CompilationError)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return "expected a compilation error"
|
err2, ok := actual.(*compiler.MultiCompilationError)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return "expected a compilation error"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = err2.Errors[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
msg = assertExpectedError(err, ex)
|
msg = assertExpectedError(err, ex)
|
||||||
|
@@ -16,6 +16,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected a RETURN or FOR clause at end of query",
|
Message: "Expected a RETURN or FOR clause at end of query",
|
||||||
Hint: "All queries must return a value. Add a RETURN statement to complete the query.",
|
Hint: "All queries must return a value. Add a RETURN statement to complete the query.",
|
||||||
}, "Missing return statement"),
|
}, "Missing return statement"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
LET i = NONE
|
LET i = NONE
|
||||||
@@ -25,6 +26,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected expression after 'RETURN'",
|
Message: "Expected expression after 'RETURN'",
|
||||||
Hint: "Did you forget to provide a value to return?",
|
Hint: "Did you forget to provide a value to return?",
|
||||||
}, "Missing return value"),
|
}, "Missing return value"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
FOR i IN [1, 2, 3]
|
FOR i IN [1, 2, 3]
|
||||||
@@ -34,6 +36,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected expression after 'RETURN'",
|
Message: "Expected expression after 'RETURN'",
|
||||||
Hint: "Did you forget to provide a value to return?",
|
Hint: "Did you forget to provide a value to return?",
|
||||||
}, "Missing return value in for loop"),
|
}, "Missing return value in for loop"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
LET i =
|
LET i =
|
||||||
@@ -43,6 +46,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected expression after '=' for variable 'i'",
|
Message: "Expected expression after '=' for variable 'i'",
|
||||||
Hint: "Did you forget to provide a value?",
|
Hint: "Did you forget to provide a value?",
|
||||||
}, "Missing variable assignment value"),
|
}, "Missing variable assignment value"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
FOR i IN
|
FOR i IN
|
||||||
@@ -52,6 +56,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected expression after 'IN'",
|
Message: "Expected expression after 'IN'",
|
||||||
Hint: "Each FOR loop must iterate over a collection or range.",
|
Hint: "Each FOR loop must iterate over a collection or range.",
|
||||||
}, "Missing iterable in FOR"),
|
}, "Missing iterable in FOR"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
FOR i [1, 2, 3]
|
FOR i [1, 2, 3]
|
||||||
@@ -61,6 +66,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected 'IN' after loop variable",
|
Message: "Expected 'IN' after loop variable",
|
||||||
Hint: "Use 'FOR x IN [iterable]' syntax.",
|
Hint: "Use 'FOR x IN [iterable]' syntax.",
|
||||||
}, "Missing IN in FOR"),
|
}, "Missing IN in FOR"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
FOR IN [1, 2, 3]
|
FOR IN [1, 2, 3]
|
||||||
@@ -70,6 +76,7 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected loop variable before 'IN'",
|
Message: "Expected loop variable before 'IN'",
|
||||||
Hint: "FOR must declare a variable.",
|
Hint: "FOR must declare a variable.",
|
||||||
}, "FOR without variable"),
|
}, "FOR without variable"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
LET users = []
|
LET users = []
|
||||||
@@ -81,16 +88,77 @@ func TestSyntaxErrors(t *testing.T) {
|
|||||||
Message: "Expected variable before '=' in COLLECT",
|
Message: "Expected variable before '=' in COLLECT",
|
||||||
Hint: "COLLECT must group by a variable.",
|
Hint: "COLLECT must group by a variable.",
|
||||||
}, "COLLECT with no variable"),
|
}, "COLLECT with no variable"),
|
||||||
|
|
||||||
ErrorCase(
|
ErrorCase(
|
||||||
`
|
`
|
||||||
LET users = []
|
LET users = []
|
||||||
FOR x IN users
|
FOR x IN users
|
||||||
COLLECT i =
|
COLLECT AGGREGATE total =
|
||||||
|
RETURN total
|
||||||
|
`, E{
|
||||||
|
Kind: compiler.SyntaxError,
|
||||||
|
Message: "Expected expression after '=' for variable 'total'",
|
||||||
|
Hint: "Did you forget to provide a value?",
|
||||||
|
}, "COLLECT AGGREGATE without expression"),
|
||||||
|
|
||||||
|
ErrorCase(
|
||||||
|
`
|
||||||
|
LET users = []
|
||||||
|
FOR x IN users
|
||||||
|
FILTER
|
||||||
RETURN x
|
RETURN x
|
||||||
`, E{
|
`, E{
|
||||||
Kind: compiler.SyntaxError,
|
Kind: compiler.SyntaxError,
|
||||||
Message: "Expected expression after '=' for variable 'i'",
|
Message: "Expected condition after 'FILTER'",
|
||||||
Hint: "Did you forget to provide a value?",
|
Hint: "FILTER requires a boolean expression.",
|
||||||
}, "COLLECT with no variable assignment"),
|
}, "FILTER with no expression"),
|
||||||
|
|
||||||
|
ErrorCase(
|
||||||
|
`
|
||||||
|
LET users = []
|
||||||
|
FOR x IN users
|
||||||
|
LIMIT
|
||||||
|
RETURN x
|
||||||
|
`, E{
|
||||||
|
Kind: compiler.SyntaxError,
|
||||||
|
Message: "Expected number after 'LIMIT'",
|
||||||
|
Hint: "LIMIT requires a numeric value.",
|
||||||
|
}, "LIMIT with no value"),
|
||||||
|
|
||||||
|
ErrorCase(
|
||||||
|
`
|
||||||
|
LET users = []
|
||||||
|
FOR x IN users
|
||||||
|
LIMIT 1, 2, 3
|
||||||
|
RETURN x
|
||||||
|
`, E{
|
||||||
|
Kind: compiler.SyntaxError,
|
||||||
|
Message: "Too many arguments provided to LIMIT clause",
|
||||||
|
Hint: "LIMIT accepts at most two arguments: offset and count.",
|
||||||
|
}, "LIMIT with too many values"),
|
||||||
|
|
||||||
|
ErrorCase(
|
||||||
|
`
|
||||||
|
LET users = []
|
||||||
|
FOR x IN users
|
||||||
|
LIMIT 1, 2,
|
||||||
|
RETURN x
|
||||||
|
`, E{
|
||||||
|
Kind: compiler.SyntaxError,
|
||||||
|
Message: "Too many arguments provided to LIMIT clause",
|
||||||
|
Hint: "LIMIT accepts at most two arguments: offset and count.",
|
||||||
|
}, "LIMIT unexpected comma"),
|
||||||
|
|
||||||
|
ErrorCase(
|
||||||
|
`
|
||||||
|
LET users = []
|
||||||
|
FOR x IN users
|
||||||
|
LIMIT 1,
|
||||||
|
RETURN x
|
||||||
|
`, E{
|
||||||
|
Kind: compiler.SyntaxError,
|
||||||
|
Message: "---",
|
||||||
|
Hint: "FILTER requires a boolean expression.",
|
||||||
|
}, "LIMIT unexpected comma 2"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user