Merge pull request #19453 from hashicorp/b-whole-body-diag-rendering

command/format: Fix rendering of attribute-agnostic diagnostics
This commit is contained in:
Radek Simko 2018-11-26 23:52:21 +00:00 committed by GitHub
commit 9e5677adeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 701 additions and 161 deletions

View File

@ -58,23 +58,11 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
if sourceRefs.Context != nil {
snippetRange = sourceRefs.Context.ToHCL()
}
// Make sure the snippet includes the highlight. This should be true
// for any reasonable diagnostic, but we'll make sure.
snippetRange = hcl.RangeOver(snippetRange, highlightRange)
// We can't illustrate an empty range, so we'll turn such ranges into
// single-character ranges, which might not be totally valid (may point
// off the end of a line, or off the end of the file) but are good
// enough for the bounds checks we do below.
if snippetRange.Empty() {
snippetRange.End.Byte++
snippetRange.End.Column++
}
if highlightRange.Empty() {
highlightRange.End.Byte++
highlightRange.End.Column++
}
var src []byte
if sources != nil {
src = sources[snippetRange.Filename]
@ -86,12 +74,25 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
// a not-so-helpful error message.
fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line)
} else {
contextStr := sourceCodeContextStr(src, highlightRange)
file, offset := parseRange(src, highlightRange)
headerRange := highlightRange
if snippetRange.Empty() {
// We assume that empty range signals diagnostic
// related to the whole body, so we lookup the definition
// instead of attempting to render empty range
snippetRange = hcled.ContextDefRange(file, offset-1)
headerRange = snippetRange
}
contextStr := hcled.ContextString(file, offset-1)
if contextStr != "" {
contextStr = ", in " + contextStr
}
fmt.Fprintf(&buf, " on %s line %d%s:\n", highlightRange.Filename, highlightRange.Start.Line, contextStr)
fmt.Fprintf(&buf, " on %s line %d%s:\n", headerRange.Filename, headerRange.Start.Line, contextStr)
// Config snippet rendering
sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
for sc.Scan() {
lineRange := sc.Range()
@ -185,7 +186,7 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
// An empty string is returned if no suitable description is available, e.g.
// because the source is invalid, or because the offset is not inside any sort
// of identifiable container.
func sourceCodeContextStr(src []byte, rng hcl.Range) string {
func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) {
filename := rng.Filename
offset := rng.Start.Byte
@ -203,10 +204,10 @@ func sourceCodeContextStr(src []byte, rng hcl.Range) string {
file, diags = parser.ParseHCL(src, filename)
}
if diags.HasErrors() {
return ""
return file, offset
}
return hcled.ContextString(file, offset)
return file, offset
}
// traversalStr produces a representation of an HCL traversal that is compact,

View File

@ -148,11 +148,11 @@ func TestOutputWithoutValueShouldFail(t *testing.T) {
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
}
wantError := `The attribute "value" is required, but no definition was found.`
wantError := `The argument "value" is required, but no definition was found.`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
}
wantError = `An attribute named "values" is not expected here. Did you mean "value"?`
wantError = `An argument named "values" is not expected here. Did you mean "value"?`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
}

View File

