package hclsyntax import ( "fmt" "strings" "github.com/hashicorp/hcl/v2" ) // AsHCLBlock returns the block data expressed as a *hcl.Block. func (b *Block) AsHCLBlock() *hcl.Block { if b == nil { return nil } lastHeaderRange := b.TypeRange if len(b.LabelRanges) > 0 { lastHeaderRange = b.LabelRanges[len(b.LabelRanges)-1] } return &hcl.Block{ Type: b.Type, Labels: b.Labels, Body: b.Body, DefRange: hcl.RangeBetween(b.TypeRange, lastHeaderRange), TypeRange: b.TypeRange, LabelRanges: b.LabelRanges, } } // Body is the implementation of hcl.Body for the HCL native syntax. type Body struct { Attributes Attributes Blocks Blocks // These are used with PartialContent to produce a "remaining items" // body to return. They are nil on all bodies fresh out of the parser. hiddenAttrs map[string]struct{} hiddenBlocks map[string]struct{} SrcRange hcl.Range EndRange hcl.Range // Final token of the body, for reporting missing items } // Assert that *Body implements hcl.Body var assertBodyImplBody hcl.Body = &Body{} func (b *Body) walkChildNodes(w internalWalkFunc) { w(b.Attributes) w(b.Blocks) } func (b *Body) Range() hcl.Range { return b.SrcRange } func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { content, remainHCL, diags := b.PartialContent(schema) // Now we'll see if anything actually remains, to produce errors about // extraneous items. remain := remainHCL.(*Body) for name, attr := range b.Attributes { if _, hidden := remain.hiddenAttrs[name]; !hidden { var suggestions []string for _, attrS := range schema.Attributes { if _, defined := content.Attributes[attrS.Name]; defined { continue } suggestions = append(suggestions, attrS.Name) } suggestion := nameSuggestion(name, suggestions) if suggestion != "" { suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) } else { // Is there a block of the same name? for _, blockS := range schema.Blocks { if blockS.Type == name { suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name) break } } } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsupported argument", Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion), Subject: &attr.NameRange, }) } } for _, block := range b.Blocks { blockTy := block.Type if _, hidden := remain.hiddenBlocks[blockTy]; !hidden { var suggestions []string for _, blockS := range schema.Blocks { suggestions = append(suggestions, blockS.Type) } suggestion := nameSuggestion(blockTy, suggestions) if suggestion != "" { suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) } else { // Is there an attribute of the same name? for _, attrS := range schema.Attributes { if attrS.Name == blockTy { suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy) break } } } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsupported block type", Detail: fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion), Subject: &block.TypeRange, }) } } return content, diags } func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { attrs := make(hcl.Attributes) var blocks hcl.Blocks var diags hcl.Diagnostics hiddenAttrs := make(map[string]struct{}) hiddenBlocks := make(map[string]struct{}) if b.hiddenAttrs != nil { for k, v := range b.hiddenAttrs { hiddenAttrs[k] = v } } if b.hiddenBlocks != nil { for k, v := range b.hiddenBlocks { hiddenBlocks[k] = v } } for _, attrS := range schema.Attributes { name := attrS.Name attr, exists := b.Attributes[name] _, hidden := hiddenAttrs[name] if hidden || !exists { if attrS.Required { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing required argument", Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name), Subject: b.MissingItemRange().Ptr(), }) } continue } hiddenAttrs[name] = struct{}{} attrs[name] = attr.AsHCLAttribute() } blocksWanted := make(map[string]hcl.BlockHeaderSchema) for _, blockS := range schema.Blocks { blocksWanted[blockS.Type] = blockS } for _, block := range b.Blocks { if _, hidden := hiddenBlocks[block.Type]; hidden { continue } blockS, wanted := blocksWanted[block.Type] if !wanted { continue } if len(block.Labels) > len(blockS.LabelNames) { name := block.Type if len(blockS.LabelNames) == 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Extraneous label for %s", name), Detail: fmt.Sprintf( "No labels are expected for %s blocks.", name, ), Subject: block.LabelRanges[0].Ptr(), Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(), }) } else { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.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, ), Subject: block.LabelRanges[len(blockS.LabelNames)].Ptr(), Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(), }) } continue } if len(block.Labels) < len(blockS.LabelNames) { name := block.Type diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Missing %s for %s", blockS.LabelNames[len(block.Labels)], name), Detail: fmt.Sprintf( "All %s blocks must have %d labels (%s).", name, len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), ), Subject: &block.OpenBraceRange, Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(), }) continue } blocks = append(blocks, block.AsHCLBlock()) } // We hide blocks only after we've processed all of them, since otherwise // we can't process more than one of the same type. for _, blockS := range schema.Blocks { hiddenBlocks[blockS.Type] = struct{}{} } remain := &Body{ Attributes: b.Attributes, Blocks: b.Blocks, hiddenAttrs: hiddenAttrs, hiddenBlocks: hiddenBlocks, SrcRange: b.SrcRange, EndRange: b.EndRange, } return &hcl.BodyContent{ Attributes: attrs, Blocks: blocks, MissingItemRange: b.MissingItemRange(), }, remain, diags } func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { attrs := make(hcl.Attributes) var diags hcl.Diagnostics if len(b.Blocks) > 0 { example := b.Blocks[0] diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Unexpected %q block", example.Type), Detail: "Blocks are not allowed here.", Subject: &example.TypeRange, }) // we will continue processing anyway, and return the attributes // we are able to find so that certain analyses can still be done // in the face of errors. } if b.Attributes == nil { return attrs, diags } for name, attr := range b.Attributes { if _, hidden := b.hiddenAttrs[name]; hidden { continue } attrs[name] = attr.AsHCLAttribute() } return attrs, diags } func (b *Body) MissingItemRange() hcl.Range { return hcl.Range{ Filename: b.SrcRange.Filename, Start: b.SrcRange.Start, End: b.SrcRange.Start, } } // Attributes is the collection of attribute definitions within a body. type Attributes map[string]*Attribute func (a Attributes) walkChildNodes(w internalWalkFunc) { for _, attr := range a { w(attr) } } // Range returns the range of some arbitrary point within the set of // attributes, or an invalid range if there are no attributes. // // This is provided only to complete the Node interface, but has no practical // use. func (a Attributes) Range() hcl.Range { // An attributes doesn't really have a useful range to report, since // it's just a grouping construct. So we'll arbitrarily take the // range of one of the attributes, or produce an invalid range if we have // none. In practice, there's little reason to ask for the range of // an Attributes. for _, attr := range a { return attr.Range() } return hcl.Range{ Filename: "", } } // Attribute represents a single attribute definition within a body. type Attribute struct { Name string Expr Expression SrcRange hcl.Range NameRange hcl.Range EqualsRange hcl.Range } func (a *Attribute) walkChildNodes(w internalWalkFunc) { w(a.Expr) } func (a *Attribute) Range() hcl.Range { return a.SrcRange } // AsHCLAttribute returns the block data expressed as a *hcl.Attribute. func (a *Attribute) AsHCLAttribute() *hcl.Attribute { if a == nil { return nil } return &hcl.Attribute{ Name: a.Name, Expr: a.Expr, Range: a.SrcRange, NameRange: a.NameRange, } } // Blocks is the list of nested blocks within a body. type Blocks []*Block func (bs Blocks) walkChildNodes(w internalWalkFunc) { for _, block := range bs { w(block) } } // Range returns the range of some arbitrary point within the list of // blocks, or an invalid range if there are no blocks. // // This is provided only to complete the Node interface, but has no practical // use. func (bs Blocks) Range() hcl.Range { if len(bs) > 0 { return bs[0].Range() } return hcl.Range{ Filename: "", } } // Block represents a nested block structure type Block struct { Type string Labels []string Body *Body TypeRange hcl.Range LabelRanges []hcl.Range OpenBraceRange hcl.Range CloseBraceRange hcl.Range } func (b *Block) walkChildNodes(w internalWalkFunc) { w(b.Body) } func (b *Block) Range() hcl.Range { return hcl.RangeBetween(b.TypeRange, b.CloseBraceRange) } func (b *Block) DefRange() hcl.Range { return hcl.RangeBetween(b.TypeRange, b.OpenBraceRange) }