From e72341df8ac7e995c125b20d4f05b4198587e2e6 Mon Sep 17 00:00:00 2001 From: Kazuma Watanabe Date: Tue, 25 Aug 2020 02:53:10 +0900 Subject: [PATCH] json: Add json.ParseWithStartPos function This is like json.Parse but allows specifying a non-default start position, in case the caller is parsing a fragment from a larger JSON document. --- json/parser.go | 11 ++----- json/parser_test.go | 44 +++++++++++++++++++++++++++- json/public.go | 11 ++++++- json/public_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/json/parser.go b/json/parser.go index 7a54c51..4e40068 100644 --- a/json/parser.go +++ b/json/parser.go @@ -8,15 +8,8 @@ import ( "github.com/zclconf/go-cty/cty" ) -func parseFileContent(buf []byte, filename string) (node, hcl.Diagnostics) { - tokens := scan(buf, pos{ - Filename: filename, - Pos: hcl.Pos{ - Byte: 0, - Line: 1, - Column: 1, - }, - }) +func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) { + tokens := scan(buf, pos{Filename: filename, Pos: start}) p := newPeeker(tokens) node, diags := parseValue(p) if len(diags) == 0 && p.Peek().Type != tokenEOF { diff --git a/json/parser_test.go b/json/parser_test.go index 3752a15..dd0b373 100644 --- a/json/parser_test.go +++ b/json/parser_test.go @@ -598,7 +598,49 @@ func TestParse(t *testing.T) { for _, test := range tests { t.Run(test.Input, func(t *testing.T) { - got, diag := parseFileContent([]byte(test.Input), "") + got, diag := parseFileContent([]byte(test.Input), "", hcl.Pos{Byte: 0, Line: 1, Column: 1}) + + if len(diag) != test.DiagCount { + t.Errorf("got %d diagnostics; want %d", len(diag), test.DiagCount) + for _, d := range diag { + t.Logf(" - %s", d.Error()) + } + } + + if diff := deep.Equal(got, test.Want); diff != nil { + for _, problem := range diff { + t.Error(problem) + } + } + }) + } +} + +func TestParseWithPos(t *testing.T) { + tests := []struct { + Input string + StartPos hcl.Pos + Want node + DiagCount int + }{ + // Simple, single-token constructs + { + `true`, + hcl.Pos{Byte: 0, Line: 3, Column: 10}, + &booleanVal{ + Value: true, + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 10, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 14, Byte: 4}, + }, + }, + 0, + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, diag := parseFileContent([]byte(test.Input), "", test.StartPos) if len(diag) != test.DiagCount { t.Errorf("got %d diagnostics; want %d", len(diag), test.DiagCount) diff --git a/json/public.go b/json/public.go index 8dc4a36..d3bc9a0 100644 --- a/json/public.go +++ b/json/public.go @@ -18,7 +18,16 @@ import ( // from its HasErrors method. If HasErrors returns true, the file represents // the subset of data that was able to be parsed, which may be none. func Parse(src []byte, filename string) (*hcl.File, hcl.Diagnostics) { - rootNode, diags := parseFileContent(src, filename) + return ParseWithStartPos(src, filename, hcl.Pos{Byte: 0, Line: 1, Column: 1}) +} + +// ParseWithStartPos attempts to parse like json.Parse, but unlike json.Parse +// you can pass a start position of the given JSON as a hcl.Pos. +// +// In most cases json.Parse should be sufficient, but it can be useful for parsing +// a part of JSON with correct positions. +func ParseWithStartPos(src []byte, filename string, start hcl.Pos) (*hcl.File, hcl.Diagnostics) { + rootNode, diags := parseFileContent(src, filename, start) switch rootNode.(type) { case *objectVal, *arrayVal: diff --git a/json/public_test.go b/json/public_test.go index 9887014..70944d9 100644 --- a/json/public_test.go +++ b/json/public_test.go @@ -112,3 +112,73 @@ func TestParse_malformed(t *testing.T) { t.Errorf("got nil File; want actual file") } } + +func TestParseWithStartPos(t *testing.T) { + src := `{ + "foo": { + "bar": "baz" + } +}` + part := `{ + "bar": "baz" + }` + + file, diags := Parse([]byte(src), "") + partFile, partDiags := ParseWithStartPos([]byte(part), "", hcl.Pos{Byte: 0, Line: 2, Column: 10}) + if len(diags) != 0 { + t.Errorf("got %d diagnostics on parse src; want 0", len(diags)) + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + } + if len(partDiags) != 0 { + t.Errorf("got %d diagnostics on parse part src; want 0", len(partDiags)) + for _, diag := range partDiags { + t.Logf("- %s", diag.Error()) + } + } + + if file == nil { + t.Errorf("got nil File; want actual file") + } + if file.Body == nil { + t.Fatalf("got nil Body; want actual body") + } + if partFile == nil { + t.Errorf("got nil part File; want actual file") + } + if partFile.Body == nil { + t.Fatalf("got nil part Body; want actual body") + } + + content, diags := file.Body.Content(&hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{{Type: "foo"}}, + }) + if len(diags) != 0 { + t.Errorf("got %d diagnostics on decode; want 0", len(diags)) + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + } + attrs, diags := content.Blocks[0].Body.JustAttributes() + if len(diags) != 0 { + t.Errorf("got %d diagnostics on decode; want 0", len(diags)) + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + } + srcRange := attrs["bar"].Expr.Range() + + partAttrs, diags := partFile.Body.JustAttributes() + if len(diags) != 0 { + t.Errorf("got %d diagnostics on decode; want 0", len(diags)) + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + } + partRange := partAttrs["bar"].Expr.Range() + + if srcRange.String() != partRange.String() { + t.Errorf("The two ranges did not match: src=%s, part=%s", srcRange, partRange) + } +}