@ -102,7 +102,7 @@ func TestParserLoadConfigFileFailureMessages(t *testing.T) {
{
"invalid-files/unexpected-attr.tf",
hcl.DiagError,
"Unsupported attribute",
"Unsupported argument",
},
{
"invalid-files/unexpected-block.tf",

2
go.mod
View File

@ -73,7 +73,7 @@ require (
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl2 v0.0.0-20180925175540-3f1c5474d4f7
github.com/hashicorp/hcl2 v0.0.0-20181126233546-67424e43b184
github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250
github.com/hashicorp/logutils v0.0.0-20150609070431-0dc08b1671f3
github.com/hashicorp/memberlist v0.1.0 // indirect

6
go.sum
View File

@ -50,6 +50,8 @@ github.com/blang/semver v0.0.0-20170202183821-4a1e882c79dc h1:J/iAaGTCZYfT/allw6
github.com/blang/semver v0.0.0-20170202183821-4a1e882c79dc/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e h1:D64GF/Xr5zSUnM3q1Jylzo4sK7szhP/ON+nb2DB5XJA=
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20161106042343-c914be64f07d h1:aG5FcWiZTOhPQzYIxwxSR1zEOxzL32fwr1CsaCfhO6w=
@ -166,6 +168,10 @@ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+Db
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl2 v0.0.0-20180925175540-3f1c5474d4f7 h1:lNBI8nmNlIpLl8Lqf+9JLBmNGrU91Nlt71q7cGpZi+k=
github.com/hashicorp/hcl2 v0.0.0-20180925175540-3f1c5474d4f7/go.mod h1:hrRmgPTdXWuNLpHcv7cRXL+ofoFsY76vDylTzH8eu3E=
github.com/hashicorp/hcl2 v0.0.0-20181123160133-f901b36da204 h1:/rUVBMdrcpWOwNw5KFhqrbMVnbgsN1zJOp5sAEx2XXQ=
github.com/hashicorp/hcl2 v0.0.0-20181123160133-f901b36da204/go.mod h1:4nBvwJRETsbpa0LQ7FbXXVFmo0Crvhya1Dmpbm7cVow=
github.com/hashicorp/hcl2 v0.0.0-20181126233546-67424e43b184 h1:cBDzEsx+Tm9vHp9WiUeBlhPazwbbKJxOiRAT+pVLuag=
github.com/hashicorp/hcl2 v0.0.0-20181126233546-67424e43b184/go.mod h1:4nBvwJRETsbpa0LQ7FbXXVFmo0Crvhya1Dmpbm7cVow=
github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250 h1:fooK5IvDL/KIsi4LxF/JH68nVdrBSiGNPhS2JAQjtjo=
github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250/go.mod h1:KHvg/R2/dPtaePb16oW4qIyzkMxXOL38xjRN64adsts=
github.com/hashicorp/logutils v0.0.0-20150609070431-0dc08b1671f3 h1:oD64EFjELI9RY9yoWlfua58r+etdnoIC871z+rr6lkA=

View File

@ -40,6 +40,10 @@
// present then any attributes or blocks not matched by another valid tag
// will cause an error diagnostic.
//
// Only a subset of this tagging/typing vocabulary is supported for the
// "Encode" family of functions. See the EncodeIntoBody docs for full details
// on the constraints there.
//
// Broadly-speaking this package deals with two types of error. The first is
// errors in the configuration itself, which are returned as diagnostics
// written with the configuration author as the target audience. The second

191
vendor/github.com/hashicorp/hcl2/gohcl/encode.go generated vendored Normal file
View File

@ -0,0 +1,191 @@
package gohcl
import (
"fmt"
"reflect"
"sort"
"github.com/hashicorp/hcl2/hclwrite"
"github.com/zclconf/go-cty/cty/gocty"
)
// EncodeIntoBody replaces the contents of the given hclwrite Body with
// attributes and blocks derived from the given value, which must be a
// struct value or a pointer to a struct value with the struct tags defined
// in this package.
//
// This function can work only with fully-decoded data. It will ignore any
// fields tagged as "remain", any fields that decode attributes into either
// hcl.Attribute or hcl.Expression values, and any fields that decode blocks
// into hcl.Attributes values. This function does not have enough information
// to complete the decoding of these types.
//
// Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock
// to produce a whole hclwrite.Block including block labels.
//
// As long as a suitable value is given to encode and the destination body
// is non-nil, this function will always complete. It will panic in case of
// any errors in the calling program, such as passing an inappropriate type
// or a nil body.
//
// The layout of the resulting HCL source is derived from the ordering of
// the struct fields, with blank lines around nested blocks of different types.
// Fields representing attributes should usually precede those representing
// blocks so that the attributes can group togather in the result. For more
// control, use the hclwrite API directly.
func EncodeIntoBody(val interface{}, dst *hclwrite.Body) {
rv := reflect.ValueOf(val)
ty := rv.Type()
if ty.Kind() == reflect.Ptr {
rv = rv.Elem()
ty = rv.Type()
}
if ty.Kind() != reflect.Struct {
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
}
tags := getFieldTags(ty)
populateBody(rv, ty, tags, dst)
}
// EncodeAsBlock creates a new hclwrite.Block populated with the data from
// the given value, which must be a struct or pointer to struct with the
// struct tags defined in this package.
//
// If the given struct type has fields tagged with "label" tags then they
// will be used in order to annotate the created block with labels.
//
// This function has the same constraints as EncodeIntoBody and will panic
// if they are violated.
func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block {
rv := reflect.ValueOf(val)
ty := rv.Type()
if ty.Kind() == reflect.Ptr {
rv = rv.Elem()
ty = rv.Type()
}
if ty.Kind() != reflect.Struct {
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
}
tags := getFieldTags(ty)
labels := make([]string, len(tags.Labels))
for i, lf := range tags.Labels {
lv := rv.Field(lf.FieldIndex)
// We just stringify whatever we find. It should always be a string
// but if not then we'll still do something reasonable.
labels[i] = fmt.Sprintf("%s", lv.Interface())
}
block := hclwrite.NewBlock(blockType, labels)
populateBody(rv, ty, tags, block.Body())
return block
}
func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) {
nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks))
namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks))
for n, i := range tags.Attributes {
nameIdxs[n] = i
namesOrder = append(namesOrder, n)
}
for n, i := range tags.Blocks {
nameIdxs[n] = i
namesOrder = append(namesOrder, n)
}
sort.SliceStable(namesOrder, func(i, j int) bool {
ni, nj := namesOrder[i], namesOrder[j]
return nameIdxs[ni] < nameIdxs[nj]
})
dst.Clear()
prevWasBlock := false
for _, name := range namesOrder {
fieldIdx := nameIdxs[name]
field := ty.Field(fieldIdx)
fieldTy := field.Type
fieldVal := rv.Field(fieldIdx)
if fieldTy.Kind() == reflect.Ptr {
fieldTy = fieldTy.Elem()
fieldVal = fieldVal.Elem()
}
if _, isAttr := tags.Attributes[name]; isAttr {
if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) {
continue // ignore undecoded fields
}
if !fieldVal.IsValid() {
continue // ignore (field value is nil pointer)
}
if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
continue // ignore
}
if prevWasBlock {
dst.AppendNewline()
prevWasBlock = false
}
valTy, err := gocty.ImpliedType(fieldVal.Interface())
if err != nil {
panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err))
}
val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy)
if err != nil {
// This should never happen, since we should always be able
// to decode into the implied type.
panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err))
}
dst.SetAttributeValue(name, val)
} else { // must be a block, then
elemTy := fieldTy
isSeq := false
if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array {
isSeq = true
elemTy = elemTy.Elem()
}
if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) {
continue // ignore undecoded fields
}
prevWasBlock = false
if isSeq {
l := fieldVal.Len()
for i := 0; i < l; i++ {
elemVal := fieldVal.Index(i)
if !elemVal.IsValid() {
continue // ignore (elem value is nil pointer)
}
if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() {
continue // ignore
}
block := EncodeAsBlock(elemVal.Interface(), name)
if !prevWasBlock {
dst.AppendNewline()
prevWasBlock = true
}
dst.AppendBlock(block)
}
} else {
if !fieldVal.IsValid() {
continue // ignore (field value is nil pointer)
}
if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
continue // ignore
}
block := EncodeAsBlock(fieldVal.Interface(), name)
if !prevWasBlock {
dst.AppendNewline()
prevWasBlock = true
}
dst.AppendBlock(block)
}
}
}
}

