mirror of
https://github.com/MontFerret/ferret.git
synced 2026-06-20 01:17:53 +02:00
Chore/improved syntax error message (#932)
* feat: add diagnostics for missing commas between MATCH arms * feat: enhance syntax error diagnostics for mixed function body usage * feat: improve diagnostics for missing function parameters before function body
This commit is contained in:
@@ -76,19 +76,25 @@ func (c *Compiler) Compile(src *source.Source) (program *bytecode.Program, err e
|
||||
}
|
||||
}()
|
||||
|
||||
l := NewVisitor(src, errorHandler, c.opts.Level)
|
||||
tokenHistory := parserd.NewTokenHistory(10)
|
||||
tokenHistory := parserd.NewTokenHistory(64)
|
||||
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
|
||||
return parserd.NewTrackingTokenStream(stream, tokenHistory)
|
||||
})
|
||||
// Remove all default error listeners
|
||||
p.RemoveErrorListeners()
|
||||
// Add custom error listener
|
||||
p.AddErrorListener(parserd.NewErrorListener(src, l.Session.Program.Errors, tokenHistory))
|
||||
p.AddErrorListener(parserd.NewErrorListener(src, errorHandler, tokenHistory))
|
||||
p.Program()
|
||||
|
||||
if errorHandler.HasErrors() {
|
||||
return nil, errorHandler.Unwrap()
|
||||
}
|
||||
|
||||
l := NewVisitor(src, errorHandler, c.opts.Level)
|
||||
p.Visit(l)
|
||||
|
||||
if l.Session.Program.Errors.HasErrors() {
|
||||
return nil, l.Session.Program.Errors.Unwrap()
|
||||
if errorHandler.HasErrors() {
|
||||
return nil, errorHandler.Unwrap()
|
||||
}
|
||||
|
||||
var udfs []bytecode.UDF
|
||||
|
||||
@@ -59,7 +59,7 @@ func (fmt *Formatter) Format(out io.Writer, src *source.Source) error {
|
||||
}
|
||||
}()
|
||||
|
||||
tokenHistory := parserd.NewTokenHistory(10)
|
||||
tokenHistory := parserd.NewTokenHistory(64)
|
||||
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
|
||||
return parserd.NewTrackingTokenStream(stream, tokenHistory)
|
||||
})
|
||||
@@ -67,6 +67,12 @@ func (fmt *Formatter) Format(out io.Writer, src *source.Source) error {
|
||||
p.RemoveErrorListeners()
|
||||
// Add custom error listener
|
||||
p.AddErrorListener(parserd.NewErrorListener(src, errorHandler, tokenHistory))
|
||||
p.Program()
|
||||
|
||||
if errorHandler.HasErrors() {
|
||||
return errorHandler.Unwrap()
|
||||
}
|
||||
|
||||
l := internal.NewVisitor(src, out, fmt.opts)
|
||||
p.Visit(l)
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ func AnalyzeSyntaxError(src *source.Source, err *diagnostics.Diagnostic, offendi
|
||||
matchWaitForErrors,
|
||||
matchMissingAssignmentValue,
|
||||
matchForLoopErrors,
|
||||
matchMatchArmSeparatorErrors,
|
||||
matchMissingFunctionParamsClose,
|
||||
matchMixedFunctionBodySyntax,
|
||||
matchCommonErrors,
|
||||
matchMissingReturnValue,
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
|
||||
type ErrorListener struct {
|
||||
*antlr.DiagnosticErrorListener
|
||||
src *source.Source
|
||||
handler *ErrorHandler
|
||||
history *TokenHistory
|
||||
src *source.Source
|
||||
handler *ErrorHandler
|
||||
history *TokenHistory
|
||||
stopAfterSyntaxError bool
|
||||
}
|
||||
|
||||
func NewErrorListener(src *source.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener {
|
||||
@@ -47,6 +48,10 @@ func (d *ErrorListener) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA,
|
||||
}
|
||||
|
||||
func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
|
||||
if d.stopAfterSyntaxError {
|
||||
return
|
||||
}
|
||||
|
||||
var offending antlr.Token
|
||||
|
||||
// Get offending token
|
||||
@@ -57,6 +62,7 @@ func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interfac
|
||||
if !d.handler.HasErrorOnLine(line) {
|
||||
if err := d.parseError(msg, offending); err != nil {
|
||||
d.handler.Add(err)
|
||||
d.stopAfterSyntaxError = isFunctionBodySyntaxDiagnostic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message) {
|
||||
prev := offending.Prev()
|
||||
if node := anyIs(prev, offending, "=>"); node != nil {
|
||||
if node := anyIs(prev, offending, "=>"); node != nil && !isArrowBodyStart(node, offending) {
|
||||
span := spanFromTokenSafe(node.Token(), src)
|
||||
span.Start += 2
|
||||
span.End += 2
|
||||
@@ -276,6 +276,19 @@ func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offendin
|
||||
return false
|
||||
}
|
||||
|
||||
func isArrowBodyStart(arrow, offending *TokenNode) bool {
|
||||
if arrow == nil || offending == nil || arrow == offending {
|
||||
return false
|
||||
}
|
||||
|
||||
switch offending.GetText() {
|
||||
case "", "<EOF>", ",", ")", "]", "}", "=>", ":", "?":
|
||||
return false
|
||||
default:
|
||||
return isExpressionStart(offending)
|
||||
}
|
||||
}
|
||||
|
||||
func findGroupingParen(node *TokenNode, max int) *TokenNode {
|
||||
current := node
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
const (
|
||||
missingFunctionParamsCloseMessage = "Expected function parameters before function body"
|
||||
missingFunctionParamsCloseLabel = "missing parameter list before function body"
|
||||
missingFunctionParamsCloseHint = "Add a parameter list before the block body, e.g. FUNC fib(n) ( ... RETURN expr ). Use FUNC fib() ( ... ) for no parameters."
|
||||
|
||||
mixedFunctionBodyMessage = "Cannot combine arrow and block function body syntax"
|
||||
mixedFunctionBodyLabel = "RETURN is only valid in a block function body"
|
||||
mixedFunctionBodyNote = "Remove '=>' to use a block body, or remove RETURN and keep a single expression after '=>'."
|
||||
mixedFunctionBodyHint = "Use either 'FUNC f(x) => expr' or 'FUNC f(x) ( ... RETURN expr )'."
|
||||
)
|
||||
|
||||
func matchMissingFunctionParamsClose(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if src == nil || err == nil || offending == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isNoAlternative(err.Message) && !isMissing(err.Message) && !isMismatched(err.Message) {
|
||||
return false
|
||||
}
|
||||
|
||||
tokens := lexDefaultTokens(src.Content())
|
||||
offendingIdx := findDiagnosticSpanTokenIndex(tokens, err)
|
||||
if offendingIdx < 0 || !isFunctionBlockBodyStartToken(tokens[offendingIdx]) {
|
||||
return false
|
||||
}
|
||||
|
||||
paramsOpenIdx := findUnclosedFunctionParamsOpen(tokens, offendingIdx)
|
||||
if paramsOpenIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
err.Message = missingFunctionParamsCloseMessage
|
||||
err.Hint = missingFunctionParamsCloseHint
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(spanFromTokenSafe(tokens[paramsOpenIdx], src), missingFunctionParamsCloseLabel),
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func matchMixedFunctionBodySyntax(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if src == nil || err == nil || offending == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isNoAlternative(err.Message) && !isMissing(err.Message) && !isMismatched(err.Message) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !is(offending, "RETURN") {
|
||||
return false
|
||||
}
|
||||
|
||||
openParen := offending.Prev()
|
||||
if !is(openParen, "(") || !is(openParen.Prev(), "=>") {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isFunctionDeclarationArrow(openParen.Prev()) {
|
||||
return false
|
||||
}
|
||||
|
||||
err.Message = mixedFunctionBodyMessage
|
||||
err.Hint = mixedFunctionBodyHint
|
||||
err.Note = mixedFunctionBodyNote
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(spanFromTokenSafe(offending.Token(), src), mixedFunctionBodyLabel),
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isFunctionBodySyntaxDiagnostic(err *diagnostics.Diagnostic) bool {
|
||||
return isMissingFunctionParamsCloseDiagnostic(err) || isMixedFunctionBodySyntaxDiagnostic(err)
|
||||
}
|
||||
|
||||
func isMissingFunctionParamsCloseDiagnostic(err *diagnostics.Diagnostic) bool {
|
||||
return err != nil && err.Kind == SyntaxError && err.Message == missingFunctionParamsCloseMessage
|
||||
}
|
||||
|
||||
func isMixedFunctionBodySyntaxDiagnostic(err *diagnostics.Diagnostic) bool {
|
||||
return err != nil && err.Kind == SyntaxError && err.Message == mixedFunctionBodyMessage
|
||||
}
|
||||
|
||||
func isFunctionDeclarationArrow(arrow *TokenNode) bool {
|
||||
if !is(arrow, "=>") {
|
||||
return false
|
||||
}
|
||||
|
||||
paramsClose := arrow.Prev()
|
||||
if !is(paramsClose, ")") {
|
||||
return false
|
||||
}
|
||||
|
||||
depth := 0
|
||||
for current := paramsClose; current != nil; current = current.Prev() {
|
||||
switch {
|
||||
case is(current, ")"):
|
||||
depth++
|
||||
case is(current, "("):
|
||||
depth--
|
||||
if depth == 0 {
|
||||
name := current.Prev()
|
||||
return isFunctionNameToken(name) && is(name.Prev(), "FUNC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func findDiagnosticSpanTokenIndex(tokens []antlr.Token, err *diagnostics.Diagnostic) int {
|
||||
if err == nil || len(err.Spans) == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
span := err.Spans[0].Span
|
||||
for idx, token := range tokens {
|
||||
if token.GetStart() == span.Start && token.GetStop()+1 == span.End {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func isFunctionBlockBodyStartToken(token antlr.Token) bool {
|
||||
return isTokenText(token, "RETURN") ||
|
||||
isTokenText(token, "LET") ||
|
||||
isTokenText(token, "VAR") ||
|
||||
isTokenText(token, "FUNC") ||
|
||||
isTokenText(token, "WAITFOR") ||
|
||||
isTokenText(token, "DISPATCH")
|
||||
}
|
||||
|
||||
func findUnclosedFunctionParamsOpen(tokens []antlr.Token, offendingIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := offendingIdx - 1; i >= 0; i-- {
|
||||
switch tokenText(tokens[i]) {
|
||||
case ")":
|
||||
depth++
|
||||
case "(":
|
||||
if depth > 0 {
|
||||
depth--
|
||||
continue
|
||||
}
|
||||
|
||||
if i >= 2 && isTokenText(tokens[i-2], "FUNC") {
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func isFunctionNameToken(node *TokenNode) bool {
|
||||
if node == nil || node.Token() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isIdentifier(node) || isKeyword(node)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/parser/fql"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
const (
|
||||
missingMatchArmCommaMessage = "Expected ',' between MATCH arms"
|
||||
missingMatchArmCommaHint = "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0."
|
||||
)
|
||||
|
||||
func matchMatchArmSeparatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if !(isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if src == nil || offending == nil || offending.Token() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tokens := lexDefaultTokens(src.Content())
|
||||
offendingIdx := findLexedTokenIndex(tokens, offending.Token())
|
||||
if offendingIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for arrowIdx := 0; arrowIdx < len(tokens); arrowIdx++ {
|
||||
if !isTokenText(tokens[arrowIdx], "=>") || !isInsideMatchArms(tokens, arrowIdx) {
|
||||
continue
|
||||
}
|
||||
|
||||
nextArrowIdx, ok := findNextTopLevelArrowBeforeSeparator(tokens, arrowIdx+1)
|
||||
if !ok || nextArrowIdx <= arrowIdx+1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if offendingIdx < arrowIdx || offendingIdx > nextArrowIdx {
|
||||
continue
|
||||
}
|
||||
|
||||
span, ok := matchArmSeparatorInsertionSpan(tokens, arrowIdx, nextArrowIdx, src)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
err.Message = missingMatchArmCommaMessage
|
||||
err.Hint = missingMatchArmCommaHint
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, "missing comma"),
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func lexDefaultTokens(input string) []antlr.Token {
|
||||
stream := antlr.NewInputStream(input)
|
||||
lexer := fql.NewFqlLexer(stream)
|
||||
lexer.RemoveErrorListeners()
|
||||
tokens := make([]antlr.Token, 0)
|
||||
|
||||
for {
|
||||
token := lexer.NextToken()
|
||||
if token == nil || token.GetTokenType() == antlr.TokenEOF {
|
||||
break
|
||||
}
|
||||
|
||||
if token.GetChannel() == antlr.TokenDefaultChannel {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func findLexedTokenIndex(tokens []antlr.Token, token antlr.Token) int {
|
||||
if token == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
for idx, candidate := range tokens {
|
||||
if candidate.GetStart() == token.GetStart() && candidate.GetStop() == token.GetStop() {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func isInsideMatchArms(tokens []antlr.Token, idx int) bool {
|
||||
openIdx := findEnclosingOpenParen(tokens, idx)
|
||||
if openIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
depth := 0
|
||||
for i := openIdx - 1; i >= 0; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")", "]", "}":
|
||||
depth++
|
||||
case "(", "[", "{":
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
case "MATCH":
|
||||
if depth == 0 {
|
||||
return true
|
||||
}
|
||||
case "FUNC", "RETURN", "LET", "VAR", "FOR", "USE":
|
||||
if depth == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func findEnclosingOpenParen(tokens []antlr.Token, idx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := idx - 1; i >= 0; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")":
|
||||
depth++
|
||||
case "(":
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
|
||||
depth--
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func findNextTopLevelArrowBeforeSeparator(tokens []antlr.Token, startIdx int) (int, bool) {
|
||||
depth := 0
|
||||
|
||||
for i := startIdx; i < len(tokens); i++ {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case "(", "[", "{":
|
||||
depth++
|
||||
case ")", "]", "}":
|
||||
if depth == 0 {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
depth--
|
||||
case ",":
|
||||
if depth == 0 {
|
||||
return -1, false
|
||||
}
|
||||
case "=>":
|
||||
if depth == 0 {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func matchArmSeparatorInsertionSpan(tokens []antlr.Token, currentArrowIdx, nextArrowIdx int, src *source.Source) (source.Span, bool) {
|
||||
nextArmStartIdx := matchArmStartTokenIndex(tokens, currentArrowIdx, nextArrowIdx)
|
||||
if nextArmStartIdx <= currentArrowIdx+1 {
|
||||
return source.Span{}, false
|
||||
}
|
||||
|
||||
previousValue := tokens[nextArmStartIdx-1]
|
||||
if previousValue == nil {
|
||||
return source.Span{}, false
|
||||
}
|
||||
|
||||
offset := previousValue.GetStop() + 1
|
||||
if offset < 0 || offset >= len(src.Content()) {
|
||||
return spanFromTokenSafe(previousValue, src), true
|
||||
}
|
||||
|
||||
return source.Span{Start: offset, End: offset + 1}, true
|
||||
}
|
||||
|
||||
func matchArmStartTokenIndex(tokens []antlr.Token, currentArrowIdx, nextArrowIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := nextArrowIdx - 1; i > currentArrowIdx; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")", "]", "}":
|
||||
depth++
|
||||
case "(", "[", "{":
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
case "WHEN":
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextArrowIdx - 1
|
||||
}
|
||||
|
||||
func isTokenText(token antlr.Token, expected string) bool {
|
||||
return tokenText(token) == expected
|
||||
}
|
||||
|
||||
func tokenText(token antlr.Token) string {
|
||||
if token == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToUpper(token.GetText())
|
||||
}
|
||||
+14
-3
@@ -9,7 +9,8 @@ import (
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
tree *fql.FqlParser
|
||||
tree *fql.FqlParser
|
||||
program *fql.ProgramContext
|
||||
}
|
||||
|
||||
func New(query string, tr ...TokenStreamTransformer) *Parser {
|
||||
@@ -45,10 +46,20 @@ func (p *Parser) RemoveErrorListeners() {
|
||||
p.tree.RemoveErrorListeners()
|
||||
}
|
||||
|
||||
// Program parses and returns the source program, caching the parse tree so
|
||||
// syntax diagnostics can be inspected before visitors consume it.
|
||||
func (p *Parser) Program() *fql.ProgramContext {
|
||||
if p.program == nil {
|
||||
p.program = p.tree.Program().(*fql.ProgramContext)
|
||||
}
|
||||
|
||||
return p.program
|
||||
}
|
||||
|
||||
func (p *Parser) Visit(visitor fql.FqlParserVisitor) interface{} {
|
||||
return visitor.VisitProgram(p.tree.Program().(*fql.ProgramContext))
|
||||
return visitor.VisitProgram(p.Program())
|
||||
}
|
||||
|
||||
func (p *Parser) Walk(listener fql.FqlParserListener) {
|
||||
antlr.ParseTreeWalkerDefault.Walk(listener, p.tree.Program())
|
||||
antlr.ParseTreeWalkerDefault.Walk(listener, p.Program())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package compiler_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler"
|
||||
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
"github.com/MontFerret/ferret/v2/test/spec"
|
||||
. "github.com/MontFerret/ferret/v2/test/spec/compile"
|
||||
)
|
||||
@@ -17,5 +21,93 @@ func TestMatchErrors(t *testing.T) {
|
||||
Message: "duplicate binding 'v' in MATCH pattern",
|
||||
},
|
||||
),
|
||||
Failure(
|
||||
`
|
||||
FUNC fib(n) (
|
||||
RETURN MATCH n (
|
||||
0 => 0
|
||||
1 => 1
|
||||
_ => fib(n - 1) + fib(n - 2)
|
||||
)
|
||||
)
|
||||
|
||||
RETURN fib(10)
|
||||
`,
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Expected ',' between MATCH arms",
|
||||
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
|
||||
},
|
||||
"Missing comma between MATCH pattern arms",
|
||||
),
|
||||
Failure(
|
||||
`
|
||||
RETURN MATCH (
|
||||
WHEN TRUE => "yes"
|
||||
WHEN FALSE => "no",
|
||||
_ => "fallback",
|
||||
)
|
||||
`,
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Expected ',' between MATCH arms",
|
||||
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
|
||||
},
|
||||
"Missing comma between MATCH guard arms",
|
||||
),
|
||||
Failure(
|
||||
`RETURN MATCH 0 ( 1 => 1 _ => 0 )`,
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Expected ',' between MATCH arms",
|
||||
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
|
||||
},
|
||||
"Missing comma before MATCH default arm",
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatchMissingCommaDiagnosticSpan(t *testing.T) {
|
||||
c := compiler.New()
|
||||
query := `FUNC fib(n) (
|
||||
RETURN MATCH n (
|
||||
0 => 0
|
||||
1 => 1
|
||||
_ => fib(n - 1) + fib(n - 2)
|
||||
)
|
||||
)
|
||||
|
||||
RETURN fib(10)`
|
||||
|
||||
_, err := c.Compile(source.NewAnonymous(query))
|
||||
if err == nil {
|
||||
t.Fatal("expected compilation error")
|
||||
}
|
||||
|
||||
diag := firstCompilationError(err)
|
||||
if diag == nil {
|
||||
t.Fatalf("expected diagnostic, got %T", err)
|
||||
}
|
||||
|
||||
if diag.Message != "Expected ',' between MATCH arms" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
|
||||
if len(diag.Spans) == 0 {
|
||||
t.Fatal("expected diagnostic span")
|
||||
}
|
||||
|
||||
if diag.Spans[0].Label != "missing comma" {
|
||||
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
|
||||
}
|
||||
|
||||
line, col := diag.Source.LocationAt(diag.Spans[0].Span)
|
||||
if line != 3 || col != 15 {
|
||||
t.Fatalf("unexpected span location: got %d:%d, want 3:15", line, col)
|
||||
}
|
||||
|
||||
formatted := pkgdiagnostics.Format(err)
|
||||
if !strings.Contains(formatted, "3 | 0 => 0\n | ^ missing comma\n4 | 1 => 1") {
|
||||
t.Fatalf("diagnostic should point after previous MATCH arm value, got:\n%s", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package compiler_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler"
|
||||
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
"github.com/MontFerret/ferret/v2/test/spec"
|
||||
. "github.com/MontFerret/ferret/v2/test/spec/compile"
|
||||
)
|
||||
@@ -295,3 +299,126 @@ func TestSyntaxErrors(t *testing.T) {
|
||||
}, "Incomplete range 2"),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMixedFunctionBodySyntaxDiagnosticDoesNotCascade(t *testing.T) {
|
||||
query := `
|
||||
FUNC fib(n) => (
|
||||
RETURN MATCH n (
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
_ => fib(n - 1) + fib(n - 2)
|
||||
)
|
||||
)
|
||||
|
||||
RETURN fib(10)`
|
||||
|
||||
_, err := compiler.New().Compile(source.NewAnonymous(query))
|
||||
if err == nil {
|
||||
t.Fatal("expected compilation error")
|
||||
}
|
||||
|
||||
diag := firstCompilationError(err)
|
||||
if diag == nil {
|
||||
t.Fatalf("expected diagnostic, got %T", err)
|
||||
}
|
||||
|
||||
if diag.Kind != parserd.SyntaxError {
|
||||
t.Fatalf("unexpected diagnostic kind: %s", diag.Kind)
|
||||
}
|
||||
|
||||
if diag.Message != "Cannot combine arrow and block function body syntax" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
|
||||
if diag.Hint != "Use either 'FUNC f(x) => expr' or 'FUNC f(x) ( ... RETURN expr )'." {
|
||||
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
|
||||
}
|
||||
|
||||
if diag.Note != "Remove '=>' to use a block body, or remove RETURN and keep a single expression after '=>'." {
|
||||
t.Fatalf("unexpected diagnostic note: %q", diag.Note)
|
||||
}
|
||||
|
||||
if len(diag.Spans) == 0 {
|
||||
t.Fatal("expected diagnostic span")
|
||||
}
|
||||
|
||||
if diag.Spans[0].Label != "RETURN is only valid in a block function body" {
|
||||
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
|
||||
}
|
||||
|
||||
formatted := pkgdiagnostics.Format(err)
|
||||
for _, unexpected := range []string{
|
||||
"Unclosed parenthesized expression",
|
||||
"mismatched input ')' expecting <EOF>",
|
||||
"Variable 'n' is not defined",
|
||||
} {
|
||||
if strings.Contains(formatted, unexpected) {
|
||||
t.Fatalf("formatted diagnostic contains cascade %q:\n%s", unexpected, formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFunctionParamsCloseDiagnosticDoesNotCascade(t *testing.T) {
|
||||
query := `FUNC fib (
|
||||
RETURN MATCH n (
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
_ => fib(n - 1) + fib(n - 2)
|
||||
)
|
||||
)
|
||||
|
||||
RETURN fib(10)`
|
||||
|
||||
_, err := compiler.New().Compile(source.NewAnonymous(query))
|
||||
if err == nil {
|
||||
t.Fatal("expected compilation error")
|
||||
}
|
||||
|
||||
diag := firstCompilationError(err)
|
||||
if diag == nil {
|
||||
t.Fatalf("expected diagnostic, got %T", err)
|
||||
}
|
||||
|
||||
if diag.Kind != parserd.SyntaxError {
|
||||
t.Fatalf("unexpected diagnostic kind: %s", diag.Kind)
|
||||
}
|
||||
|
||||
if diag.Message != "Expected function parameters before function body" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
|
||||
if diag.Hint != "Add a parameter list before the block body, e.g. FUNC fib(n) ( ... RETURN expr ). Use FUNC fib() ( ... ) for no parameters." {
|
||||
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
|
||||
}
|
||||
|
||||
if len(diag.Spans) == 0 {
|
||||
t.Fatal("expected diagnostic span")
|
||||
}
|
||||
|
||||
if diag.Spans[0].Label != "missing parameter list before function body" {
|
||||
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
|
||||
}
|
||||
|
||||
line, col := diag.Source.LocationAt(diag.Spans[0].Span)
|
||||
if line != 1 || col != 10 {
|
||||
t.Fatalf("unexpected span location: got %d:%d, want 1:10", line, col)
|
||||
}
|
||||
|
||||
formatted := pkgdiagnostics.Format(err)
|
||||
if got := strings.Count(formatted, "SyntaxError:"); got != 1 {
|
||||
t.Fatalf("expected one syntax diagnostic, got %d:\n%s", got, formatted)
|
||||
}
|
||||
|
||||
if !strings.Contains(formatted, "1 | FUNC fib (\n | ^ missing parameter list before function body\n2 | RETURN MATCH n (") {
|
||||
t.Fatalf("diagnostic should point at the premature function body paren, got:\n%s", formatted)
|
||||
}
|
||||
|
||||
for _, unexpected := range []string{
|
||||
"mismatched input 'RETURN'",
|
||||
"mismatched input ')' expecting <EOF>",
|
||||
} {
|
||||
if strings.Contains(formatted, unexpected) {
|
||||
t.Fatalf("formatted diagnostic contains cascade %q:\n%s", unexpected, formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ FUNC id(x) => x
|
||||
RETURN id(1)
|
||||
`, 1, "UDF arrow body"),
|
||||
S(`
|
||||
FUNC grouped() => (1 + 2)
|
||||
RETURN grouped()
|
||||
`, 3, "UDF arrow grouped expression"),
|
||||
S(`
|
||||
FUNC id(x) (
|
||||
RETURN x
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user