From 99f4ae46cc3371e7f82561062de8d8113ea4f78f Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Fri, 20 Sep 2024 13:59:56 -0400 Subject: [PATCH] Improve error messages and tests --- runtime/parser/expression.go | 3 + runtime/parser/expression_test.go | 214 ++++++++++++------ runtime/parser/lexer/lexer_test.go | 146 +++++++++++- runtime/tests/checker/string_test.go | 14 ++ runtime/tests/interpreter/interpreter_test.go | 55 +++++ 5 files changed, 358 insertions(+), 74 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index e3b6bfface..01d9dba5d1 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1186,6 +1186,9 @@ func defineStringExpression() { if curToken.Is(lexer.TokenStringTemplate) { p.next() // advance to the expression + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError("expected an identifier got: %s", p.currentTokenSource()) + } value, err := parseExpression(p, lowestBindingPower) if err != nil { return nil, err diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 95ea2cb792..dabaff5057 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,107 +6055,175 @@ func TestParseStringWithUnicode(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) } -func TestParseStringTemplateSimple(t *testing.T) { +func TestParseStringTemplate(t *testing.T) { t.Parallel() - actual, errs := testParseExpression(` - "$test" - `) + t.Run("simple", func(t *testing.T) { - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, + t.Parallel() + + actual, errs := testParseExpression(` + "$test" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } } - } - require.NoError(t, err) + require.NoError(t, err) - expected := &ast.StringTemplateExpression{ - Values: []string{ - "", - "", - }, - Expressions: []ast.Expression{ - &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "test", - Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 5, Line: 2, Column: 4}, + }, }, }, - }, - Range: ast.Range{ - StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, - EndPos: ast.Position{Offset: 13, Line: 2, Column: 12}, - }, - } + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + } - utils.AssertEqualWithDiff(t, expected, actual) -} + utils.AssertEqualWithDiff(t, expected, actual) + }) -func TestParseStringTemplateMulti(t *testing.T) { + t.Run("multi", func(t *testing.T) { - t.Parallel() + t.Parallel() - actual, errs := testParseExpression(` - "this is a test $abc $def test" - `) + actual, errs := testParseExpression(` + "this is a test $abc$def test" + `) - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } } - } - require.NoError(t, err) + require.NoError(t, err) - expected := &ast.StringTemplateExpression{ - Values: []string{ - "this is a test ", - " ", - " test", - }, - Expressions: []ast.Expression{ - &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "abc", - Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + expected := &ast.StringTemplateExpression{ + Values: []string{ + "this is a test ", + "", + " test", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "abc", + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "def", + Pos: ast.Position{Offset: 24, Line: 2, Column: 24}, + }, }, }, - &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "def", - Pos: ast.Position{Offset: 29, Line: 2, Column: 28}, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 32}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("missing end", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "this is a test $FOO + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 25, Line: 2, Column: 25}, }, }, - }, - Range: ast.Range{ - StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, - EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, - }, - } + errs, + ) + }) - utils.AssertEqualWithDiff(t, expected, actual) -} + t.Run("invalid identifier", func(t *testing.T) { -func TestParseStringTemplateFail(t *testing.T) { + t.Parallel() - t.Parallel() + _, errs := testParseExpression(` + "$$" + `) - _, errs := testParseExpression(` - "this is a test $FOO - `) + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected an identifier got: $", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + errs, + ) + }) + + t.Run("invalid, num", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "$(2 + 2) is a" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } } - } - require.Error(t, err) + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected an identifier got: (", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + errs, + ) + }) } func TestParseNilCoalescing(t *testing.T) { diff --git a/runtime/parser/lexer/lexer_test.go b/runtime/parser/lexer/lexer_test.go index 4206edd22e..643f57e902 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1071,7 +1071,7 @@ func TestLexString(t *testing.T) { ) }) - t.Run("invalid, string template", func(t *testing.T) { + t.Run("invalid, number string template", func(t *testing.T) { testLex(t, `"$1"`, []token{ @@ -1128,6 +1128,150 @@ func TestLexString(t *testing.T) { ) }) + t.Run("invalid, string template", func(t *testing.T) { + testLex(t, + `"$a + 2`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: ` + 2`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, multi string template", func(t *testing.T) { + testLex(t, + `"$a$b"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 2}, + }, + }, + Source: ``, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 3}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 4}, + }, + }, + Source: `b`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 6}, + }, + }, + }, + }, + ) + }) + t.Run("invalid, empty, not terminated at line end", func(t *testing.T) { testLex(t, "\"\n", diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index de16b37bcc..ee2609758f 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -726,6 +726,20 @@ func TestCheckStringTemplate(t *testing.T) { require.NoError(t, err) }) + t.Run("valid, struct", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "$a" + `) + + require.NoError(t, err) + }) + t.Run("invalid, missing variable", func(t *testing.T) { t.Parallel() diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 86e89ac73b..3669f17960 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12343,4 +12343,59 @@ func TestInterpretStringTemplates(t *testing.T) { inter.Globals.Get("z").GetValue(inter), ) }) + + t.Run("struct", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "$a" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("S.test.SomeStruct()"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("func", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let add = fun(): Int { + return 2+2 + } + let x: String = "$add()" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("fun(): Int()"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("func", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let add = fun(): Int { + return 2+2 + } + let y = add() + let x: String = "$y" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("4"), + inter.Globals.Get("x").GetValue(inter), + ) + }) }