hclhil: Parsing and JustAttributes for HCL

This commit is contained in:
Martin Atkins 2017-05-21 16:13:04 -07:00
parent c5df265cd0
commit 764e4c465b
6 changed files with 419 additions and 2 deletions

29
zcl/hclhil/parser.go Normal file
View File

@ -0,0 +1,29 @@
package hclhil
import (
"fmt"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/hashicorp/hcl"
hclast "github.com/hashicorp/hcl/hcl/ast"
)
func parse(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
hclFile, err := hcl.ParseBytes(src)
if err != nil {
return nil, zcl.Diagnostics{
&zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Syntax error in configuration",
Detail: fmt.Sprintf("The file %q could not be parsed: %s", filename, err),
Subject: errorRange(err),
},
}
}
return &zcl.File{
Body: &body{
oli: hclFile.Node.(*hclast.ObjectList),
},
}, nil
}

51
zcl/hclhil/public.go Normal file
View File

@ -0,0 +1,51 @@
package hclhil
import (
"fmt"
"io/ioutil"
"os"
"github.com/apparentlymart/go-zcl/zcl"
)
// Parse attempts to parse the given buffer as HCL with HIL expressions and,
// if successful, returns a zcl.File for the zcl configuration represented by
// it.
//
// The returned file is valid only if the returned diagnostics returns false
// 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) (*zcl.File, zcl.Diagnostics) {
return parse(src, filename)
}
// ParseFile is a convenience wrapper around Parse that first attempts to load
// data from the given filename, passing the result to Parse if successful.
//
// If the file cannot be read, an error diagnostic with nil context is returned.
func ParseFile(filename string) (*zcl.File, zcl.Diagnostics) {
f, err := os.Open(filename)
if err != nil {
return nil, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Failed to open file",
Detail: fmt.Sprintf("The file %q could not be opened.", filename),
},
}
}
defer f.Close()
src, err := ioutil.ReadAll(f)
if err != nil {
return nil, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Failed to read file",
Detail: fmt.Sprintf("The file %q was opened, but an error occured while reading it.", filename),
},
}
}
return Parse(src, filename)
}

39
zcl/hclhil/shim.go Normal file
View File

@ -0,0 +1,39 @@
package hclhil
import (
"github.com/apparentlymart/go-zcl/zcl"
hclparser "github.com/hashicorp/hcl/hcl/parser"
hcltoken "github.com/hashicorp/hcl/hcl/token"
)
// errorRange attempts to extract a source range from the given error,
// returning a pointer to the range if possible or nil if not.
//
// errorRange understands HCL's "PosError" type, which wraps an error
// with a source position.
func errorRange(err error) *zcl.Range {
if perr, ok := err.(*hclparser.PosError); ok {
rng := rangeFromHCLPos(perr.Pos)
return &rng
}
return nil
}
func rangeFromHCLPos(pos hcltoken.Pos) zcl.Range {
// HCL only marks single positions rather than ranges, so we adapt this
// by creating a single-character range at the given position.
return zcl.Range{
Filename: pos.Filename,
Start: zcl.Pos{
Byte: pos.Offset,
Line: pos.Line,
Column: pos.Column,
},
End: zcl.Pos{
Byte: pos.Offset + 1,
Line: pos.Line,
Column: pos.Column + 1,
},
}
}

106
zcl/hclhil/structure.go Normal file
View File

