1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-08-13 19:52:52 +02:00

Add syntax error tests for FOR loops and COLLECT statements: enhance diagnostics for missing values, incomplete clauses, and unexpected syntax to improve error handling and clarity.

This commit is contained in:
Tim Voronov
2025-08-10 12:30:07 -04:00
parent f1dc2b12a1
commit b69f8af716
10 changed files with 355 additions and 137 deletions

View File

@@ -110,10 +110,26 @@ func is(node *TokenNode, expected string) bool {
return strings.ToUpper(node.GetText()) == expected
}
func anyIs(first, second *TokenNode, expected string) *TokenNode {
if is(first, expected) {
return first
}
if is(second, expected) {
return second
}
return nil
}
func has(msg string, substr string) bool {
return strings.Contains(strings.ToLower(msg), strings.ToLower(substr))
}
func isMismatched(msg string) bool {
return has(msg, "mismatched input")
}
func isNoAlternative(msg string) bool {
return has(msg, "no viable alternative at input")
}

View File

@@ -13,11 +13,11 @@ func matchMissingAssignmentValue(src *file.Source, err *CompilationError, offend
prev := offending.Prev()
if is(offending, "LET") || is(prev, "=") {
span := spanFromTokenSafe(prev.Token(), src)
if node := anyIs(offending, prev, "="); node != nil {
span := spanFromTokenSafe(node.Token(), src)
span.Start++
span.End++
err.Message = fmt.Sprintf("Expected expression after '=' for variable '%s'", prev.Prev())
err.Message = fmt.Sprintf("Expected expression after '=' for variable '%s'", node.Prev())
err.Hint = "Did you forget to provide a value?"
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing value"),
@@ -26,5 +26,18 @@ func matchMissingAssignmentValue(src *file.Source, err *CompilationError, offend
return true
}
if is(offending, "LET") {
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End
span.End = span.Start + 1
err.Message = "Expected variable name"
err.Hint = "Did you forget to provide a variable name?"
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing name"),
}
return true
}
return false
}

View File

@@ -19,5 +19,9 @@ func matchCommonErrors(src *file.Source, err *CompilationError, offending *Token
}
}
if isMismatched(err.Message) {
}
return false
}

View File

@@ -62,6 +62,48 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke
NewMainErrorSpan(span, "missing variable"),
}
return true
} else if isNoAlternative(msg) {
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End
span.End = span.Start + 1
err.Message = "Incomplete COLLECT clause"
err.Hint = "COLLECT must specify a grouping key, an AGGREGATE clause, or WITH COUNT."
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing grouping or aggregation"),
}
return true
}
}
if is(offending, "INTO") {
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End + 1
span.End = span.Start + 1
err.Message = "Expected variable name after INTO"
err.Hint = "Provide a variable name to store grouped values, e.g. INTO groups."
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing variable name"),
}
return true
}
if is(offending, "AGGREGATE") {
if isNoAlternative(err.Message) {
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End + 1
span.End = span.Start + 1
err.Message = "Expected variable assignment after AGGREGATE"
err.Hint = "Provide at least one variable assignment, e.g. AGGREGATE total = COUNT(x)."
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing variable assignment"),
}
return true
}
}
@@ -71,7 +113,7 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke
span.Start = span.End
span.End = span.Start + 1
err.Message = "Expected condition after 'FILTER'"
err.Message = "Incomplete FILTER clause"
err.Hint = "FILTER requires a boolean expression."
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing expression"),

View File

