1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-08-15 20:02:56 +02:00

Refactor error handling for FOR and COLLECT statements: enhance error messages for missing variables, improve error span tracking, and streamline error analysis for better clarity and maintainability.

This commit is contained in:
Tim Voronov
2025-08-06 16:30:51 -04:00
parent 0edbdc6a1b
commit f4dab1e3a3
6 changed files with 92 additions and 15 deletions

View File

@@ -38,7 +38,11 @@ func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interfac
offending = tok offending = tok
} }
d.handler.Add(d.parseError(msg, offending)) if err := d.parseError(msg, offending); err != nil {
if !d.handler.HasErrorOnLine(line) {
d.handler.Add(err)
}
}
} }
func (d *ErrorListener) parseError(msg string, offending antlr.Token) *CompilationError { func (d *ErrorListener) parseError(msg string, offending antlr.Token) *CompilationError {
@@ -55,8 +59,9 @@ func (d *ErrorListener) parseError(msg string, offending antlr.Token) *Compilati
} }
for _, handler := range []func(*CompilationError) bool{ for _, handler := range []func(*CompilationError) bool{
d.extraneousError, d.extraneousInput,
d.noViableAltError, d.noViableAlternative,
d.mismatchedInput,
} { } {
if handler(err) { if handler(err) {
break break
@@ -66,7 +71,7 @@ func (d *ErrorListener) parseError(msg string, offending antlr.Token) *Compilati
return err return err
} }
func (d *ErrorListener) extraneousError(err *CompilationError) (matched bool) { func (d *ErrorListener) extraneousInput(err *CompilationError) (matched bool) {
if !strings.Contains(err.Message, "extraneous input") { if !strings.Contains(err.Message, "extraneous input") {
return false return false
} }
@@ -88,10 +93,18 @@ func (d *ErrorListener) extraneousError(err *CompilationError) (matched bool) {
return true return true
} }
func (d *ErrorListener) noViableAltError(err *CompilationError) bool { func (d *ErrorListener) noViableAlternative(err *CompilationError) bool {
if !strings.Contains(err.Message, "viable alternative at input") { if !strings.Contains(err.Message, "viable alternative at input") {
return false return false
} }
return AnalyzeSyntaxError(d.src, err, d.history.Last()) 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())
}

View File

@@ -9,9 +9,10 @@ import (
) )
type ErrorHandler struct { type ErrorHandler struct {
src *file.Source src *file.Source
errors []*CompilationError errors []*CompilationError
threshold int linesWithErrors map[int]bool
threshold int
} }
func NewErrorHandler(src *file.Source, threshold int) *ErrorHandler { func NewErrorHandler(src *file.Source, threshold int) *ErrorHandler {
@@ -20,9 +21,10 @@ func NewErrorHandler(src *file.Source, threshold int) *ErrorHandler {
} }
return &ErrorHandler{ return &ErrorHandler{
src: src, src: src,
errors: make([]*CompilationError, 0), errors: make([]*CompilationError, 0),
threshold: threshold, linesWithErrors: make(map[int]bool),
threshold: threshold,
} }
} }
@@ -60,6 +62,13 @@ func (h *ErrorHandler) Add(err *CompilationError) {
err.Source = h.src err.Source = h.src
} }
for _, span := range err.Spans {
if err.Source != nil {
line, _ := err.Source.LocationAt(span.Span)
h.linesWithErrors[line] = true
}
}
h.errors = append(h.errors, err) h.errors = append(h.errors, err)
if len(h.errors) == h.threshold { if len(h.errors) == h.threshold {
@@ -71,6 +80,10 @@ func (h *ErrorHandler) Add(err *CompilationError) {
} }
} }
func (h *ErrorHandler) HasErrorOnLine(line int) bool {
return h.linesWithErrors[line]
}
func (h *ErrorHandler) VariableNotUnique(ctx antlr.ParserRuleContext, name string) { func (h *ErrorHandler) VariableNotUnique(ctx antlr.ParserRuleContext, name string) {
// TODO: Add information where the variable was defined // TODO: Add information where the variable was defined
h.Add(&CompilationError{ h.Add(&CompilationError{

View File

@@ -107,3 +107,7 @@ func is(node *TokenNode, expected string) bool {
return strings.ToUpper(node.GetText()) == expected return strings.ToUpper(node.GetText()) == expected
} }
func has(msg string, substr string) bool {
return strings.Contains(strings.ToLower(msg), strings.ToLower(substr))
}

View File

@@ -46,5 +46,23 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke
return true return true
} }
if is(offending, "COLLECT") {
msg := err.Message
if has(msg, "COLLECT =") {
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End
span.End = span.Start + 1
err.Message = "Expected variable before '=' in COLLECT"
err.Hint = "COLLECT must group by a variable."
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing variable"),
}
return true
}
}
return false return false
} }

View File

@@ -23,8 +23,15 @@ func NewSnippetWithCaret(lines []string, span Span, line int) Snippet {
} }
srcLine := lines[line-1] srcLine := lines[line-1]
startCol := computeVisualOffset(srcLine, span.Start) lineStartOffset := 0
endCol := computeVisualOffset(srcLine, span.End)
// Compute actual start-of-line offset
for i := 0; i < line-1; i++ {
lineStartOffset += len(lines[i]) + 1 // +1 for \n
}
startCol := computeVisualOffset(srcLine, span.Start-lineStartOffset+1)
endCol := computeVisualOffset(srcLine, span.End-lineStartOffset+1)
caret := "" caret := ""

View File

@@ -67,8 +67,30 @@ func TestSyntaxErrors(t *testing.T) {
RETURN i RETURN i
`, E{ `, E{
Kind: compiler.SyntaxError, Kind: compiler.SyntaxError,
Message: "--", Message: "Expected loop variable before 'IN'",
Hint: "Use 'FOR x IN [iterable]' syntax.", Hint: "FOR must declare a variable.",
}, "FOR without variable"), }, "FOR without variable"),
ErrorCase(
`
LET users = []
FOR x IN users
COLLECT =
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Expected variable before '=' in COLLECT",
Hint: "COLLECT must group by a variable.",
}, "COLLECT with no variable"),
ErrorCase(
`
LET users = []
FOR x IN users
COLLECT i =
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "COLLECT with no variable assignment"),
}) })
} }