@ -0,0 +1,106 @@
package hclhil
import (
"fmt"
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
hclast "github.com/hashicorp/hcl/hcl/ast"
)
// body is our implementation of zcl.Body in terms of an HCL ObjectList
type body struct {
oli *hclast.ObjectList
}
func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
return nil, nil
}
func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
return nil, nil, nil
}
func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
items := b.oli.Items
attrs := make(zcl.Attributes)
var diags zcl.Diagnostics
for _, item := range items {
if len(item.Keys) == 0 {
// Should never happen, since we don't use b.oli.Filter
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid item",
Detail: "Somehow we have an HCL item with no keys. This should never happen.",
Context: rangeFromHCLPos(item.Pos()).Ptr(),
})
continue
}
if len(item.Keys) > 1 {
name := item.Keys[0].Token.Value().(string)
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: fmt.Sprintf("Unexpected %s block", name),
Detail: "Blocks are not allowed here.",
Context: rangeFromHCLPos(item.Pos()).Ptr(),
})
continue
}
name := item.Keys[0].Token.Value().(string)
if item.Assign.Line == 0 {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagWarning,
Summary: "Block syntax used for attribute",
Detail: fmt.Sprintf("Attribute %q is defined using block syntax, which is deprecated. Use an equals sign after the attribute name instead.", name),
Context: rangeFromHCLPos(item.Pos()).Ptr(),
})
}
if attrs[name] != nil {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Duplicate attribute definition",
Detail: fmt.Sprintf(
"Attribute %q was previously defined at %s",
name, attrs[name].NameRange.String(),
),
Context: rangeFromHCLPos(item.Pos()).Ptr(),
})
continue
}
attrs[name] = &zcl.Attribute{
Name: name,
Expr: &expression{src: item.Val},
Range: rangeFromHCLPos(item.Pos()),
NameRange: rangeFromHCLPos(item.Keys[0].Pos()),
}
}
return attrs, diags
}
func (b *body) MissingItemRange() zcl.Range {
return rangeFromHCLPos(b.oli.Pos())
}
// body is our implementation of zcl.Body in terms of an HCL node, which may
// internally have strings to be interpreted as HIL templates.
type expression struct {
src hclast.Node
}
func (e *expression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
// TODO: Implement
return cty.NilVal, nil
}
func (e *expression) Range() zcl.Range {
return rangeFromHCLPos(e.src.Pos())
}
func (e *expression) StartRange() zcl.Range {
return rangeFromHCLPos(e.src.Pos())
}

View File

@ -0,0 +1,179 @@
package hclhil
import (
"fmt"
"reflect"
"testing"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/davecgh/go-spew/spew"
hclast "github.com/hashicorp/hcl/hcl/ast"
hcltoken "github.com/hashicorp/hcl/hcl/token"
)
func TestBodyJustAttributes(t *testing.T) {
tests := []struct {
Source string
Want zcl.Attributes
DiagCount int
}{
{
``,
zcl.Attributes{},
0,
},
{
`foo = "a"`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.LiteralType{
Token: hcltoken.Token{
Type: hcltoken.STRING,
Pos: hcltoken.Pos{
Offset: 6,
Line: 1,
Column: 7,
},
Text: `"a"`,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
0,
},
{
`foo = {}`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.ObjectType{
List: &hclast.ObjectList{},
Lbrace: hcltoken.Pos{
Offset: 6,
Line: 1,
Column: 7,
},
Rbrace: hcltoken.Pos{
Offset: 7,
Line: 1,
Column: 8,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
0,
},
{
`foo {}`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.ObjectType{
List: &hclast.ObjectList{},
Lbrace: hcltoken.Pos{
Offset: 4,
Line: 1,
Column: 5,
},
Rbrace: hcltoken.Pos{
Offset: 5,
Line: 1,
Column: 6,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
1, // warning about using block syntax
},
{
`foo "bar" {}`,
zcl.Attributes{},
1, // blocks are not allowed here
},
{
`
foo = 1
foo = 2
`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.LiteralType{
Token: hcltoken.Token{
Type: hcltoken.NUMBER,
Pos: hcltoken.Pos{
Offset: 14,
Line: 2,
Column: 14,
},
Text: `1`,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
},
},
},
1, // duplicate definition of foo
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
file, diags := Parse([]byte(test.Source), "test.hcl")
if len(diags) != 0 {
t.Fatalf("diagnostics from parse: %s", diags.Error())
}
got, diags := file.Body.JustAttributes()
if len(diags) != test.DiagCount {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want))
}
})
}
}

View File

@ -2,6 +2,7 @@ package parser
import (
"github.com/apparentlymart/go-zcl/zcl"
"github.com/apparentlymart/go-zcl/zcl/hclhil"
"github.com/apparentlymart/go-zcl/zcl/json"
)
@ -54,14 +55,26 @@ func (p *Parser) ParseJSONFile(filename string) (*zcl.File, zcl.Diagnostics) {
// This HCL/HIL parser is a compatibility interface to ease migration for
// apps that previously used HCL and HIL directly.
func (p *Parser) ParseHCLHIL(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
return nil, nil
if existing := p.files[filename]; existing != nil {
return existing, nil
}
file, diags := hclhil.Parse(src, filename)
p.files[filename] = file
return file, diags
}
// ParseHCLHILFile reads the given filename and parses it as HCL/HIL, similarly
// to ParseHCLHIL. An error diagnostic is returned if the given file cannot be
// read.
func (p *Parser) ParseHCLHILFile(filename string) (*zcl.File, zcl.Diagnostics) {
return nil, nil
if existing := p.files[filename]; existing != nil {
return existing, nil
}
file, diags := hclhil.ParseFile(filename)
p.files[filename] = file
return file, diags
}
// AddFile allows a caller to record in a parser a file that was parsed some