hcl/hclpack/structure.go
Antti Kupila f7764c6954 hclpack: Fix name range for unsupported attribute
Fixes an issue where the name range may be incorrect in case there are
multiple attributes and one of them is wrong. Another attribute's name
range could overwrite the previous one as the attr variable is
overwritten in the for loop.
2019-04-18 07:51:23 -07:00

298 lines
7.9 KiB
Go

package hclpack
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
)
// Body is an implementation of hcl.Body.
type Body struct {
Attributes map[string]Attribute
ChildBlocks []Block
MissingItemRange_ hcl.Range
}
var _ hcl.Body = (*Body)(nil)
// Content is an implementation of the method of the same name on hcl.Body.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// are guaranteed to be of type *hclpack.Body, so callers can type-assert
// to obtain a child Body in order to serialize it separately if needed.
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
return b.content(schema, nil)
}
// PartialContent is an implementation of the method of the same name on hcl.Body.
//
// The returned "remain" body may share some backing objects with the receiver,
// so neither the receiver nor the returned remain body, or any descendent
// objects within them, may be mutated after this method is used.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// and the returned "remain" body are guaranteed to be of type *hclpack.Body,
// so callers can type-assert to obtain a child Body in order to serialize it
// separately if needed.
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
remain := &Body{
MissingItemRange_: b.MissingItemRange_,
}
content, diags := b.content(schema, remain)
return content, remain, diags
}
func (b *Body) content(schema *hcl.BodySchema, remain *Body) (*hcl.BodyContent, hcl.Diagnostics) {
if b == nil {
b = &Body{} // We'll treat a nil body like an empty one, for convenience
}
var diags hcl.Diagnostics
var attrs map[string]*hcl.Attribute
var attrUsed map[string]struct{}
if len(b.Attributes) > 0 {
attrs = make(map[string]*hcl.Attribute, len(b.Attributes))
attrUsed = make(map[string]struct{}, len(b.Attributes))
}
for _, attrS := range schema.Attributes {
name := attrS.Name
attr, exists := b.Attributes[name]
if !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_,
})
}
continue
}
attrs[name] = attr.asHCLAttribute(name)
attrUsed[name] = struct{}{}
}
for name, attr := range b.Attributes {
if _, used := attrUsed[name]; used {
continue
}
if remain != nil {
remain.setAttribute(name, attr)
continue
}
var suggestions []string
for _, attrS := range schema.Attributes {
if _, defined := 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.Ptr(),
})
}
blocksWanted := make(map[string]hcl.BlockHeaderSchema)
for _, blockS := range schema.Blocks {
blocksWanted[blockS.Type] = blockS
}
var blocks []*hcl.Block
for _, block := range b.ChildBlocks {
// Redeclare block on stack so the pointer to the body is set on the
// correct block. https://github.com/hashicorp/hcl2/issues/72
block := block
blockTy := block.Type
blockS, wanted := blocksWanted[blockTy]
if !wanted {
if remain != nil {
remain.appendBlock(block)
continue
}
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?", 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,
})
continue
}
if len(block.Labels) != len(blockS.LabelNames) {
if len(blockS.LabelNames) == 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Extraneous label for %s", blockTy),
Detail: fmt.Sprintf(
"No labels are expected for %s blocks.", blockTy,
),
Subject: &block.DefRange,
Context: &block.DefRange,
})
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Wrong label count for %s", blockTy),
Detail: fmt.Sprintf(
"%s blocks expect %d label(s), but got %d.",
blockTy, len(blockS.LabelNames), len(block.Labels),
),
Subject: &block.DefRange,
Context: &block.DefRange,
})
}
continue
}
blocks = append(blocks, block.asHCLBlock())
}
return &hcl.BodyContent{
Attributes: attrs,
Blocks: blocks,
MissingItemRange: b.MissingItemRange_,
}, diags
}
// JustAttributes is an implementation of the method of the same name on hcl.Body.
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(b.ChildBlocks) > 0 {
for _, block := range b.ChildBlocks {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unexpected %s block", block.Type),
Detail: "Blocks are not allowed here.",
Context: &block.TypeRange,
})
}
// We'll continue processing anyway, and return any attributes we find
// so that the caller can do careful partial analysis.
}
if len(b.Attributes) == 0 {
return nil, diags
}
ret := make(hcl.Attributes, len(b.Attributes))
for n, a := range b.Attributes {
ret[n] = a.asHCLAttribute(n)
}
return ret, diags
}
// MissingItemRange is an implementation of the method of the same name on hcl.Body.
func (b *Body) MissingItemRange() hcl.Range {
return b.MissingItemRange_
}
func (b *Body) setAttribute(name string, attr Attribute) {
if b.Attributes == nil {
b.Attributes = make(map[string]Attribute)
}
b.Attributes[name] = attr
}
func (b *Body) appendBlock(block Block) {
b.ChildBlocks = append(b.ChildBlocks, block)
}
func (b *Body) addRanges(rngs map[hcl.Range]struct{}) {
rngs[b.MissingItemRange_] = struct{}{}
for _, attr := range b.Attributes {
attr.addRanges(rngs)
}
for _, block := range b.ChildBlocks {
block.addRanges(rngs)
}
}
// Block represents a nested block within a body.
type Block struct {
Type string
Labels []string
Body Body
DefRange, TypeRange hcl.Range
LabelRanges []hcl.Range
}
func (b *Block) asHCLBlock() *hcl.Block {
return &hcl.Block{
Type: b.Type,
Labels: b.Labels,
Body: &b.Body,
TypeRange: b.TypeRange,
DefRange: b.DefRange,
LabelRanges: b.LabelRanges,
}
}
func (b *Block) addRanges(rngs map[hcl.Range]struct{}) {
rngs[b.DefRange] = struct{}{}
rngs[b.TypeRange] = struct{}{}
for _, rng := range b.LabelRanges {
rngs[rng] = struct{}{}
}
b.Body.addRanges(rngs)
}
// Attribute represents an attribute definition within a body.
type Attribute struct {
Expr Expression
Range, NameRange hcl.Range
}
func (a *Attribute) asHCLAttribute(name string) *hcl.Attribute {
return &hcl.Attribute{
Name: name,
Expr: &a.Expr,
Range: a.Range,
NameRange: a.NameRange,
}
}
func (a *Attribute) addRanges(rngs map[hcl.Range]struct{}) {
rngs[a.Range] = struct{}{}
rngs[a.NameRange] = struct{}{}
a.Expr.addRanges(rngs)
}