336 lines
8.3 KiB
Go
336 lines
8.3 KiB
Go
package parser_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.sharkk.net/Sharkk/Mako/parser"
|
|
)
|
|
|
|
func TestParsingErrors(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedError string
|
|
line int
|
|
column int
|
|
}{
|
|
{"= 5", "assignment operator '=' without left-hand side identifier", 1, 1},
|
|
{"x =", "expected expression after assignment operator", 1, 3},
|
|
{"(1 + 2", "expected next token to be )", 1, 7},
|
|
{"+ 5", "unexpected operator '+'", 1, 1},
|
|
{"1 +", "expected expression after operator '+'", 1, 3},
|
|
{"@", "unexpected token '@'", 1, 1},
|
|
{"invalid@", "unexpected identifier", 1, 1},
|
|
{"{1, 2", "expected next token to be }", 1, 6},
|
|
{"{a =", "expected expression after assignment operator", 1, 4},
|
|
{"{a = 1,", "expected next token to be }", 1, 8},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
|
|
switch tt.input {
|
|
case "(1 + 2", "+ 5", "1 +", "{1, 2", "{a =", "{a = 1,":
|
|
p.ParseExpression(parser.LOWEST)
|
|
default:
|
|
p.ParseProgram()
|
|
}
|
|
|
|
errors := p.Errors()
|
|
if len(errors) == 0 {
|
|
t.Fatalf("expected parsing errors, got none")
|
|
}
|
|
|
|
found := false
|
|
for _, err := range errors {
|
|
if strings.Contains(err.Message, tt.expectedError) {
|
|
found = true
|
|
if err.Line != tt.line {
|
|
t.Errorf("expected error at line %d, got line %d", tt.line, err.Line)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
errorMsgs := make([]string, len(errors))
|
|
for i, err := range errors {
|
|
errorMsgs[i] = err.Message
|
|
}
|
|
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestErrorRecovery(t *testing.T) {
|
|
input := `x = 42
|
|
= 5
|
|
y = "hello"`
|
|
|
|
l := parser.NewLexer(input)
|
|
p := parser.NewParser(l)
|
|
program := p.ParseProgram()
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
found := false
|
|
for _, err := range errors {
|
|
if strings.Contains(err.Message, "assignment operator '=' without left-hand side identifier") {
|
|
found = true
|
|
if err.Line != 2 {
|
|
t.Errorf("expected error at line 2, got line %d", err.Line)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Error("expected specific assignment error")
|
|
}
|
|
|
|
validStatements := 0
|
|
for _, stmt := range program.Statements {
|
|
if stmt != nil {
|
|
validStatements++
|
|
}
|
|
}
|
|
|
|
if validStatements < 2 {
|
|
t.Errorf("expected at least 2 valid statements, got %d", validStatements)
|
|
}
|
|
}
|
|
|
|
func TestErrorMessages(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedMessage string
|
|
}{
|
|
{"= 5", "Parse error at line 1, column 1: assignment operator '=' without left-hand side identifier (near '=')"},
|
|
{"x =", "Parse error at line 1, column 3: expected expression after assignment operator (near '')"},
|
|
{"(", "Parse error at line 1, column 1: unexpected end of input (near '')"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseProgram()
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
if len(errors) == 0 {
|
|
t.Fatal("expected at least one error")
|
|
}
|
|
|
|
errorStr := errors[0].Error()
|
|
if !strings.Contains(errorStr, "Parse error at line") {
|
|
t.Errorf("expected formatted error message, got: %s", errorStr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEchoErrors(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedError string
|
|
desc string
|
|
}{
|
|
{"echo", "expected expression after 'echo'", "echo without expression"},
|
|
{"echo +", "unexpected operator '+'", "echo with invalid expression"},
|
|
{"echo (", "unexpected end of input", "echo with incomplete expression"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseProgram()
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
found := false
|
|
for _, err := range errors {
|
|
if strings.Contains(err.Message, tt.expectedError) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
errorMsgs := make([]string, len(errors))
|
|
for i, err := range errors {
|
|
errorMsgs[i] = err.Message
|
|
}
|
|
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTokenTypeStringWithEcho(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedMessage string
|
|
}{
|
|
{"echo", "Parse error at line 1, column 1: expected expression after 'echo' (near '')"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseProgram()
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
if len(errors) == 0 {
|
|
t.Fatal("expected at least one error")
|
|
}
|
|
|
|
errorStr := errors[0].Error()
|
|
if !strings.Contains(errorStr, "Parse error at line") {
|
|
t.Errorf("expected formatted error message, got: %s", errorStr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrefixOperatorErrors(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedError string
|
|
desc string
|
|
}{
|
|
{"-", "expected expression after prefix operator '-'", "minus without operand"},
|
|
{"-(", "unexpected end of input", "minus with incomplete expression"},
|
|
{"-+", "unexpected operator '+'", "minus followed by plus"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseExpression(parser.LOWEST)
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
found := false
|
|
for _, err := range errors {
|
|
if strings.Contains(err.Message, tt.expectedError) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
errorMsgs := make([]string, len(errors))
|
|
for i, err := range errors {
|
|
errorMsgs[i] = err.Message
|
|
}
|
|
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComparisonOperatorErrors(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedError string
|
|
desc string
|
|
}{
|
|
{"x ==", "expected expression after operator '=='", "== without right operand"},
|
|
{"!= 5", "unexpected token '!='", "!= without left operand"},
|
|
{"< y", "unexpected token '<'", "< without left operand"},
|
|
{"> z", "unexpected token '>'", "> without left operand"},
|
|
{"<= 10", "unexpected token '<='", "<= without left operand"},
|
|
{">= 20", "unexpected token '>='", ">= without left operand"},
|
|
{"x !=", "expected expression after operator '!='", "!= without right operand"},
|
|
{"a <", "expected expression after operator '<'", "< without right operand"},
|
|
{"b >", "expected expression after operator '>'", "> without right operand"},
|
|
{"c <=", "expected expression after operator '<='", "<= without right operand"},
|
|
{"d >=", "expected expression after operator '>='", ">= without right operand"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseExpression(parser.LOWEST)
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
found := false
|
|
for _, err := range errors {
|
|
if strings.Contains(err.Message, tt.expectedError) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
errorMsgs := make([]string, len(errors))
|
|
for i, err := range errors {
|
|
errorMsgs[i] = err.Message
|
|
}
|
|
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTokenTypeStringWithComparisons(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedMessage string
|
|
}{
|
|
{"!= 5", "Parse error at line 1, column 1: unexpected token '!=' (near '!=')"},
|
|
{"< y", "Parse error at line 1, column 1: unexpected token '<' (near '<')"},
|
|
{">= 20", "Parse error at line 1, column 1: unexpected token '>=' (near '>=')"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
l := parser.NewLexer(tt.input)
|
|
p := parser.NewParser(l)
|
|
p.ParseExpression(parser.LOWEST)
|
|
|
|
if !p.HasErrors() {
|
|
t.Fatal("expected parsing errors")
|
|
}
|
|
|
|
errors := p.Errors()
|
|
if len(errors) == 0 {
|
|
t.Fatal("expected at least one error")
|
|
}
|
|
|
|
errorStr := errors[0].Error()
|
|
if !strings.Contains(errorStr, "Parse error at line") {
|
|
t.Errorf("expected formatted error message, got: %s", errorStr)
|
|
}
|
|
})
|
|
}
|
|
}
|