diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 96e838a..5c7e116 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -106,15 +106,6 @@ func (t Token) IsKind(kind ...TokenKind) bool { return slices.Contains(kind, t.Kind) } -type PositionalError struct { - Pos int - Err error -} - -func (e *PositionalError) Error() string { - return fmt.Sprintf("%v. Position %d.", e.Pos, e.Err) -} - type UnexpectedTokenError struct { Pos int Token rune @@ -123,3 +114,11 @@ type UnexpectedTokenError struct { func (e *UnexpectedTokenError) Error() string { return fmt.Sprintf("failed to tokenize: unexpected token: %s at position %d.", string(e.Token), e.Pos) } + +type UnexpectedEOFError struct { + Pos int +} + +func (e *UnexpectedEOFError) Error() string { + return fmt.Sprintf("failed to tokenize: unexpected EOF at position %d.", e.Pos) +} diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 535f366..4aebae1 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -160,25 +160,35 @@ func (p *Tokenizer) parseCurRune() (Token, error) { pos := p.i buf := make([]rune, 0) pos++ - var escaped bool + foundCloseRune := false for pos < p.srcLen { - if p.src[pos] == p.src[p.i] && !escaped { + if p.src[pos] == p.src[p.i] { + foundCloseRune = true break } - if escaped { - escaped = false - buf = append(buf, rune(p.src[pos])) - pos++ - continue - } if p.src[pos] == '\\' { pos++ - escaped = true + buf = append(buf, rune(p.src[pos])) + pos++ continue } buf = append(buf, rune(p.src[pos])) pos++ } + if !foundCloseRune { + // We didn't find a closing quote. + if pos < p.srcLen { + // This shouldn't be possible. + return Token{}, &UnexpectedTokenError{ + Pos: p.i, + Token: rune(p.src[pos]), + } + } + // This can happen if the selector ends before the closing quote. + return Token{}, &UnexpectedEOFError{ + Pos: pos, + } + } res := NewToken(String, string(buf), p.i, pos+1-p.i) return res, nil default: diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 19eb2a3..3d89c8f 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -1,83 +1,135 @@ -package lexer +package lexer_test -import "testing" +import ( + "errors" + "testing" -func TestTokenizer_Parse(t *testing.T) { - type testCase struct { - in string - out []TokenKind + "github.com/tomwright/dasel/v3/selector/lexer" +) + +type testCase struct { + in string + out []lexer.TokenKind +} + +func (tc testCase) run(t *testing.T) { + tok := lexer.NewTokenizer(tc.in) + tokens, err := tok.Tokenize() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tokens) != len(tc.out) { + t.Fatalf("unexpected number of tokens: %d", len(tokens)) + } + for i := range tokens { + if tokens[i].Kind != tc.out[i] { + t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind) + return + } } +} + +type errTestCase struct { + in string + match func(error) bool +} - runTest := func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - tok := NewTokenizer(tc.in) - tokens, err := tok.Tokenize() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(tokens) != len(tc.out) { - t.Fatalf("unexpected number of tokens: %d", len(tokens)) - } - for i := range tokens { - if tokens[i].Kind != tc.out[i] { - t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind) - return - } - } +func (tc errTestCase) run(t *testing.T) { + tok := lexer.NewTokenizer(tc.in) + tokens, err := tok.Tokenize() + if !tc.match(err) { + t.Errorf("unexpected error, got %v", err) + } + if tokens != nil { + t.Errorf("unexpected tokens: %v", tokens) + } +} + +func matchUnexpectedError(r rune, p int) func(error) bool { + return func(err error) bool { + var e *lexer.UnexpectedTokenError + if !errors.As(err, &e) { + return false } + + return e.Token == r && e.Pos == p } +} + +func matchUnexpectedEOFError(p int) func(error) bool { + return func(err error) bool { + var e *lexer.UnexpectedEOFError + if !errors.As(err, &e) { + return false + } - t.Run("variables", runTest(testCase{ + return e.Pos == p + } +} + +func TestTokenizer_Parse(t *testing.T) { + t.Run("variables", testCase{ in: "$foo $bar123 $baz $", - out: []TokenKind{ - Variable, - Variable, - Variable, - Dollar, + out: []lexer.TokenKind{ + lexer.Variable, + lexer.Variable, + lexer.Variable, + lexer.Dollar, }, - })) + }.run) - t.Run("if", runTest(testCase{ + t.Run("if", testCase{ in: `if elseif else`, - out: []TokenKind{ - If, - ElseIf, - Else, + out: []lexer.TokenKind{ + lexer.If, + lexer.ElseIf, + lexer.Else, }, - })) + }.run) - t.Run("regex", runTest(testCase{ + t.Run("regex", testCase{ in: `r/asd/ r/hello there/`, - out: []TokenKind{ - RegexPattern, - RegexPattern, + out: []lexer.TokenKind{ + lexer.RegexPattern, + lexer.RegexPattern, }, - })) + }.run) - t.Run("sort by", runTest(testCase{ + t.Run("sort by", testCase{ in: `sortBy(foo, asc)`, - out: []TokenKind{ - SortBy, - OpenParen, - Symbol, - Comma, - Asc, - CloseParen, + out: []lexer.TokenKind{ + lexer.SortBy, + lexer.OpenParen, + lexer.Symbol, + lexer.Comma, + lexer.Asc, + lexer.CloseParen, }, - })) + }.run) - t.Run("everything", runTest(testCase{ + t.Run("everything", testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", - out: []TokenKind{ - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, - Or, - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, - And, - Symbol, Equal, String, - Plus, Bool, Bool, - Dot, Spread, Dot, - Symbol, Spread, - Variable, Null, + out: []lexer.TokenKind{ + lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.Number, lexer.CloseBracket, lexer.NotEqual, lexer.Number, + lexer.Or, + lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.String, lexer.CloseBracket, lexer.Equal, lexer.Number, + lexer.And, + lexer.Symbol, lexer.Equal, lexer.String, + lexer.Plus, lexer.Bool, lexer.Bool, + lexer.Dot, lexer.Spread, lexer.Dot, + lexer.Symbol, lexer.Spread, + lexer.Variable, lexer.Null, }, - })) + }.run) + + t.Run("unhappy", func(t *testing.T) { + t.Run("unfinished double quote", errTestCase{ + in: `"hello`, + match: matchUnexpectedEOFError(6), + }.run) + t.Run("unfinished single quote", errTestCase{ + in: `'hello`, + match: matchUnexpectedEOFError(6), + }.run) + }) } diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index e04eec4..43debac 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -9,30 +9,28 @@ import ( "github.com/tomwright/dasel/v3/selector/parser" ) -func TestParser_Parse_HappyPath(t *testing.T) { - type testCase struct { - input string - expected ast.Expr - } +type happyTestCase struct { + input string + expected ast.Expr +} - run := func(t *testing.T, tc testCase) func(*testing.T) { - return func(t *testing.T) { - tokens, err := lexer.NewTokenizer(tc.input).Tokenize() - if err != nil { - t.Fatal(err) - } - got, err := parser.NewParser(tokens).Parse() - if err != nil { - t.Fatal(err) - } - if !cmp.Equal(tc.expected, got) { - t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got)) - } - } +func (tc happyTestCase) run(t *testing.T) { + tokens, err := lexer.NewTokenizer(tc.input).Tokenize() + if err != nil { + t.Fatal(err) + } + got, err := parser.NewParser(tokens).Parse() + if err != nil { + t.Fatal(err) } + if !cmp.Equal(tc.expected, got) { + t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got)) + } +} +func TestParser_Parse_HappyPath(t *testing.T) { t.Run("branching", func(t *testing.T) { - t.Run("two branches", run(t, testCase{ + t.Run("two branches", happyTestCase{ input: `branch("hello", len("world"))`, expected: ast.BranchExprs( ast.StringExpr{Value: "hello"}, @@ -43,80 +41,80 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, ), ), - })) - t.Run("three branches", run(t, testCase{ + }.run) + t.Run("three branches", happyTestCase{ input: `branch("foo", "bar", "baz")`, expected: ast.BranchExprs( ast.StringExpr{Value: "foo"}, ast.StringExpr{Value: "bar"}, ast.StringExpr{Value: "baz"}, ), - })) + }.run) }) t.Run("literal access", func(t *testing.T) { - t.Run("string", run(t, testCase{ + t.Run("string", happyTestCase{ input: `"hello world"`, expected: ast.StringExpr{Value: "hello world"}, - })) - t.Run("int", run(t, testCase{ + }.run) + t.Run("int", happyTestCase{ input: "42", expected: ast.NumberIntExpr{Value: 42}, - })) - t.Run("float", run(t, testCase{ + }.run) + t.Run("float", happyTestCase{ input: "42.1", expected: ast.NumberFloatExpr{Value: 42.1}, - })) - t.Run("whole number float", run(t, testCase{ + }.run) + t.Run("whole number float", happyTestCase{ input: "42f", expected: ast.NumberFloatExpr{Value: 42}, - })) - t.Run("bool true lowercase", run(t, testCase{ + }.run) + t.Run("bool true lowercase", happyTestCase{ input: "true", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool true uppercase", run(t, testCase{ + }.run) + t.Run("bool true uppercase", happyTestCase{ input: "TRUE", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool true mixed case", run(t, testCase{ + }.run) + t.Run("bool true mixed case", happyTestCase{ input: "TrUe", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool false lowercase", run(t, testCase{ + }.run) + t.Run("bool false lowercase", happyTestCase{ input: "false", expected: ast.BoolExpr{Value: false}, - })) - t.Run("bool false uppercase", run(t, testCase{ + }.run) + t.Run("bool false uppercase", happyTestCase{ input: "FALSE", expected: ast.BoolExpr{Value: false}, - })) - t.Run("bool false mixed case", run(t, testCase{ + }.run) + t.Run("bool false mixed case", happyTestCase{ input: "FaLsE", expected: ast.BoolExpr{Value: false}, - })) + }.run) }) t.Run("property access", func(t *testing.T) { - t.Run("single property access", run(t, testCase{ + t.Run("single property access", happyTestCase{ input: "foo", expected: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, - })) - t.Run("chained property access", run(t, testCase{ + }.run) + t.Run("chained property access", happyTestCase{ input: "foo.bar", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, ), - })) - t.Run("property access spread", run(t, testCase{ + }.run) + t.Run("property access spread", happyTestCase{ input: "foo...", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) - t.Run("property access spread into property access", run(t, testCase{ + }.run) + t.Run("property access spread into property access", happyTestCase{ input: "foo....bar", expected: ast.ChainExprs( ast.ChainExprs( @@ -125,20 +123,20 @@ func TestParser_Parse_HappyPath(t *testing.T) { ), ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, ), - })) + }.run) }) t.Run("array access", func(t *testing.T) { t.Run("root array", func(t *testing.T) { - t.Run("index", run(t, testCase{ + t.Run("index", happyTestCase{ input: "$this[1]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), - })) + }.run) t.Run("range", func(t *testing.T) { - t.Run("start and end funcs", run(t, testCase{ + t.Run("start and end funcs", happyTestCase{ input: "$this[calcStart(1):calcEnd()]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, @@ -154,56 +152,56 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, ), - })) - t.Run("start and end", run(t, testCase{ + }.run) + t.Run("start and end", happyTestCase{ input: "$this[5:10]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, ), - })) - t.Run("start", run(t, testCase{ + }.run) + t.Run("start", happyTestCase{ input: "$this[5:]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, ), - })) - t.Run("end", run(t, testCase{ + }.run) + t.Run("end", happyTestCase{ input: "$this[:10]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, ), - })) + }.run) }) t.Run("spread", func(t *testing.T) { - t.Run("standard", run(t, testCase{ + t.Run("standard", happyTestCase{ input: "$this...", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.SpreadExpr{}, ), - })) - t.Run("brackets", run(t, testCase{ + }.run) + t.Run("brackets", happyTestCase{ input: "$this[...]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.SpreadExpr{}, ), - })) + }.run) }) }) t.Run("property array", func(t *testing.T) { - t.Run("index", run(t, testCase{ + t.Run("index", happyTestCase{ input: "foo[1]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), - })) + }.run) t.Run("range", func(t *testing.T) { - t.Run("start and end funcs", run(t, testCase{ + t.Run("start and end funcs", happyTestCase{ input: "foo[calcStart(1):calcEnd()]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -219,50 +217,50 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, ), - })) - t.Run("start and end", run(t, testCase{ + }.run) + t.Run("start and end", happyTestCase{ input: "foo[5:10]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, ), - })) - t.Run("start", run(t, testCase{ + }.run) + t.Run("start", happyTestCase{ input: "foo[5:]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, ), - })) - t.Run("end", run(t, testCase{ + }.run) + t.Run("end", happyTestCase{ input: "foo[:10]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, ), - })) + }.run) }) t.Run("spread", func(t *testing.T) { - t.Run("standard", run(t, testCase{ + t.Run("standard", happyTestCase{ input: "foo...", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) - t.Run("brackets", run(t, testCase{ + }.run) + t.Run("brackets", happyTestCase{ input: "foo[...]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) + }.run) }) }) }) t.Run("map", func(t *testing.T) { - t.Run("single property", run(t, testCase{ + t.Run("single property", happyTestCase{ input: "foo.map(x)", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -270,8 +268,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Expr: ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, }, ), - })) - t.Run("nested property", run(t, testCase{ + }.run) + t.Run("nested property", happyTestCase{ input: "foo.map(x.y)", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -282,39 +280,39 @@ func TestParser_Parse_HappyPath(t *testing.T) { ), }, ), - })) + }.run) }) t.Run("object", func(t *testing.T) { - t.Run("get single property", run(t, testCase{ + t.Run("get single property", happyTestCase{ input: "{foo}", expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, }}, - })) - t.Run("get multiple properties", run(t, testCase{ + }.run) + t.Run("get multiple properties", happyTestCase{ input: "{foo, bar, baz}", expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}}, {Key: ast.StringExpr{Value: "baz"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}}, }}, - })) - t.Run("set single property", run(t, testCase{ + }.run) + t.Run("set single property", happyTestCase{ input: `{"foo":1}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, }}, - })) - t.Run("set multiple properties", run(t, testCase{ + }.run) + t.Run("set multiple properties", happyTestCase{ input: `{foo: 1, bar: 2, baz: 3}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, {Key: ast.StringExpr{Value: "baz"}, Value: ast.NumberIntExpr{Value: 3}}, }}, - })) - t.Run("combine get set", run(t, testCase{ + }.run) + t.Run("combine get set", happyTestCase{ input: `{ ..., nestedSpread..., @@ -331,41 +329,41 @@ func TestParser_Parse_HappyPath(t *testing.T) { {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething"}}, {Key: ast.StringExpr{Value: "Name"}, Value: ast.StringExpr{Value: "Tom"}}, }}, - })) + }.run) }) t.Run("variables", func(t *testing.T) { - t.Run("single variable", run(t, testCase{ + t.Run("single variable", happyTestCase{ input: `$foo`, expected: ast.VariableExpr{Name: "foo"}, - })) - t.Run("variable passed to func", run(t, testCase{ + }.run) + t.Run("variable passed to func", happyTestCase{ input: `len($foo)`, expected: ast.CallExpr{Function: "len", Args: ast.Expressions{ast.VariableExpr{Name: "foo"}}}, - })) + }.run) }) t.Run("combinations and grouping", func(t *testing.T) { - t.Run("string concat with grouping", run(t, testCase{ + t.Run("string concat with grouping", happyTestCase{ input: `(foo.a) + (foo.b)`, expected: ast.BinaryExpr{ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 8, Len: 1}, Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), }, - })) - t.Run("string concat with nested properties", run(t, testCase{ + }.run) + t.Run("string concat with nested properties", happyTestCase{ input: `foo.a + foo.b`, expected: ast.BinaryExpr{ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 6, Len: 1}, Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), }, - })) + }.run) }) t.Run("conditional", func(t *testing.T) { - t.Run("if", run(t, testCase{ + t.Run("if", happyTestCase{ input: `if (foo == 1) { "yes" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -376,8 +374,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Then: ast.StringExpr{Value: "yes"}, Else: ast.StringExpr{Value: "no"}, }, - })) - t.Run("if elseif else", run(t, testCase{ + }.run) + t.Run("if elseif else", happyTestCase{ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -396,8 +394,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Else: ast.StringExpr{Value: "no"}, }, }, - })) - t.Run("if elseif elseif else", run(t, testCase{ + }.run) + t.Run("if elseif elseif else", happyTestCase{ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } elseif (foo == 3) { "probably" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -424,11 +422,11 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, }, - })) + }.run) }) t.Run("coalesce", func(t *testing.T) { - t.Run("chained on left side", run(t, testCase{ + t.Run("chained on left side", happyTestCase{ input: `foo ?? bar ?? baz`, expected: ast.BinaryExpr{ Left: ast.BinaryExpr{ @@ -439,9 +437,9 @@ func TestParser_Parse_HappyPath(t *testing.T) { Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2}, Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}, }, - })) + }.run) - t.Run("chained nested on left side", run(t, testCase{ + t.Run("chained nested on left side", happyTestCase{ input: `nested.one ?? nested.two ?? nested.three ?? 10`, expected: ast.BinaryExpr{ Left: ast.BinaryExpr{ @@ -465,6 +463,6 @@ func TestParser_Parse_HappyPath(t *testing.T) { Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 41, Len: 2}, Right: ast.NumberIntExpr{Value: 10}, }, - })) + }.run) }) }