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: "", } } 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)) } }