package parser_test import ( "fmt" "path/filepath" "reflect" "strings" "testing" "github.com/goccy/go-yaml" "github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/lexer" "github.com/goccy/go-yaml/parser" "github.com/goccy/go-yaml/token" ) func TestParser(t *testing.T) { sources := []string{ "null\n", "{}\n", "v: hi\n", "v: \"true\"\n", "v: \"false\"\n", "v: true\n", "v: false\n", "v: 10\n", "v: -10\n", "v: 42\n", "v: 4294967296\n", "v: \"10\"\n", "v: 0.1\n", "v: 0.99\n", "v: -0.1\n", "v: .inf\n", "v: -.inf\n", "v: .nan\n", "v: null\n", "v: \"\"\n", "v:\n- A\n- B\n", "a: '-'\n", "123\n", "hello: world\n", "a: null\n", "v:\n- A\n- 1\n- B:\n - 2\n - 3\n", "a:\n b: c\n", "a: {x: 1}\n", "t2: 2018-01-09T10:40:47Z\nt4: 2098-01-09T10:40:47Z\n", "a: [1, 2]\n", "a: {b: c, d: e}\n", "a: 3s\n", "a: \n", "a: \"1:1\"\n", "a: 1.2.3.4\n", "a: \"2015-02-24T18:19:39Z\"\n", "a: 'b: c'\n", "a: 'Hello #comment'\n", "a: abc <> ghi", "a: <-\n B\n C\n", "v: |-\n 0\n", "v: |-\n 0\nx: 0", `"a\n1\nb"`, `{"a":"b"}`, `!!map { ? !!str "explicit":!!str "entry", ? !!str "implicit" : !!str "entry", ? !!null "" : !!null "", }`, "\"a\": a\n\"b\": b", "'a': a\n'b': b", "a: \r\n b: 1\r\n", "a_ok: \r bc: 2\r", "a_mk: \n bd: 3\n", } for _, src := range sources { if _, err := parser.Parse(lexer.Tokenize(src), 0); err != nil { t.Fatalf("parse error: source [%s]: %+v", src, err) } } } func TestParseComplicatedDocument(t *testing.T) { tests := []struct { source string expect string }{ { ` american: - Boston Red Sox - Detroit Tigers - New York Yankees national: - New York Mets - Chicago Cubs - Atlanta Braves `, ` american: - Boston Red Sox - Detroit Tigers - New York Yankees national: - New York Mets - Chicago Cubs - Atlanta Braves `, }, { ` a: b: c d: e f: g h: i: j k: l: m n: o p: q r: s `, ` a: b: c d: e f: g h: i: j k: l: m n: o p: q r: s `, }, { ` - a: - b - c - d `, ` - a: - b - c - d `, }, { ` - a - b - c - d - e - f `, ` - a - b - c - d - e - f `, }, { ` a: 0 - 1 `, ` a: 0 - 1 `, }, {` - a: b: c d: e - f: g: h `, ` - a: b: c d: e - f: null g: h `, }, { ` a: b c d: e `, ` a: b c d: e `, }, { ` a b c `, ` a b c `, }, { ` a: - b - c `, ` a: - b - c `, }, { ` - a : b: c `, ` - a: null b: c `, }, { ` - a: b c d hoge: fuga `, ` - a: b c d hoge: fuga `, }, { ` - a # ' " # - : % - b # " # - : % ' - c # # - : % ' " - d # - : % ' " # - e # : % ' " # - - f # % ' : # - : `, ` - a - b - c - d - e - f `, }, { ` # comment a: # comment # comment b: c # comment # comment d: e # comment # comment `, ` a: b: c d: e `, }, { ` a: b#notcomment `, ` a: b#notcomment `, }, { ` anchored: &anchor foo aliased: *anchor `, ` anchored: &anchor foo aliased: *anchor `, }, { ` --- - &CENTER { x: 1, y: 2 } - &LEFT { x: 0, y: 2 } - &BIG { r: 10 } - &SMALL { r: 1 } # All the following maps are equal: - # Explicit keys x: 1 y: 2 r: 10 label: center/big - # Merge one map << : *CENTER r: 10 label: center/big - # Merge multiple maps << : [ *CENTER, *BIG ] label: center/big - # Override << : [ *BIG, *LEFT, *SMALL ] x: 1 label: center/big `, ` --- - &CENTER {x: 1, y: 2} - &LEFT {x: 0, y: 2} - &BIG {r: 10} - &SMALL {r: 1} - x: 1 y: 2 r: 10 label: center/big - <<: *CENTER r: 10 label: center/big - <<: [*CENTER, *BIG] label: center/big - <<: [*BIG, *LEFT, *SMALL] x: 1 label: center/big `, }, { ` a: - - b - - c - d `, ` a: - - b - - c - d `, }, { ` a: b: c: d e: f: g h: i j: k `, ` a: b: c: d e: f: g h: i j: k `, }, { ` --- a: 1 b: 2 ... --- c: 3 d: 4 ... `, ` --- a: 1 b: 2 ... --- c: 3 d: 4 ... `, }, { ` a: b: | { [ 1, 2 ] } c: d `, ` a: b: | { [ 1, 2 ] } c: d `, }, { ` | hoge fuga piyo`, ` | hoge fuga piyo `, }, { ` a: | bbbbbbb ccccccc d: eeeeeeeeeeeeeeeee `, ` a: | bbbbbbb ccccccc d: eeeeeeeeeeeeeeeee `, }, { ` a: b c `, ` a: b c `, }, { ` a: b: c `, ` a: b: c `, }, { ` a: b c: d `, ` a: b c: d `, }, { ` - ab - cd - ef - gh `, ` - ab - cd - ef - gh `, }, { ` - 0 - 1 - 2 - 3 `, ` - 0 - 1 - 2 - 3 `, }, { ` a - b - c: value `, ` a - b - c: value `, }, { ` a: - b: c d: e - f: g h: i `, ` a: - b: c d: e - f: g h: i `, }, { ` a: |- value b: c `, ` a: |- value b: c `, }, { ` a: |+ value b: c `, ` a: |+ value b: c `, }, { ` - key1: val key2: ( foo + bar ) `, ` - key1: val key2: ( foo + bar ) `, }, { ` "a": b 'c': d "e": "f" g: "h" i: 'j' `, ` "a": b 'c': d "e": "f" g: "h" i: 'j' `, }, } for _, test := range tests { t.Run(test.source, func(t *testing.T) { tokens := lexer.Tokenize(test.source) f, err := parser.Parse(tokens, 0) if err != nil { t.Fatalf("%+v", err) } var v Visitor for _, doc := range f.Docs { ast.Walk(&v, doc.Body) } expect := fmt.Sprintf("\n%+v", f) if test.expect != expect { tokens.Dump() t.Fatalf("unexpected output: [%s] != [%s]", test.expect, expect) } }) } } func TestNewLineChar(t *testing.T) { for _, f := range []string{ "lf.yml", "cr.yml", "crlf.yml", } { ast, err := parser.ParseFile(filepath.Join("testdata", f), 0) if err != nil { t.Fatalf("%+v", err) } actual := fmt.Sprintf("%v", ast) expect := `a: "a" b: 1 ` if expect != actual { t.Fatal("unexpected result") } } } func TestSyntaxError(t *testing.T) { tests := []struct { source string expect string }{ { ` a: - b c: d e: f g: h`, ` [3:3] unexpected key name 2 | a: > 3 | - b 4 | c: d ^ 5 | e: f 6 | g: h`, }, { ` a - b: c`, ` [2:1] unexpected key name > 2 | a 3 | - b: c ^ `, }, { `%YAML 1.1 {}`, ` [1:2] unexpected directive value. document not started > 1 | %YAML 1.1 {} ^ `, }, { `{invalid`, ` [1:2] unexpected map > 1 | {invalid ^ `, }, { `{ "key": "value" `, ` [1:1] unterminated flow mapping > 1 | { "key": "value" ^ `, }, { ` a: - b: c - `, ` [4:1] empty sequence entry 2 | a: 3 | - b: c > 4 | - ^ `, }, } for _, test := range tests { t.Run(test.source, func(t *testing.T) { _, err := parser.ParseBytes([]byte(test.source), 0) if err == nil { t.Fatal("cannot catch syntax error") } actual := "\n" + err.Error() if test.expect != actual { t.Fatalf("expected: [%s] but got [%s]", test.expect, actual) } }) } } func TestComment(t *testing.T) { tests := []struct { name string yaml string }{ { name: "map with comment", yaml: ` # commentA a: #commentB # commentC b: c # commentD # commentE d: e # commentF # commentG f: g # commentH # commentI f: g # commentJ # commentK `, }, { name: "sequence with comment", yaml: ` # commentA - a # commentB # commentC - b: # commentD # commentE - d # commentF - e # commentG # commentH `, }, { name: "anchor and alias", yaml: ` a: &x b # commentA c: *x # commentB `, }, { name: "multiline", yaml: ` # foo comment # foo comment2 foo: # map key comment # bar above comment # bar above comment2 bar: 10 # comment for bar # baz above comment # baz above comment2 baz: bbbb # comment for baz piyo: # sequence key comment # sequence1 above comment 1 # sequence1 above comment 2 - sequence1 # sequence1 # sequence2 above comment 1 # sequence2 above comment 2 - sequence2 # sequence2 # sequence3 above comment 1 # sequence3 above comment 2 - false # sequence3 # foo2 comment # foo2 comment2 foo2: &anchor text # anchor comment # foo3 comment # foo3 comment2 foo3: *anchor # alias comment `, }, { name: "literal", yaml: ` foo: | # comment x: 42 `, }, { name: "folded", yaml: ` foo: > # comment x: 42 `, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { f, err := parser.ParseBytes([]byte(test.yaml), parser.ParseComments) if err != nil { t.Fatalf("%+v", err) } got := "\n" + f.String() if test.yaml != got { t.Fatalf("expected:%s\ngot:%s", test.yaml, got) } }) } } func TestCommentWithNull(t *testing.T) { t.Run("same line", func(t *testing.T) { content := ` foo: bar: # comment baz: 1 ` expected := ` foo: bar: null # comment baz: 1` f, err := parser.ParseBytes([]byte(content), parser.ParseComments) if err != nil { t.Fatal(err) } if len(f.Docs) != 1 { t.Fatal("failed to parse content with same line comment") } if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") { t.Fatal("failed to parse comment") } }) t.Run("next line", func(t *testing.T) { content := ` foo: bar: # comment baz: 1 ` expected := ` foo: bar: null # comment baz: 1` f, err := parser.ParseBytes([]byte(content), parser.ParseComments) if err != nil { t.Fatal(err) } if len(f.Docs) != 1 { t.Fatal("failed to parse content with next line comment") } if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") { t.Fatal("failed to parse comment") } }) t.Run("next line and different indent", func(t *testing.T) { content := ` foo: bar: # comment baz: 1` f, err := parser.ParseBytes([]byte(content), parser.ParseComments) if err != nil { t.Fatal(err) } if len(f.Docs) != 1 { t.Fatal("failed to parse content with next line comment") } expected := ` foo: bar: null # comment baz: 1` if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") { t.Fatal("failed to parse comment") } }) } func TestSequenceComment(t *testing.T) { content := ` foo: - # comment bar: 1 baz: - xxx ` f, err := parser.ParseBytes([]byte(content), parser.ParseComments) if err != nil { t.Fatal(err) } if len(f.Docs) != 1 { t.Fatal("failed to parse content with next line with sequence") } expected := ` foo: # comment - bar: 1 baz: - xxx` if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") { t.Fatal("failed to parse comment") } t.Run("foo[0].bar", func(t *testing.T) { path, err := yaml.PathString("$.foo[0].bar") if err != nil { t.Fatal(err) } v, err := path.FilterFile(f) if err != nil { t.Fatal(err) } if v.String() != "1" { t.Fatal("failed to get foo[0].bar value") } }) t.Run("baz[0]", func(t *testing.T) { path, err := yaml.PathString("$.baz[0]") if err != nil { t.Fatal(err) } v, err := path.FilterFile(f) if err != nil { t.Fatal(err) } if v.String() != "xxx" { t.Fatal("failed to get baz[0] value") } }) } func TestNodePath(t *testing.T) { yml := ` a: # commentA b: # commentB c: foo # commentC d: bar # commentD e: baz # commentE f: # commentF g: hoge # commentG h: # commentH - list1 # comment list1 - list2 # comment list2 - list3 # comment list3 i: fuga # commentI j: piyo # commentJ k.l.m.n: moge # commentKLMN o#p: hogera # commentOP q#.r: hogehoge # commentQR ` f, err := parser.ParseBytes([]byte(yml), parser.ParseComments) if err != nil { t.Fatalf("%+v", err) } var capturer pathCapturer for _, doc := range f.Docs { ast.Walk(&capturer, doc.Body) } commentPaths := []string{} for i := 0; i < capturer.capturedNum; i++ { if capturer.orderedTypes[i] == ast.CommentType { commentPaths = append(commentPaths, capturer.orderedPaths[i]) } } expectedPaths := []string{ "$.a", "$.a.b", "$.a.b.c", "$.a.b.d", "$.a.b.e", "$.a.f", "$.a.f.g", "$.a.h", "$.a.h[0]", "$.a.h[1]", "$.a.h[2]", "$.a.i", "$.j", "$.'k.l.m.n'", "$.o#p", "$.'q#.r'", } if !reflect.DeepEqual(expectedPaths, commentPaths) { t.Fatalf("failed to get YAMLPath to the comment node:\nexpected[%s]\ngot [%s]", expectedPaths, commentPaths) } } type pathCapturer struct { capturedNum int orderedPaths []string orderedTypes []ast.NodeType orderedTokens []*token.Token } func (c *pathCapturer) Visit(node ast.Node) ast.Visitor { c.capturedNum++ c.orderedPaths = append(c.orderedPaths, node.GetPath()) c.orderedTypes = append(c.orderedTypes, node.Type()) c.orderedTokens = append(c.orderedTokens, node.GetToken()) return c } type Visitor struct { } func (v *Visitor) Visit(node ast.Node) ast.Visitor { tk := node.GetToken() tk.Prev = nil tk.Next = nil return v }