From a12943633e2c9b09d6d175dc220411b9417bab6e Mon Sep 17 00:00:00 2001 From: Tim Voronov Date: Tue, 12 Aug 2025 13:07:00 -0400 Subject: [PATCH] Enhance diagnostics for unclosed string literals: improve error messages and hints for missing opening and closing quotes, and add comprehensive test cases for better clarity and coverage. --- pkg/compiler/internal/diagnostics/helpers.go | 27 +++ .../diagnostics/match_common_errors.go | 67 ++++++-- .../compiler/compiler_errors_syntax_test.go | 154 +++++++++++++++++- 3 files changed, 226 insertions(+), 22 deletions(-) diff --git a/pkg/compiler/internal/diagnostics/helpers.go b/pkg/compiler/internal/diagnostics/helpers.go index 48a1aa3a..d8533468 100644 --- a/pkg/compiler/internal/diagnostics/helpers.go +++ b/pkg/compiler/internal/diagnostics/helpers.go @@ -106,6 +106,22 @@ func isQuote(input string) bool { return false } +func isValidString(input string) bool { + if input == "" { + return false + } + + if isQuote(input) { + return true + } + + if isQuote(input[0:1]) && isQuote(input[len(input)-1:]) { + return true + } + + return false +} + func is(node *TokenNode, expected string) bool { if node == nil { return false @@ -182,3 +198,14 @@ func extractExtraneousInput(msg string) string { return input } + +func extractMismatchedInput(msg string) string { + re := regexp.MustCompile(`mismatched input\s+(?P.+?)\s+expecting`) + match := re.FindStringSubmatch(msg) + + input := match[re.SubexpIndex("input")] + input = strings.TrimPrefix(input, "'") + input = strings.TrimSuffix(input, "'") + + return input +} diff --git a/pkg/compiler/internal/diagnostics/match_common_errors.go b/pkg/compiler/internal/diagnostics/match_common_errors.go index 67e273b8..41f519d8 100644 --- a/pkg/compiler/internal/diagnostics/match_common_errors.go +++ b/pkg/compiler/internal/diagnostics/match_common_errors.go @@ -42,23 +42,41 @@ func matchCommonErrors(src *file.Source, err *CompilationError, offending *Token input := extractNoAlternativeInputs(err.Message) token := input[len(input)-1] - if isQuote(token) { - span := spanFromTokenSafe(offending.Token(), src) - inputRaw := extractNoAlternativeInput(err.Message) - spaces := strings.Count(inputRaw, " ") + 1 - span.Start += spaces - span.End += spaces + isMissingClosingQuote := isQuote(token) + isMissingOpeningQuote := isKeyword(offending.Prev()) && isQuote(token[len(token)-1:]) && !isValidString(token) + + if isMissingClosingQuote || isMissingOpeningQuote { + var span file.Span + var typeOfQuote string + var quote string + + if isMissingClosingQuote { + quote = token + typeOfQuote = "closing" + span = spanFromTokenSafe(offending.Token(), src) + inputRaw := extractNoAlternativeInput(err.Message) + spaces := strings.Count(inputRaw, " ") + 1 + span.Start += spaces + span.End += spaces + } else { + quote = token[len(token)-1:] + typeOfQuote = "opening" + span = spanFromTokenSafe(offending.Prev().Token(), src) + span.Start += 2 + span.End += 2 + } + err.Message = "Unclosed string literal" - if token == "'" { - err.Hint = fmt.Sprintf("Add a matching \"%s\" to close the string.", token) + if quote == "'" { + err.Hint = fmt.Sprintf("Add a matching \"%s\" to close the string.", quote) err.Spans = []ErrorSpan{ - NewMainErrorSpan(span, fmt.Sprintf("missing closing \"%s\"", token)), + NewMainErrorSpan(span, fmt.Sprintf("missing %s \"%s\"", typeOfQuote, quote)), } } else { - err.Hint = fmt.Sprintf("Add a matching '%s' to close the string.", token) + err.Hint = fmt.Sprintf("Add a matching '%s' to close the string.", quote) err.Spans = []ErrorSpan{ - NewMainErrorSpan(span, fmt.Sprintf("missing closing '%s'", token)), + NewMainErrorSpan(span, fmt.Sprintf("missing %s '%s'", typeOfQuote, quote)), } } @@ -222,25 +240,42 @@ func matchCommonErrors(src *file.Source, err *CompilationError, offending *Token return true } + } - token := extractExtraneousInput(err.Message) + if isExtraneous(err.Message) || isMismatched(err.Message) { + var token string + + if isExtraneous(err.Message) { + token = extractExtraneousInput(err.Message) + } else { + token = extractMismatchedInput(err.Message) + } if isQuote(token) { - span := spanFromTokenSafe(offending.Token(), src) + var span file.Span + var typeOfQuote string + + if isKeyword(offending) { + span = spanFromTokenSafe(offending.Token(), src) + typeOfQuote = "closing" + } else { + span = spanFromTokenSafe(offending.Prev().Token(), src) + typeOfQuote = "opening" + } + span.Start += 2 span.End += 2 - err.Message = "Unclosed string literal" if token == "'" { err.Hint = fmt.Sprintf("Add a matching \"%s\" to close the string.", token) err.Spans = []ErrorSpan{ - NewMainErrorSpan(span, fmt.Sprintf("missing closing \"%s\"", token)), + NewMainErrorSpan(span, fmt.Sprintf("missing %s \"%s\"", typeOfQuote, token)), } } else { err.Hint = fmt.Sprintf("Add a matching '%s' to close the string.", token) err.Spans = []ErrorSpan{ - NewMainErrorSpan(span, fmt.Sprintf("missing closing '%s'", token)), + NewMainErrorSpan(span, fmt.Sprintf("missing %s '%s'", typeOfQuote, token)), } } diff --git a/test/integration/compiler/compiler_errors_syntax_test.go b/test/integration/compiler/compiler_errors_syntax_test.go index bf3f5ce5..cb97d356 100644 --- a/test/integration/compiler/compiler_errors_syntax_test.go +++ b/test/integration/compiler/compiler_errors_syntax_test.go @@ -45,7 +45,37 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching '\"' to close the string.", - }, "Incomplete string"), + }, "Incomplete string (closing quote missing)"), + + ErrorCase( + ` + LET i = "foo bar + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string.", + }, "Incomplete multi-string (closing quote missing)"), + + ErrorCase( + ` + LET i = foo" + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string.", + }, "Incomplete string (opening quote missing)"), + + ErrorCase( + ` + LET i = foo bar" + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string.", + }, "Incomplete multi-string (opening quote missing)"), ErrorCase( ` @@ -55,7 +85,37 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching \"'\" to close the string.", - }, "Incomplete string 2"), + }, "Incomplete string (closing quote missing) 2"), + + ErrorCase( + ` + LET i = 'foo bar + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete multi-string (closing quote missing) 2"), + + ErrorCase( + ` + LET i = foo' + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete string (opening quote missing) 2"), + + ErrorCase( + ` + LET i = foo bar' + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete multi-string (opening quote missing) 2"), ErrorCase( "LET i = `foo "+ @@ -63,7 +123,31 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching '`' to close the string.", - }, "Incomplete string 3"), + }, "Incomplete string (closing quote missing) 3"), + + ErrorCase( + "LET i = `foo bar"+ + "RETURN i", E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '`' to close the string.", + }, "Incomplete multi-string (closing quote missing) 3"), + + ErrorCase( + "LET i = foo` "+ + "RETURN i", E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '`' to close the string.", + }, "Incomplete string (opening quote missing) 3"), + + ErrorCase( + "LET i = foo bar` "+ + "RETURN i", E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '`' to close the string.", + }, "Incomplete multi-string (opening quote missing) 3"), ErrorCase( ` @@ -73,7 +157,37 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching '\"' to close the string.", - }, "Incomplete string 4"), + }, "Incomplete string (closing quote missing) 4"), + + ErrorCase( + ` + LET i = { "foo bar: } + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string.", + }, "Incomplete multi-string (closing quote missing) 4"), + + ErrorCase( + ` + LET i = { foo": } + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string.", + }, "Incomplete string (opening quote missing) 4"), + + SkipErrorCase( + ` + LET i = { foo bar": } + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching '\"' to close the string", + }, "Incomplete multi-string (opening quote missing) 4"), ErrorCase( ` @@ -83,7 +197,27 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching \"'\" to close the string.", - }, "Incomplete string 5"), + }, "Incomplete string (closing quote missing) 5"), + + ErrorCase( + ` + LET i = { foo': } + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete string (opening quote missing) 5"), + + SkipErrorCase( + ` + LET i = { foo bar': } + RETURN i + `, E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete multi-string (opening quote missing) 5"), ErrorCase( "LET i = { 'foo: }"+ @@ -91,7 +225,15 @@ func TestSyntaxErrors(t *testing.T) { Kind: compiler.SyntaxError, Message: "Unclosed string literal", Hint: "Add a matching \"'\" to close the string.", - }, "Incomplete string 6"), + }, "Incomplete string (closing quote missing) 6"), + + ErrorCase( + "LET i = { 'foo bar: }"+ + "RETURN i", E{ + Kind: compiler.SyntaxError, + Message: "Unclosed string literal", + Hint: "Add a matching \"'\" to close the string.", + }, "Incomplete multi-string (closing quote missing) 6"), ErrorCase( `