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) } }) } }