View File

@ -132,7 +132,7 @@ type RelativeTraversalExpr struct {
}
func (e *RelativeTraversalExpr) walkChildNodes(w internalWalkFunc) {
e.Source = w(e.Source).(Expression)
w(e.Source)
}
func (e *RelativeTraversalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
@ -181,8 +181,8 @@ type FunctionCallExpr struct {
}
func (e *FunctionCallExpr) walkChildNodes(w internalWalkFunc) {
for i, arg := range e.Args {
e.Args[i] = w(arg).(Expression)
for _, arg := range e.Args {
w(arg)
}
}
@ -463,9 +463,9 @@ type ConditionalExpr struct {
}
func (e *ConditionalExpr) walkChildNodes(w internalWalkFunc) {
e.Condition = w(e.Condition).(Expression)
e.TrueResult = w(e.TrueResult).(Expression)
e.FalseResult = w(e.FalseResult).(Expression)
w(e.Condition)
w(e.TrueResult)
w(e.FalseResult)
}
func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
@ -593,8 +593,8 @@ type IndexExpr struct {
}
func (e *IndexExpr) walkChildNodes(w internalWalkFunc) {
e.Collection = w(e.Collection).(Expression)
e.Key = w(e.Key).(Expression)
w(e.Collection)
w(e.Key)
}
func (e *IndexExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
@ -625,8 +625,8 @@ type TupleConsExpr struct {
}
func (e *TupleConsExpr) walkChildNodes(w internalWalkFunc) {
for i, expr := range e.Exprs {
e.Exprs[i] = w(expr).(Expression)
for _, expr := range e.Exprs {
w(expr)
}
}
@ -674,9 +674,9 @@ type ObjectConsItem struct {
}
func (e *ObjectConsExpr) walkChildNodes(w internalWalkFunc) {
for i, item := range e.Items {
e.Items[i].KeyExpr = w(item.KeyExpr).(Expression)
e.Items[i].ValueExpr = w(item.ValueExpr).(Expression)
for _, item := range e.Items {
w(item.KeyExpr)
w(item.ValueExpr)
}
}
@ -792,7 +792,7 @@ func (e *ObjectConsKeyExpr) walkChildNodes(w internalWalkFunc) {
// We only treat our wrapped expression as a real expression if we're
// not going to interpret it as a literal.
if e.literalName() == "" {
e.Wrapped = w(e.Wrapped).(Expression)
w(e.Wrapped)
}
}
@ -1157,7 +1157,7 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
}
func (e *ForExpr) walkChildNodes(w internalWalkFunc) {
e.CollExpr = w(e.CollExpr).(Expression)
w(e.CollExpr)
scopeNames := map[string]struct{}{}
if e.KeyVar != "" {
@ -1170,17 +1170,17 @@ func (e *ForExpr) walkChildNodes(w internalWalkFunc) {
if e.KeyExpr != nil {
w(ChildScope{
LocalNames: scopeNames,
Expr: &e.KeyExpr,
Expr: e.KeyExpr,
})
}
w(ChildScope{
LocalNames: scopeNames,
Expr: &e.ValExpr,
Expr: e.ValExpr,
})
if e.CondExpr != nil {
w(ChildScope{
LocalNames: scopeNames,
Expr: &e.CondExpr,
Expr: e.CondExpr,
})
}
}
@ -1266,8 +1266,8 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
}
func (e *SplatExpr) walkChildNodes(w internalWalkFunc) {
e.Source = w(e.Source).(Expression)
e.Each = w(e.Each).(Expression)
w(e.Source)
w(e.Each)
}
func (e *SplatExpr) Range() hcl.Range {

View File

@ -129,8 +129,8 @@ type BinaryOpExpr struct {
}
func (e *BinaryOpExpr) walkChildNodes(w internalWalkFunc) {
e.LHS = w(e.LHS).(Expression)
e.RHS = w(e.RHS).(Expression)
w(e.LHS)
w(e.RHS)
}
func (e *BinaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
@ -212,7 +212,7 @@ type UnaryOpExpr struct {
}
func (e *UnaryOpExpr) walkChildNodes(w internalWalkFunc) {
e.Val = w(e.Val).(Expression)
w(e.Val)
}
func (e *UnaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {

View File

@ -16,8 +16,8 @@ type TemplateExpr struct {
}
func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
for i, part := range e.Parts {
e.Parts[i] = w(part).(Expression)
for _, part := range e.Parts {
w(part)
}
}
@ -98,7 +98,7 @@ type TemplateJoinExpr struct {
}
func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) {
e.Tuple = w(e.Tuple).(Expression)
w(e.Tuple)
}
func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
@ -184,7 +184,7 @@ type TemplateWrapExpr struct {
}
func (e *TemplateWrapExpr) walkChildNodes(w internalWalkFunc) {
e.Wrapped = w(e.Wrapped).(Expression)
w(e.Wrapped)
}
func (e *TemplateWrapExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {

View File

@ -3,6 +3,8 @@ package hclsyntax
import (
"bytes"
"fmt"
"github.com/hashicorp/hcl2/hcl"
)
type navigation struct {
@ -39,3 +41,19 @@ func (n navigation) ContextString(offset int) string {
}
return buf.String()
}
func (n navigation) ContextDefRange(offset int) hcl.Range {
var block *Block
for _, candidate := range n.root.Blocks {
if candidate.Range().ContainsOffset(offset) {
block = candidate
break
}
}
if block == nil {
return hcl.Range{}
}
return block.DefRange()
}

View File

@ -19,4 +19,4 @@ type Node interface {
Range() hcl.Range
}
type internalWalkFunc func(Node) Node
type internalWalkFunc func(Node)

View File

@ -273,7 +273,10 @@ Token:
return &Block{
Type: blockType,
Labels: labels,
Body: nil,
Body: &Body{
SrcRange: ident.Range,
EndRange: ident.Range,
},
TypeRange: ident.Range,
LabelRanges: labelRanges,

View File

@ -47,8 +47,8 @@ type Body struct {
var assertBodyImplBody hcl.Body = &Body{}
func (b *Body) walkChildNodes(w internalWalkFunc) {
b.Attributes = w(b.Attributes).(Attributes)
b.Blocks = w(b.Blocks).(Blocks)
w(b.Attributes)
w(b.Blocks)
}
func (b *Body) Range() hcl.Range {
@ -86,8 +86,8 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported attribute",
Detail: fmt.Sprintf("An attribute named %q is not expected here.%s", name, suggestion),
Summary: "Unsupported argument",
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
Subject: &attr.NameRange,
})
}
@ -107,7 +107,7 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
// 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 attribute %q?", blockTy)
suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy)
break
}
}
@ -151,8 +151,8 @@ func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Bod
if attrS.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required attribute",
Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name),
Summary: "Missing required argument",
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
Subject: b.MissingItemRange().Ptr(),
})
}
@ -286,8 +286,8 @@ func (b *Body) MissingItemRange() hcl.Range {
type Attributes map[string]*Attribute
func (a Attributes) walkChildNodes(w internalWalkFunc) {
for k, attr := range a {
a[k] = w(attr).(*Attribute)
for _, attr := range a {
w(attr)
}
}
@ -321,7 +321,7 @@ type Attribute struct {
}
func (a *Attribute) walkChildNodes(w internalWalkFunc) {
a.Expr = w(a.Expr).(Expression)
w(a.Expr)
}
func (a *Attribute) Range() hcl.Range {
@ -346,8 +346,8 @@ func (a *Attribute) AsHCLAttribute() *hcl.Attribute {
type Blocks []*Block
func (bs Blocks) walkChildNodes(w internalWalkFunc) {
for i, block := range bs {
bs[i] = w(block).(*Block)
for _, block := range bs {
w(block)
}
}
@ -378,9 +378,13 @@ type Block struct {
}
func (b *Block) walkChildNodes(w internalWalkFunc) {
b.Body = w(b.Body).(*Body)
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)
}

View File

@ -72,15 +72,15 @@ func (w *variablesWalker) Exit(n Node) hcl.Diagnostics {
// that the child scope struct wraps.
type ChildScope struct {
LocalNames map[string]struct{}
Expr *Expression // pointer because it can be replaced on walk
Expr Expression
}
func (e ChildScope) walkChildNodes(w internalWalkFunc) {
*(e.Expr) = w(*(e.Expr)).(Expression)
w(e.Expr)
}
// Range returns the range of the expression that the ChildScope is
// encapsulating. It isn't really very useful to call Range on a ChildScope.
func (e ChildScope) Range() hcl.Range {
return (*e.Expr).Range()
return e.Expr.Range()
}

View File

@ -15,9 +15,8 @@ type VisitFunc func(node Node) hcl.Diagnostics
// and returned as a single set.
func VisitAll(node Node, f VisitFunc) hcl.Diagnostics {
diags := f(node)
node.walkChildNodes(func(node Node) Node {
node.walkChildNodes(func(node Node) {
diags = append(diags, VisitAll(node, f)...)
return node
})
return diags
}
@ -33,47 +32,10 @@ type Walker interface {
// Enter and Exit functions.
func Walk(node Node, w Walker) hcl.Diagnostics {
diags := w.Enter(node)
node.walkChildNodes(func(node Node) Node {
node.walkChildNodes(func(node Node) {
diags = append(diags, Walk(node, w)...)
return node
})
moreDiags := w.Exit(node)
diags = append(diags, moreDiags...)
return diags
}
// Transformer is an interface used with Transform
type Transformer interface {
// Transform accepts a node and returns a replacement node along with
// a flag for whether to also visit child nodes. If the flag is false,
// none of the child nodes will be visited and the TransformExit method
// will not be called for the node.
//
// It is acceptable and appropriate for Transform to return the same node
// it was given, for situations where no transform is needed.
Transform(node Node) (Node, bool, hcl.Diagnostics)
// TransformExit signals the end of transformations of child nodes of the
// given node. If Transform returned a new node, the given node is the
// node that was returned, rather than the node that was originally
// encountered.
TransformExit(node Node) hcl.Diagnostics
}
// Transform allows for in-place transformations of an AST starting with a
// particular node. The provider Transformer implementation drives the
// transformation process. The return value is the node that replaced the
// given top-level node.
func Transform(node Node, t Transformer) (Node, hcl.Diagnostics) {
newNode, descend, diags := t.Transform(node)
if !descend {
return newNode, diags
}
node.walkChildNodes(func(node Node) Node {
newNode, newDiags := Transform(node, t)
diags = append(diags, newDiags...)
return newNode
})
diags = append(diags, t.TransformExit(newNode)...)
return newNode, diags
}

View File

@ -18,3 +18,17 @@ func ContextString(file *hcl.File, offset int) string {
}
return ""
}
type contextDefRanger interface {
ContextDefRange(offset int) hcl.Range
}
func ContextDefRange(file *hcl.File, offset int) hcl.Range {
if cser, ok := file.Nav.(contextDefRanger); ok {
defRange := cser.ContextDefRange(offset)
if !defRange.Empty() {
return defRange
}
}
return file.Body.MissingItemRange()
}

View File

@ -12,6 +12,17 @@ type File struct {
body *node
}
// NewEmptyFile constructs a new file with no content, ready to be mutated
// by other calls that append to its body.
func NewEmptyFile() *File {
f := &File{
inTree: newInTree(),
}
body := newBody()
f.body = f.children.Append(body)
return f
}
// Body returns the root body of the file, which contains the top-level
// attributes and blocks.
func (f *File) Body() *Body {
@ -22,7 +33,7 @@ func (f *File) Body() *Body {
//
// The tokens first have a simple formatting pass applied that adjusts only
// the spaces between them.
func (f *File) WriteTo(wr io.Writer) (int, error) {
func (f *File) WriteTo(wr io.Writer) (int64, error) {
tokens := f.inTree.children.BuildTokens(nil)
format(tokens)
return tokens.WriteTo(wr)

View File

@ -0,0 +1,48 @@
package hclwrite
import (
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
type Attribute struct {
inTree
leadComments *node
name *node
expr *node
lineComments *node
}
func newAttribute() *Attribute {
return &Attribute{
inTree: newInTree(),
}
}
func (a *Attribute) init(name string, expr *Expression) {
expr.assertUnattached()
nameTok := newIdentToken(name)
nameObj := newIdentifier(nameTok)
a.leadComments = a.children.Append(newComments(nil))
a.name = a.children.Append(nameObj)
a.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
},
})
a.expr = a.children.Append(expr)
a.expr.list = a.children
a.lineComments = a.children.Append(newComments(nil))
a.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
},
})
}
func (a *Attribute) Expr() *Expression {
return a.expr.content.(*Expression)
}

74
vendor/github.com/hashicorp/hcl2/hclwrite/ast_block.go generated vendored Normal file
View File

@ -0,0 +1,74 @@
package hclwrite
import (
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type Block struct {
inTree
leadComments *node
typeName *node
labels nodeSet
open *node
body *node
close *node
}
func newBlock() *Block {
return &Block{
inTree: newInTree(),
labels: newNodeSet(),
}
}
// NewBlock constructs a new, empty block with the given type name and labels.
func NewBlock(typeName string, labels []string) *Block {
block := newBlock()
block.init(typeName, labels)
return block
}
func (b *Block) init(typeName string, labels []string) {
nameTok := newIdentToken(typeName)
nameObj := newIdentifier(nameTok)
b.leadComments = b.children.Append(newComments(nil))
b.typeName = b.children.Append(nameObj)
for _, label := range labels {
labelToks := TokensForValue(cty.StringVal(label))
labelObj := newQuoted(labelToks)
labelNode := b.children.Append(labelObj)
b.labels.Add(labelNode)
}
b.open = b.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
},
})
body := newBody() // initially totally empty; caller can append to it subsequently
b.body = b.children.Append(body)
b.close = b.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
},
})
}
// Body returns the body that represents the content of the receiving block.
//
// Appending to or otherwise modifying this body will make changes to the
// tokens that are generated between the blocks open and close braces.
func (b *Block) Body() *Body {
return b.body.content.(*Body)
}

View File

@ -12,6 +12,13 @@ type Body struct {
items nodeSet
}
func newBody() *Body {
return &Body{
inTree: newInTree(),
items: newNodeSet(),
}
}
func (b *Body) appendItem(c nodeContent) *node {
nn := b.children.Append(c)
b.items.Add(nn)
@ -25,10 +32,40 @@ func (b *Body) appendItemNode(nn *node) *node {
return nn
}
// Clear removes all of the items from the body, making it empty.
func (b *Body) Clear() {
b.children.Clear()
}
func (b *Body) AppendUnstructuredTokens(ts Tokens) {
b.inTree.children.Append(ts)
}
// Attributes returns a new map of all of the attributes in the body, with
// the attribute names as the keys.
func (b *Body) Attributes() map[string]*Attribute {
ret := make(map[string]*Attribute)
for n := range b.items {
if attr, isAttr := n.content.(*Attribute); isAttr {
nameObj := attr.name.content.(*identifier)
name := string(nameObj.token.Bytes)
ret[name] = attr
}
}
return ret
}
// Blocks returns a new slice of all the blocks in the body.
func (b *Body) Blocks() []*Block {
ret := make([]*Block, 0, len(b.items))
for n := range b.items {
if block, isBlock := n.content.(*Block); isBlock {
ret = append(ret, block)
}
}
return ret
}
// GetAttribute returns the attribute from the body that has the given name,
// or returns nil if there is currently no matching attribute.
func (b *Body) GetAttribute(name string) *Attribute {
@ -67,7 +104,7 @@ func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
}
// SetAttributeTraversal either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block.
// of the given name or adds a new attribute definition to the end of the body.
//
// The new expression is given as a hcl.Traversal, which must be an absolute
// traversal. To set a literal value, use SetAttributeValue.
@ -75,59 +112,42 @@ func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
// The return value is the attribute that was either modified in-place or
// created.
func (b *Body) SetAttributeTraversal(name string, traversal hcl.Traversal) *Attribute {
panic("Body.SetAttributeTraversal not yet implemented")
attr := b.GetAttribute(name)
expr := NewExpressionAbsTraversal(traversal)
if attr != nil {
attr.expr = attr.expr.ReplaceWith(expr)
} else {
attr := newAttribute()
attr.init(name, expr)
b.appendItem(attr)
}
return attr
}
type Attribute struct {
inTree
leadComments *node
name *node
expr *node
lineComments *node
// AppendBlock appends an existing block (which must not be already attached
// to a body) to the end of the receiving body.
func (b *Body) AppendBlock(block *Block) *Block {
b.appendItem(block)
return block
}
func newAttribute() *Attribute {
return &Attribute{
inTree: newInTree(),
}
// AppendNewBlock appends a new nested block to the end of the receiving body
// with the given type name and labels.
func (b *Body) AppendNewBlock(typeName string, labels []string) *Block {
block := newBlock()
block.init(typeName, labels)
b.appendItem(block)
return block
}
func (a *Attribute) init(name string, expr *Expression) {
expr.assertUnattached()
nameTok := newIdentToken(name)
nameObj := newIdentifier(nameTok)
a.leadComments = a.children.Append(newComments(nil))
a.name = a.children.Append(nameObj)
a.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
},
})
a.expr = a.children.Append(expr)
a.expr.list = a.children
a.lineComments = a.children.Append(newComments(nil))
a.children.AppendUnstructuredTokens(Tokens{
// AppendNewline appends a newline token to th end of the receiving body,
// which generally serves as a separator between different sets of body
// contents.
func (b *Body) AppendNewline() {
b.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
},
})
}
func (a *Attribute) Expr() *Expression {
return a.expr.content.(*Expression)
}
type Block struct {
inTree
leadComments *node
typeName *node
labels nodeSet
open *node
body *node
close *node
}

View File

@ -1,7 +1,10 @@
package hclwrite
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
@ -41,9 +44,125 @@ func NewExpressionLiteral(val cty.Value) *Expression {
// NewExpressionAbsTraversal constructs an expression that represents the
// given traversal, which must be absolute or this function will panic.
func NewExpressionAbsTraversal(traversal hcl.Traversal) *Expression {
panic("NewExpressionAbsTraversal not yet implemented")
if traversal.IsRelative() {
panic("can't construct expression from relative traversal")
}
physT := newTraversal()
rootName := traversal.RootName()
steps := traversal[1:]
{
tn := newTraverseName()
tn.name = tn.children.Append(newIdentifier(&Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(rootName),
}))
physT.steps.Add(physT.children.Append(tn))
}
for _, step := range steps {
switch ts := step.(type) {
case hcl.TraverseAttr:
tn := newTraverseName()
tn.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenDot,
Bytes: []byte{'.'},
},
})
tn.name = tn.children.Append(newIdentifier(&Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(ts.Name),
}))
physT.steps.Add(physT.children.Append(tn))
case hcl.TraverseIndex:
ti := newTraverseIndex()
ti.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenOBrack,
Bytes: []byte{'['},
},
})
indexExpr := NewExpressionLiteral(ts.Key)
ti.key = ti.children.Append(indexExpr)
ti.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenCBrack,
Bytes: []byte{']'},
},
})
physT.steps.Add(physT.children.Append(ti))
}
}
expr := newExpression()
expr.absTraversals.Add(expr.children.Append(physT))
return expr
}
// Variables returns the absolute traversals that exist within the receiving
// expression.
func (e *Expression) Variables() []*Traversal {
nodes := e.absTraversals.List()
ret := make([]*Traversal, len(nodes))
for i, node := range nodes {
ret[i] = node.content.(*Traversal)
}
return ret
}
// RenameVariablePrefix examines each of the absolute traversals in the
// receiving expression to see if they have the given sequence of names as
// a prefix prefix. If so, they are updated in place to have the given
// replacement names instead of that prefix.
//
// This can be used to implement symbol renaming. The calling application can
// visit all relevant expressions in its input and apply the same renaming
// to implement a global symbol rename.
//
// The search and replacement traversals must be the same length, or this
// method will panic. Only attribute access operations can be matched and
// replaced. Index steps never match the prefix.
func (e *Expression) RenameVariablePrefix(search, replacement []string) {
if len(search) != len(replacement) {
panic(fmt.Sprintf("search and replacement length mismatch (%d and %d)", len(search), len(replacement)))
}
Traversals:
for node := range e.absTraversals {
traversal := node.content.(*Traversal)
if len(traversal.steps) < len(search) {
// If it's shorter then it can't have our prefix
continue
}
stepNodes := traversal.steps.List()
for i, name := range search {
step, isName := stepNodes[i].content.(*TraverseName)
if !isName {
continue Traversals // only name nodes can match
}
foundNameBytes := step.name.content.(*identifier).token.Bytes
if len(foundNameBytes) != len(name) {
continue Traversals
}
if string(foundNameBytes) != name {
continue Traversals
}
}
// If we get here then the prefix matched, so now we'll swap in
// the replacement strings.
for i, name := range replacement {
step := stepNodes[i].content.(*TraverseName)
token := step.name.content.(*identifier).token
token.Bytes = []byte(name)
}
}
}
// Traversal represents a sequence of variable, attribute, and/or index
// operations.
type Traversal struct {
inTree

View File

@ -4,4 +4,8 @@
// It operates at a different level of abstraction than the main HCL parser
// and AST, since details such as the placement of comments and newlines
// are preserved when unchanged.
//
// The hclwrite API follows a similar principle to XML/HTML DOM, allowing nodes
// to be read out, created and inserted, etc. Nodes represent syntax constructs
// rather than semantic concepts.
package hclwrite

View File

@ -5,6 +5,7 @@ import (
"unicode"
"unicode/utf8"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
@ -25,6 +26,19 @@ func TokensForValue(val cty.Value) Tokens {
return toks
}
// TokensForTraversal returns a sequence of tokens that represents the given
// traversal.
//
// If the traversal is absolute then the result is a self-contained, valid
// reference expression. If the traversal is relative then the returned tokens
// could be appended to some other expression tokens to traverse into the
// represented expression.
func TokensForTraversal(traversal hcl.Traversal) Tokens {
toks := appendTokensForTraversal(traversal, nil)
format(toks) // fiddle with the SpacesBefore field to get canonical spacing
return toks
}
func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
switch {
@ -143,6 +157,47 @@ func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
return toks
}
func appendTokensForTraversal(traversal hcl.Traversal, toks Tokens) Tokens {
for _, step := range traversal {
appendTokensForTraversalStep(step, toks)
}
return toks
}
func appendTokensForTraversalStep(step hcl.Traverser, toks Tokens) {
switch ts := step.(type) {
case hcl.TraverseRoot:
toks = append(toks, &Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(ts.Name),
})
case hcl.TraverseAttr:
toks = append(
toks,
&Token{
Type: hclsyntax.TokenDot,
Bytes: []byte{'.'},
},
&Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(ts.Name),
},
)
case hcl.TraverseIndex:
toks = append(toks, &Token{
Type: hclsyntax.TokenOBrack,
Bytes: []byte{'['},
})
appendTokensForValue(ts.Key, toks)
toks = append(toks, &Token{
Type: hclsyntax.TokenCBrack,
Bytes: []byte{']'},
})
default:
panic(fmt.Sprintf("unsupported traversal step type %T", step))
}
}
func escapeQuotedStringLit(s string) []byte {
if len(s) == 0 {
return nil

View File

@ -104,6 +104,11 @@ func (ns *nodes) BuildTokens(to Tokens) Tokens {
return to
}
func (ns *nodes) Clear() {
ns.first = nil
ns.last = nil
}
func (ns *nodes) Append(c nodeContent) *node {
n := &node{
content: c,

View File

@ -372,6 +372,7 @@ func parseTraversal(nativeTraversal hcl.Traversal, from inputTokens) (before inp
before, step, after := parseTraversalStep(nativeStep, stepAfter)
children.AppendUnstructuredTokens(before.Tokens())
children.AppendNode(step)
traversal.steps.Add(step)
stepAfter = after
}

View File

@ -51,7 +51,7 @@ func (ts Tokens) Columns() int {
// WriteTo takes an io.Writer and writes the bytes for each token to it,
// along with the spacing that separates each token. In other words, this
// allows serializing the tokens to a file or other such byte stream.
func (ts Tokens) WriteTo(wr io.Writer) (int, error) {
func (ts Tokens) WriteTo(wr io.Writer) (int64, error) {
// We know we're going to be writing a lot of small chunks of repeated
// space characters, so we'll prepare a buffer of these that we can
// easily pass to wr.Write without any further allocation.
@ -60,7 +60,7 @@ func (ts Tokens) WriteTo(wr io.Writer) (int, error) {
spaces[i] = ' '
}
var n int
var n int64
var err error
for _, token := range ts {
if err != nil {
@ -74,7 +74,7 @@ func (ts Tokens) WriteTo(wr io.Writer) (int, error) {
}
var thisN int
thisN, err = wr.Write(spaces[:thisChunk])
n += thisN
n += int64(thisN)
if err != nil {
return n, err
}
@ -82,7 +82,7 @@ func (ts Tokens) WriteTo(wr io.Writer) (int, error) {
var thisN int
thisN, err = wr.Write(token.Bytes)
n += thisN
n += int64(thisN)
}
return n, err

2
vendor/modules.txt vendored
View File

@ -343,7 +343,7 @@ github.com/hashicorp/hcl/hcl/scanner
github.com/hashicorp/hcl/hcl/strconv
github.com/hashicorp/hcl/json/scanner
github.com/hashicorp/hcl/json/token
# github.com/hashicorp/hcl2 v0.0.0-20180925175540-3f1c5474d4f7
# github.com/hashicorp/hcl2 v0.0.0-20181126233546-67424e43b184
github.com/hashicorp/hcl2/hcl
github.com/hashicorp/hcl2/hcl/hclsyntax
github.com/hashicorp/hcl2/hcldec