hclhil: Body.Content and Body.PartialContent implementations
This commit is contained in:
parent
764e4c465b
commit
0b8f6498ff
@ -2,6 +2,7 @@ package hclhil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-cty/cty"
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
@ -10,15 +11,206 @@ import (
|
||||
|
||||
// body is our implementation of zcl.Body in terms of an HCL ObjectList
|
||||
type body struct {
|
||||
oli *hclast.ObjectList
|
||||
oli *hclast.ObjectList
|
||||
hiddenNames map[string]struct{}
|
||||
}
|
||||
|
||||
func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
|
||||
return nil, nil
|
||||
content, _, diags := b.content(schema, false)
|
||||
return content, diags
|
||||
}
|
||||
|
||||
func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
|
||||
return nil, nil, nil
|
||||
return b.content(schema, true)
|
||||
}
|
||||
|
||||
func (b *body) content(schema *zcl.BodySchema, partial bool) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
|
||||
attrSchemas := make(map[string]zcl.AttributeSchema)
|
||||
blockSchemas := make(map[string]zcl.BlockHeaderSchema)
|
||||
for _, attrS := range schema.Attributes {
|
||||
attrSchemas[attrS.Name] = attrS
|
||||
}
|
||||
for _, blockS := range schema.Blocks {
|
||||
blockSchemas[blockS.Type] = blockS
|
||||
}
|
||||
|
||||
attrs := make(zcl.Attributes)
|
||||
var blocks zcl.Blocks
|
||||
var diags zcl.Diagnostics
|
||||
|
||||
namesUsed := make(map[string]struct{})
|
||||
|
||||
for _, item := range b.oli.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
|
||||
}
|
||||
|
||||
name := item.Keys[0].Token.Value().(string)
|
||||
if _, hidden := b.hiddenNames[name]; hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, isAttr := attrSchemas[name]; isAttr {
|
||||
if len(item.Keys) > 1 {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Unsupported block type",
|
||||
Detail: fmt.Sprintf("Blocks of type %q are not expected here. Did you mean to define an attribute named %q?", name, name),
|
||||
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
diags = append(diags, insertAttr(attrs, item)...)
|
||||
namesUsed[name] = struct{}{}
|
||||
} else if blockS, isBlock := blockSchemas[name]; isBlock {
|
||||
obj, isBlock := item.Val.(*hclast.ObjectType)
|
||||
if !isBlock {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: fmt.Sprintf("An attribute named %q is not expected here. Did you mean to define a block of type %q?", name, name),
|
||||
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Assign.Line != 0 {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagWarning,
|
||||
Summary: "Attribute syntax used for block",
|
||||
Detail: fmt.Sprintf("Block %q is defined using attribute syntax, which is deprecated. The equals sign is not used to define a block.", name),
|
||||
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
labelKeys := item.Keys[1:]
|
||||
|
||||
if len(labelKeys) > len(blockS.LabelNames) {
|
||||
if len(blockS.LabelNames) == 0 {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: fmt.Sprintf("Extraneous label for %s", name),
|
||||
Detail: fmt.Sprintf(
|
||||
"No labels are expected for %s blocks", name,
|
||||
),
|
||||
Context: rangeFromHCLPos(labelKeys[len(blockS.LabelNames)].Pos()).Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: fmt.Sprintf("Extraneous label for %s", name),
|
||||
Detail: fmt.Sprintf(
|
||||
"Only %d labels (%s) are expected for %s blocks",
|
||||
len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), name,
|
||||
),
|
||||
Context: rangeFromHCLPos(labelKeys[len(blockS.LabelNames)].Pos()).Ptr(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(labelKeys) < len(blockS.LabelNames) {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: fmt.Sprintf("Missing %s for %s", blockS.LabelNames[len(labelKeys)], name),
|
||||
Detail: fmt.Sprintf(
|
||||
"All %s blocks must have %d labels (%s).",
|
||||
name, len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "),
|
||||
),
|
||||
Context: rangeFromHCLPos(obj.Pos()).Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
var labels []string
|
||||
var labelRanges []zcl.Range
|
||||
if len(labelKeys) > 0 {
|
||||
labels = make([]string, len(labelKeys))
|
||||
labelRanges = make([]zcl.Range, len(labelKeys))
|
||||
for i, objKey := range labelKeys {
|
||||
labels[i] = objKey.Token.Value().(string)
|
||||
labelRanges[i] = rangeFromHCLPos(objKey.Pos())
|
||||
}
|
||||
}
|
||||
|
||||
blocks = append(blocks, &zcl.Block{
|
||||
Type: name,
|
||||
Labels: labels,
|
||||
Body: &body{
|
||||
oli: obj.List,
|
||||
},
|
||||
|
||||
DefRange: rangeFromHCLPos(obj.Pos()),
|
||||
TypeRange: rangeFromHCLPos(item.Keys[0].Pos()),
|
||||
LabelRanges: labelRanges,
|
||||
})
|
||||
namesUsed[name] = struct{}{}
|
||||
|
||||
} else {
|
||||
if !partial {
|
||||
if item.Assign.Line == 0 {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Unsupported block type",
|
||||
Detail: fmt.Sprintf("Blocks of type %q are not expected here.", name),
|
||||
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: fmt.Sprintf("An attribute named %q is not expected here.", name),
|
||||
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attrS := range schema.Attributes {
|
||||
if !attrS.Required {
|
||||
continue
|
||||
}
|
||||
|
||||
if attrs[attrS.Name] == nil {
|
||||
// HCL has a bug where it panics if you ask for the position of an
|
||||
// empty object list. This means we can't specify a subject for
|
||||
// this diagnostic in that case.
|
||||
var subject *zcl.Range
|
||||
if len(b.oli.Items) > 0 {
|
||||
subject = rangeFromHCLPos(b.oli.Pos()).Ptr()
|
||||
}
|
||||
diags = diags.Append(&zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Missing required attribute",
|
||||
Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name),
|
||||
Subject: subject,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var leftovers zcl.Body
|
||||
if partial {
|
||||
for name := range b.hiddenNames {
|
||||
namesUsed[name] = struct{}{}
|
||||
}
|
||||
leftovers = &body{
|
||||
oli: b.oli,
|
||||
hiddenNames: namesUsed,
|
||||
}
|
||||
}
|
||||
|
||||
return &zcl.BodyContent{
|
||||
Attributes: attrs,
|
||||
Blocks: blocks,
|
||||
}, leftovers, diags
|
||||
}
|
||||
|
||||
func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
|
||||
@ -37,6 +229,12 @@ func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
name := item.Keys[0].Token.Value().(string)
|
||||
if _, hidden := b.hiddenNames[name]; hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(item.Keys) > 1 {
|
||||
name := item.Keys[0].Token.Value().(string)
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
@ -48,42 +246,56 @@ func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
|
||||
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()),
|
||||
}
|
||||
diags = append(diags, insertAttr(attrs, item)...)
|
||||
}
|
||||
|
||||
return attrs, diags
|
||||
}
|
||||
|
||||
func insertAttr(attrs zcl.Attributes, item *hclast.ObjectItem) zcl.Diagnostics {
|
||||
name := item.Keys[0].Token.Value().(string)
|
||||
var diags zcl.Diagnostics
|
||||
|
||||
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(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
attrs[name] = &zcl.Attribute{
|
||||
Name: name,
|
||||
Expr: &expression{src: item.Val},
|
||||
Range: rangeFromHCLPos(item.Pos()),
|
||||
NameRange: rangeFromHCLPos(item.Keys[0].Pos()),
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (b *body) MissingItemRange() zcl.Range {
|
||||
if len(b.oli.Items) == 0 {
|
||||
// Can't return a sensible range in this case, because HCL panics if
|
||||
// you ask for the position of an empty list.
|
||||
return zcl.Range{
|
||||
Filename: "<unknown>",
|
||||
}
|
||||
}
|
||||
return rangeFromHCLPos(b.oli.Pos())
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,206 @@ import (
|
||||
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
func TestBodyPartialContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
Source string
|
||||
Schema *zcl.BodySchema
|
||||
Want *zcl.BodyContent
|
||||
DiagCount int
|
||||
}{
|
||||
{
|
||||
``,
|
||||
&zcl.BodySchema{},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
`foo = 1`,
|
||||
&zcl.BodySchema{},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
`foo = 1`,
|
||||
&zcl.BodySchema{
|
||||
Attributes: []zcl.AttributeSchema{
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{
|
||||
"foo": {
|
||||
Name: "foo",
|
||||
Expr: &expression{
|
||||
src: &hclast.LiteralType{
|
||||
Token: hcltoken.Token{
|
||||
Type: hcltoken.NUMBER,
|
||||
Text: `1`,
|
||||
Pos: hcltoken.Pos{
|
||||
Offset: 6,
|
||||
Line: 1,
|
||||
Column: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
``,
|
||||
&zcl.BodySchema{
|
||||
Attributes: []zcl.AttributeSchema{
|
||||
{
|
||||
Name: "foo",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
},
|
||||
1, // missing required attribute
|
||||
},
|
||||
{
|
||||
`foo {}`,
|
||||
&zcl.BodySchema{
|
||||
Blocks: []zcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
Blocks: zcl.Blocks{
|
||||
{
|
||||
Type: "foo",
|
||||
Body: &body{
|
||||
oli: &hclast.ObjectList{},
|
||||
},
|
||||
DefRange: zcl.Range{
|
||||
Start: zcl.Pos{Byte: 4, Line: 1, Column: 5},
|
||||
End: zcl.Pos{Byte: 5, Line: 1, Column: 6},
|
||||
},
|
||||
TypeRange: zcl.Range{
|
||||
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
`foo "unwanted" {}`,
|
||||
&zcl.BodySchema{
|
||||
Blocks: []zcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
},
|
||||
1, // no labels are expected
|
||||
},
|
||||
{
|
||||
`foo {}`,
|
||||
&zcl.BodySchema{
|
||||
Blocks: []zcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "foo",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
},
|
||||
1, // missing name
|
||||
},
|
||||
{
|
||||
`foo "wanted" {}`,
|
||||
&zcl.BodySchema{
|
||||
Blocks: []zcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "foo",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.BodyContent{
|
||||
Attributes: zcl.Attributes{},
|
||||
Blocks: zcl.Blocks{
|
||||
{
|
||||
Type: "foo",
|
||||
Labels: []string{"wanted"},
|
||||
Body: &body{
|
||||
oli: &hclast.ObjectList{},
|
||||
},
|
||||
DefRange: zcl.Range{
|
||||
Start: zcl.Pos{Byte: 13, Line: 1, Column: 14},
|
||||
End: zcl.Pos{Byte: 14, Line: 1, Column: 15},
|
||||
},
|
||||
TypeRange: zcl.Range{
|
||||
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||
},
|
||||
LabelRanges: []zcl.Range{
|
||||
{
|
||||
Start: zcl.Pos{Byte: 4, Line: 1, Column: 5},
|
||||
End: zcl.Pos{Byte: 5, Line: 1, Column: 6},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
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.PartialContent(test.Schema)
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBodyJustAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
Source string
|
||||
|
Loading…
Reference in New Issue
Block a user