fde586e193
Currently only deals with the literal HCL structure. Later we will also support HIL parsing and evaluation within strings, to achieve parity with existing uses of HCL/HIL together. For now, this has parity with uses of HCL alone, with the exception that float and int values are not distinguished, because cty does not make this distinction.
387 lines
11 KiB
Go
387 lines
11 KiB
Go
package hclhil
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/apparentlymart/go-cty/cty"
|
|
"github.com/apparentlymart/go-zcl/zcl"
|
|
hclast "github.com/hashicorp/hcl/hcl/ast"
|
|
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
|
)
|
|
|
|
// body is our implementation of zcl.Body in terms of an HCL ObjectList
|
|
type body struct {
|
|
oli *hclast.ObjectList
|
|
hiddenNames map[string]struct{}
|
|
}
|
|
|
|
func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
|
|
content, _, diags := b.content(schema, false)
|
|
return content, diags
|
|
}
|
|
|
|
func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
|
|
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) {
|
|
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
|
|
}
|
|
|
|
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{
|
|
Severity: zcl.DiagError,
|
|
Summary: fmt.Sprintf("Unexpected %s block", name),
|
|
Detail: "Blocks are not allowed here.",
|
|
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// 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) {
|
|
return ctyValueFromHCLNode(e.src, ctx)
|
|
}
|
|
|
|
func (e *expression) Range() zcl.Range {
|
|
return rangeFromHCLPos(e.src.Pos())
|
|
}
|
|
func (e *expression) StartRange() zcl.Range {
|
|
return rangeFromHCLPos(e.src.Pos())
|
|
}
|
|
|
|
func ctyValueFromHCLNode(node hclast.Node, ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
|
|
|
switch tn := node.(type) {
|
|
case *hclast.LiteralType:
|
|
tok := tn.Token
|
|
switch tok.Type {
|
|
case hcltoken.NUMBER: // means integer, in HCL land
|
|
val := tok.Value().(int64)
|
|
return cty.NumberIntVal(val), nil
|
|
case hcltoken.FLOAT:
|
|
val := tok.Value().(float64)
|
|
return cty.NumberFloatVal(val), nil
|
|
case hcltoken.STRING, hcltoken.HEREDOC:
|
|
val := tok.Value().(string)
|
|
// TODO: HIL parsing and evaluation, if ctx is non-nil.
|
|
return cty.StringVal(val), nil
|
|
case hcltoken.BOOL:
|
|
val := tok.Value().(bool)
|
|
return cty.BoolVal(val), nil
|
|
default:
|
|
// should never happen
|
|
panic(fmt.Sprintf("unsupported HCL literal type %s", tok.Type))
|
|
}
|
|
case *hclast.ObjectType:
|
|
list := tn.List
|
|
attrs, diags := (&body{oli: list}).JustAttributes()
|
|
if attrs == nil {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
vals := map[string]cty.Value{}
|
|
for name, attr := range attrs {
|
|
val, valDiags := attr.Expr.Value(ctx)
|
|
if len(valDiags) > 0 {
|
|
diags = append(diags, valDiags...)
|
|
}
|
|
if val == cty.NilVal {
|
|
// If we skip one attribute then our return type will be
|
|
// inconsistent, so we'll prefer to return dynamic to prevent
|
|
// any weird downstream type errors.
|
|
return cty.DynamicVal, diags
|
|
}
|
|
vals[name] = val
|
|
}
|
|
return cty.ObjectVal(vals), diags
|
|
case *hclast.ListType:
|
|
nodes := tn.List
|
|
vals := make([]cty.Value, len(nodes))
|
|
var diags zcl.Diagnostics
|
|
for i, node := range nodes {
|
|
val, valDiags := ctyValueFromHCLNode(node, ctx)
|
|
if len(valDiags) > 0 {
|
|
diags = append(diags, valDiags...)
|
|
}
|
|
if val == cty.NilVal {
|
|
// If we skip one element then our return type will be
|
|
// inconsistent, so we'll prefer to return dynamic to prevent
|
|
// any weird downstream type errors.
|
|
return cty.DynamicVal, diags
|
|
}
|
|
vals[i] = val
|
|
}
|
|
return cty.TupleVal(vals), diags
|
|
default:
|
|
panic(fmt.Sprintf("unsupported HCL value type %T", tn))
|
|
}
|
|
|
|
}
|