diff --git a/pkg/compiler/internal/diagnostics/error_analyzer.go b/pkg/compiler/internal/diagnostics/error_analyzer.go index afe4527b..0c9b402a 100644 --- a/pkg/compiler/internal/diagnostics/error_analyzer.go +++ b/pkg/compiler/internal/diagnostics/error_analyzer.go @@ -10,6 +10,7 @@ func AnalyzeSyntaxError(src *file.Source, err *CompilationError, offending *Toke matchers := []SyntaxErrorMatcher{ matchMissingAssignmentValue, matchForLoopErrors, + matchCommonErrors, matchMissingReturnValue, } diff --git a/pkg/compiler/internal/diagnostics/helpers.go b/pkg/compiler/internal/diagnostics/helpers.go index 56d7c38f..21e15c6e 100644 --- a/pkg/compiler/internal/diagnostics/helpers.go +++ b/pkg/compiler/internal/diagnostics/helpers.go @@ -114,33 +114,24 @@ func has(msg string, substr string) bool { return strings.Contains(strings.ToLower(msg), strings.ToLower(substr)) } +func isNoAlternative(msg string) bool { + return has(msg, "no viable alternative at input") +} + +func extractNoAlternativeInput(msg string) string { + re := regexp.MustCompile(`no viable alternative at input\s+(?P.+)`) + match := re.FindStringSubmatch(msg) + + return strings.Trim(match[re.SubexpIndex("input")], "'") +} + func isExtraneous(msg string) bool { return has(msg, "extraneous input") } -func parseExtraneousInput(msg string) string { +func extractExtraneousInput(msg string) string { re := regexp.MustCompile(`extraneous input\s+(?P.+?)\s+expecting`) match := re.FindStringSubmatch(msg) - return match[re.SubexpIndex("input")] -} - -func parseExtraneousInputAll(msg string) (string, []string) { - rx := regexp.MustCompile(`extraneous input\s+(?P.+?)\s+expecting\s+\{(?P.+?)\}`) - 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 + + return strings.Trim(match[re.SubexpIndex("input")], "'") } diff --git a/pkg/compiler/internal/diagnostics/match_common_errors.go b/pkg/compiler/internal/diagnostics/match_common_errors.go new file mode 100644 index 00000000..316592c5 --- /dev/null +++ b/pkg/compiler/internal/diagnostics/match_common_errors.go @@ -0,0 +1,23 @@ +package diagnostics + +import "github.com/MontFerret/ferret/pkg/file" + +func matchCommonErrors(src *file.Source, err *CompilationError, offending *TokenNode) bool { + if isNoAlternative(err.Message) { + if is(offending.Prev(), ",") { + span := spanFromTokenSafe(offending.Prev().Token(), src) + span.Start++ + span.End++ + + err.Message = "Expected expression after ','" + err.Hint = "Did you forget to provide a value?" + err.Spans = []ErrorSpan{ + NewMainErrorSpan(span, "missing value"), + } + + return true + } + } + + return false +} diff --git a/pkg/compiler/internal/diagnostics/match_for_loop_errors.go b/pkg/compiler/internal/diagnostics/match_for_loop_errors.go index 6a4bc917..ff8fef06 100644 --- a/pkg/compiler/internal/diagnostics/match_for_loop_errors.go +++ b/pkg/compiler/internal/diagnostics/match_for_loop_errors.go @@ -1,6 +1,8 @@ package diagnostics import ( + "strings" + "github.com/MontFerret/ferret/pkg/file" ) @@ -93,9 +95,9 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke } if isExtraneous(err.Message) { - input := parseExtraneousInput(err.Message) + input := extractExtraneousInput(err.Message) - if input != "','" { + if input != "," { return false } @@ -122,5 +124,47 @@ func matchForLoopErrors(src *file.Source, err *CompilationError, offending *Toke } } + if isNoAlternative(err.Message) { + if is(prev, ",") { + var steps int + + // We walk back two tokens to find if the keyword is LIMIT. + for ; steps < 2 && !is(prev, "LIMIT"); steps++ { + prev = prev.Prev() + } + + if is(prev, "LIMIT") { + span := spanFromTokenSafe(offending.Prev().Token(), src) + span.Start++ + span.End++ + + err.Message = "Dangling comma in LIMIT clause" + err.Hint = "LIMIT accepts one or two arguments. Did you forget to add a value?" + err.Spans = []ErrorSpan{ + NewMainErrorSpan(span, "missing value"), + } + + return true + } + } else if is(offending, "LIMIT") { + input := extractNoAlternativeInput(err.Message) + tokens := strings.Fields(input) + + if len(tokens) > 0 && has(tokens[len(tokens)-1], ",") { + span := spanFromTokenSafe(offending.Token(), src) + span.Start = span.End + span.End = span.Start + 1 + + err.Message = "Dangling comma in LIMIT clause" + err.Hint = "LIMIT accepts one or two arguments. Did you forget to add a value?" + err.Spans = []ErrorSpan{ + NewMainErrorSpan(span, "missing value"), + } + + return true + } + } + } + return false } diff --git a/test/integration/compiler/compiler_errors_syntax_test.go b/test/integration/compiler/compiler_errors_syntax_test.go index 848b220f..60ecdca2 100644 --- a/test/integration/compiler/compiler_errors_syntax_test.go +++ b/test/integration/compiler/compiler_errors_syntax_test.go @@ -157,8 +157,30 @@ func TestSyntaxErrors(t *testing.T) { RETURN x `, E{ Kind: compiler.SyntaxError, - Message: "---", - Hint: "FILTER requires a boolean expression.", + 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"), }) }