@@ -26,6 +26,8 @@ func matchMissingReturnValue(src *file.Source, err *CompilationError, offending
}
span := spanFromTokenSafe(offending.Token(), src)
span.Start = span.End
span.End = span.Start + 1
err.Message = fmt.Sprintf("Expected expression after '%s'", offending)
err.Hint = "Did you forget to provide a value to return?"
err.Spans = []ErrorSpan{

View File

@@ -13,3 +13,15 @@ func SkipWhitespaceForward(content string, offset int) int {
return offset
}
func SkipHorizontalWhitespaceForward(content string, offset int) int {
for offset < len(content) {
ch := content[offset]
// Skip spaces and tabs only; do NOT cross line breaks
if ch != ' ' && ch != '\t' {
break
}
offset++
}
return offset
}

View File

@@ -34,7 +34,6 @@ func NewSnippetWithCaret(lines []string, span Span, line int) Snippet {
endCol := computeVisualOffset(srcLine, span.End-lineStartOffset+1)
caret := ""
if endCol <= startCol+1 {
caret = strings.Repeat(" ", startCol) + "^"
} else {

View File

@@ -43,20 +43,36 @@ func (s *Source) LocationAt(span Span) (line, column int) {
return 0, 0
}
total := 0
offset := span.Start
total := 0
for i, l := range s.lines {
lineLen := len(l) + 1 // +1 for newline
lineLen := len(l) + 1 // +1 for '\n'
lineStart := total
lineEndWithNL := total + lineLen
if total+lineLen > offset {
// If offset is exactly at the start of this line (not the very first line),
// treat it as the end of the previous line.
if offset == lineStart && i > 0 {
prev := s.lines[i-1]
return i, len(prev) + 1
}
if lineEndWithNL > offset {
// Normal case: offset lives on this line
return i + 1, offset - total + 1
}
total += lineLen
total = lineEndWithNL
}
return total, 1
// If we somehow fell through, clamp to last line end
if len(s.lines) > 0 {
last := s.lines[len(s.lines)-1]
return len(s.lines), len(last) + 1
}
return 0, 0
}
func (s *Source) Snippet(span Span) []Snippet {

View File

@@ -0,0 +1,197 @@
package compiler_test
import (
"testing"
"github.com/MontFerret/ferret/pkg/compiler"
)
func TestForLoopSyntaxErrors(t *testing.T) {
RunUseCases(t, []UseCase{
ErrorCase(
`
FOR i IN [1, 2, 3]
RETURN
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'RETURN'",
Hint: "Did you forget to provide a value to return?",
}, "Missing return value in for loop"),
ErrorCase(
`
FOR i IN
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'IN'",
Hint: "Each FOR loop must iterate over a collection or range.",
}, "Missing iterable in FOR"),
ErrorCase(
`
FOR i [1, 2, 3]
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected 'IN' after loop variable",
Hint: "Use 'FOR x IN [iterable]' syntax.",
}, "Missing IN in FOR"),
ErrorCase(
`
FOR IN [1, 2, 3]
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected loop variable before 'IN'",
Hint: "FOR must declare a variable.",
}, "FOR without variable"),
ErrorCase(
`
LET users = []
FOR x IN users
FILTER
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Incomplete FILTER clause",
Hint: "FILTER requires a boolean expression.",
}, "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: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 2"),
ErrorCase(
`
LET users = []
FOR x IN users
LIMIT ,
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 3"),
ErrorCase(
`
LET users = []
FOR x IN users
LIMIT,
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 4"),
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
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Incomplete COLLECT clause",
Hint: "COLLECT must specify a grouping key, an AGGREGATE clause, or WITH COUNT.",
}, "COLLECT with no variables"),
ErrorCase(
`
LET users = []
FOR i IN users
COLLECT gender = i.gender INTO
RETURN {
gender,
values
}`, E{
Kind: compiler.SyntaxError,
Message: "Expected variable name after INTO",
Hint: "Provide a variable name to store grouped values, e.g. INTO groups.",
}, "COLLECT INTO with no variable"),
ErrorCase(
`
LET users = []
FOR x IN users
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
COLLECT AGGREGATE
RETURN total
`, E{
Kind: compiler.SyntaxError,
Message: "Expected variable assignment after AGGREGATE",
Hint: "Provide at least one variable assignment, e.g. AGGREGATE total = COUNT(x).",
}, "COLLECT AGGREGATE without expression 2"),
})
}

View File

@@ -8,6 +8,25 @@ import (
func TestSyntaxErrors(t *testing.T) {
RunUseCases(t, []UseCase{
ErrorCase(
`
LET
`, E{
Kind: compiler.SyntaxError,
Message: "Expected variable name",
Hint: "Did you forget to provide a variable name?",
}, "Missing variable name"),
ErrorCase(
`
LET
RETURN 5
`, E{
Kind: compiler.SyntaxError,
Message: "Expected variable name",
Hint: "Did you forget to provide a variable name?",
}, "Missing variable name 2"),
ErrorCase(
`
LET i = NONE
@@ -27,16 +46,6 @@ func TestSyntaxErrors(t *testing.T) {
Hint: "Did you forget to provide a value to return?",
}, "Missing return value"),
ErrorCase(
`
FOR i IN [1, 2, 3]
RETURN
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'RETURN'",
Hint: "Did you forget to provide a value to return?",
}, "Missing return value in for loop"),
ErrorCase(
`
LET i =
@@ -49,138 +58,46 @@ func TestSyntaxErrors(t *testing.T) {
ErrorCase(
`
FOR i IN
RETURN i
LET i =
LET j = 5
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'IN'",
Hint: "Each FOR loop must iterate over a collection or range.",
}, "Missing iterable in FOR"),
ErrorCase(
`
FOR i [1, 2, 3]
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected 'IN' after loop variable",
Hint: "Use 'FOR x IN [iterable]' syntax.",
}, "Missing IN in FOR"),
ErrorCase(
`
FOR IN [1, 2, 3]
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected loop variable before 'IN'",
Hint: "FOR must declare a 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 AGGREGATE total =
RETURN total
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after '=' for variable 'total'",
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "COLLECT AGGREGATE without expression"),
}, "Missing variable assignment value 2"),
ErrorCase(
`
LET users = []
FOR x IN users
FILTER
RETURN x
LET i =
FOR j IN [1, 2, 3] RETURN j
`, E{
Kind: compiler.SyntaxError,
Message: "Expected condition after 'FILTER'",
Hint: "FILTER requires a boolean expression.",
}, "FILTER with no expression"),
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "Missing variable assignment value 3"),
ErrorCase(
SkipErrorCase(
`
LET users = []
FOR x IN users
LIMIT
RETURN x
LET o = { foo: "bar" }
LET i = o.
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected number after 'LIMIT'",
Hint: "LIMIT requires a numeric value.",
}, "LIMIT with no value"),
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "Incomplete member access"),
ErrorCase(
SkipErrorCase(
`
LET users = []
FOR x IN users
LIMIT 1, 2, 3
RETURN x
LET o = { foo: "bar" }
LET i = o.
FUNC(i)
RETURN i
`, 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: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 2"),
ErrorCase(
`
LET users = []
FOR x IN users
LIMIT ,
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 3"),
ErrorCase(
`
LET users = []
FOR x IN users
LIMIT,
RETURN x
`, E{
Kind: compiler.SyntaxError,
Message: "Dangling comma in LIMIT clause",
Hint: "LIMIT accepts one or two arguments. Did you forget to add a value?",
}, "LIMIT unexpected comma 4"),
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "Incomplete member access 2"),
})
}