govendor fetch github.com/hashicorp/hcl2/...

This is a general catchup of some developments in the HCL2 codebase, but
in particular includes:

- Recording expression and evalcontext as part of diagnostics, so that
  variable value information can be included alongside diagnostic
  snippets.

- hcldec supports decoding blocks into tuple and object values as well as
  list and map values, which then allows cty.DynamicPseudoType nested
  attributes to work properly.
pull/19851/head
Martin Atkins 8 years ago
parent aa20c4085a
commit 08344a4159

@ -43,19 +43,23 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc
if !eachVal.CanIterateElements() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid dynamic for_each value",
Detail: fmt.Sprintf("Cannot use a value of type %s in for_each. An iterable collection is required.", eachVal.Type()),
Subject: eachAttr.Expr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid dynamic for_each value",
Detail: fmt.Sprintf("Cannot use a value of type %s in for_each. An iterable collection is required.", eachVal.Type()),
Subject: eachAttr.Expr.Range().Ptr(),
Expression: eachAttr.Expr,
EvalContext: b.forEachCtx,
})
return nil, diags
}
if eachVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid dynamic for_each value",
Detail: "Cannot use a null value in for_each.",
Subject: eachAttr.Expr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid dynamic for_each value",
Detail: "Cannot use a null value in for_each.",
Subject: eachAttr.Expr.Range().Ptr(),
Expression: eachAttr.Expr,
EvalContext: b.forEachCtx,
})
return nil, diags
}
@ -159,28 +163,34 @@ func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, h
labelVal, convErr = convert.Convert(labelVal, cty.String)
if convErr != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
Subject: labelExpr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
Subject: labelExpr.Range().Ptr(),
Expression: labelExpr,
EvalContext: lCtx,
})
return nil, diags
}
if labelVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: "Cannot use a null value as a dynamic block label.",
Subject: labelExpr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: "Cannot use a null value as a dynamic block label.",
Subject: labelExpr.Range().Ptr(),
Expression: labelExpr,
EvalContext: lCtx,
})
return nil, diags
}
if !labelVal.IsKnown() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: "This value is not yet known. Dynamic block labels must be immediately-known values.",
Subject: labelExpr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid dynamic block label",
Detail: "This value is not yet known. Dynamic block labels must be immediately-known values.",
Subject: labelExpr.Range().Ptr(),
Expression: labelExpr,
EvalContext: lCtx,
})
return nil, diags
}

@ -26,14 +26,43 @@ const (
type Diagnostic struct {
Severity DiagnosticSeverity
// Summary and detail contain the English-language description of the
// Summary and Detail contain the English-language description of the
// problem. Summary is a terse description of the general problem and
// detail is a more elaborate, often-multi-sentence description of
// the probem and what might be done to solve it.
Summary string
Detail string
// Subject and Context are both source ranges relating to the diagnostic.
//
// Subject is a tight range referring to exactly the construct that
// is problematic, while Context is an optional broader range (which should
// fully contain Subject) that ought to be shown around Subject when
// generating isolated source-code snippets in diagnostic messages.
// If Context is nil, the Subject is also the Context.
//
// Some diagnostics have no source ranges at all. If Context is set then
// Subject should always also be set.
Subject *Range
Context *Range
// For diagnostics that occur when evaluating an expression, Expression
// may refer to that expression and EvalContext may point to the
// EvalContext that was active when evaluating it. This may allow for the
// inclusion of additional useful information when rendering a diagnostic
// message to the user.
//
// It is not always possible to select a single EvalContext for a
// diagnostic, and so in some cases this field may be nil even when an
// expression causes a problem.
//
// EvalContexts form a tree, so the given EvalContext may refer to a parent
// which in turn refers to another parent, etc. For a full picture of all
// of the active variables and functions the caller must walk up this
// chain, preferring definitions that are "closer" to the expression in
// case of colliding names.
Expression Expression
EvalContext *EvalContext
}
// Diagnostics is a list of Diagnostic instances.

@ -2,11 +2,14 @@ package hcl
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"sort"
wordwrap "github.com/mitchellh/go-wordwrap"
"github.com/zclconf/go-cty/cty"
)
type diagnosticTextWriter struct {
@ -133,6 +136,62 @@ func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
w.wr.Write([]byte{'\n'})
}
if diag.Expression != nil && diag.EvalContext != nil {
// We will attempt to render the values for any variables
// referenced in the given expression as additional context, for
// situations where the same expression is evaluated multiple
// times in different scopes.
expr := diag.Expression
ctx := diag.EvalContext
vars := expr.Variables()
stmts := make([]string, 0, len(vars))
seen := make(map[string]struct{}, len(vars))
for _, traversal := range vars {
val, diags := traversal.TraverseAbs(ctx)
if diags.HasErrors() {
// Skip anything that generates errors, since we probably
// already have the same error in our diagnostics set
// already.
continue
}
traversalStr := w.traversalStr(traversal)
if _, exists := seen[traversalStr]; exists {
continue // don't show duplicates when the same variable is referenced multiple times
}
switch {
case !val.IsKnown():
// Can't say anything about this yet, then.
continue
case val.IsNull():
stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr))
default:
stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val)))
}
seen[traversalStr] = struct{}{}
}
sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
last := len(stmts) - 1
for i, stmt := range stmts {
switch i {
case 0:
w.wr.Write([]byte{'w', 'i', 't', 'h', ' '})
default:
w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '})
}
w.wr.Write([]byte(stmt))
switch i {
case last:
w.wr.Write([]byte{'.', '\n', '\n'})
default:
w.wr.Write([]byte{',', '\n'})
}
}
}
}
if diag.Detail != "" {
@ -156,6 +215,90 @@ func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
return nil
}
func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string {
// This is a specialized subset of traversal rendering tailored to
// producing helpful contextual messages in diagnostics. It is not
// comprehensive nor intended to be used for other purposes.
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case TraverseRoot:
buf.WriteString(tStep.Name)
case TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case TraverseIndex:
buf.WriteByte('[')
if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
buf.WriteString(w.valueStr(tStep.Key))
} else {
// We'll just use a placeholder for more complex values,
// since otherwise our result could grow ridiculously long.
buf.WriteString("...")
}
buf.WriteByte(']')
}
}
return buf.String()
}
func (w *diagnosticTextWriter) valueStr(val cty.Value) string {
// This is a specialized subset of value rendering tailored to producing
// helpful but concise messages in diagnostics. It is not comprehensive
// nor intended to be used for other purposes.
ty := val.Type()
switch {
case val.IsNull():
return "null"
case !val.IsKnown():
// Should never happen here because we should filter before we get
// in here, but we'll do something reasonable rather than panic.
return "(not yet known)"
case ty == cty.Bool:
if val.True() {
return "true"
}
return "false"
case ty == cty.Number:
bf := val.AsBigFloat()
return bf.Text('g', 10)
case ty == cty.String:
// Go string syntax is not exactly the same as HCL native string syntax,
// but we'll accept the minor edge-cases where this is different here
// for now, just to get something reasonable here.
return fmt.Sprintf("%q", val.AsString())
case ty.IsCollectionType() || ty.IsTupleType():
l := val.LengthInt()
switch l {
case 0:
return "empty " + ty.FriendlyName()
case 1:
return ty.FriendlyName() + " with 1 element"
default:
return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
}
case ty.IsObjectType():
atys := ty.AttributeTypes()
l := len(atys)
switch l {
case 0:
return "object with no attributes"
case 1:
var name string
for k := range atys {
name = k
}
return fmt.Sprintf("object with 1 attribute %q", name)
default:
return fmt.Sprintf("object with %d attributes", l)
}
default:
return ty.FriendlyName()
}
}
func contextString(file *File, offset int) string {
type contextStringer interface {
ContextString(offset int) string

@ -0,0 +1,23 @@
package hclsyntax
import (
"github.com/hashicorp/hcl2/hcl"
)
// setDiagEvalContext is an internal helper that will impose a particular
// EvalContext on a set of diagnostics in-place, for any diagnostic that
// does not already have an EvalContext set.
//
// We generally expect diagnostics to be immutable, but this is safe to use
// on any Diagnostics where none of the contained Diagnostic objects have yet
// been seen by a caller. Its purpose is to apply additional context to a
// set of diagnostics produced by a "deeper" component as the stack unwinds
// during expression evaluation.
func setDiagEvalContext(diags hcl.Diagnostics, expr hcl.Expression, ctx *hcl.EvalContext) {
for _, diag := range diags {
if diag.Expression == nil {
diag.Expression = expr
diag.EvalContext = ctx
}
}
}

@ -105,7 +105,9 @@ func (e *ScopeTraversalExpr) walkChildNodes(w internalWalkFunc) {
}
func (e *ScopeTraversalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return e.Traversal.TraverseAbs(ctx)
val, diags := e.Traversal.TraverseAbs(ctx)
setDiagEvalContext(diags, e, ctx)
return val, diags
}
func (e *ScopeTraversalExpr) Range() hcl.Range {
@ -136,6 +138,7 @@ func (e *RelativeTraversalExpr) walkChildNodes(w internalWalkFunc) {
func (e *RelativeTraversalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
src, diags := e.Source.Value(ctx)
ret, travDiags := e.Traversal.TraverseRel(src)
setDiagEvalContext(travDiags, e, ctx)
diags = append(diags, travDiags...)
return ret, diags
}
@ -207,10 +210,12 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
if !hasNonNilMap {
return cty.DynamicVal, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Function calls not allowed",
Detail: "Functions may not be called here.",
Subject: e.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Function calls not allowed",
Detail: "Functions may not be called here.",
Subject: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
}
@ -226,11 +231,13 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
return cty.DynamicVal, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Call to unknown function",
Detail: fmt.Sprintf("There is no function named %q.%s", e.Name, suggestion),
Subject: &e.NameRange,
Context: e.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Call to unknown function",
Detail: fmt.Sprintf("There is no function named %q.%s", e.Name, suggestion),
Subject: &e.NameRange,
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
}
@ -255,11 +262,13 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
case expandVal.Type().IsTupleType() || expandVal.Type().IsListType() || expandVal.Type().IsSetType():
if expandVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must not be null.",
Context: expandExpr.Range().Ptr(),
Subject: e.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must not be null.",
Subject: expandExpr.Range().Ptr(),
Context: e.Range().Ptr(),
Expression: expandExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
@ -280,11 +289,13 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
args = newArgs
default:
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must be of a tuple, list, or set type.",
Context: expandExpr.Range().Ptr(),
Subject: e.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must be of a tuple, list, or set type.",
Subject: expandExpr.Range().Ptr(),
Context: e.Range().Ptr(),
Expression: expandExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
@ -304,8 +315,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Function %q expects%s %d argument(s). Missing value for %q.",
e.Name, qual, len(params), missing.Name,
),
Subject: &e.CloseParenRange,
Context: e.Range().Ptr(),
Subject: &e.CloseParenRange,
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
}
@ -319,8 +332,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Function %q expects only %d argument(s).",
e.Name, len(params),
),
Subject: args[len(params)].StartRange().Ptr(),
Context: e.Range().Ptr(),
Subject: args[len(params)].StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
}
@ -350,8 +365,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Invalid value for %q parameter: %s.",
param.Name, err,
),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: argExpr,
EvalContext: ctx,
})
}
@ -387,8 +404,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Invalid value for %q parameter: %s.",
param.Name, err,
),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: argExpr,
EvalContext: ctx,
})
default:
@ -399,8 +418,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Call to function %q failed: %s.",
e.Name, err,
),
Subject: e.StartRange().Ptr(),
Context: e.Range().Ptr(),
Subject: e.StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
})
}
@ -465,10 +486,12 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
// "These expressions are object and object respectively" if the
// object types don't exactly match.
"The true and false result expressions must have consistent types. The given expressions are %s and %s, respectively.",
trueResult.Type(), falseResult.Type(),
trueResult.Type().FriendlyName(), falseResult.Type().FriendlyName(),
),
Subject: hcl.RangeBetween(e.TrueResult.Range(), e.FalseResult.Range()).Ptr(),
Context: &e.SrcRange,
Subject: hcl.RangeBetween(e.TrueResult.Range(), e.FalseResult.Range()).Ptr(),
Context: &e.SrcRange,
Expression: e,
EvalContext: ctx,
},
}
}
@ -477,11 +500,13 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
diags = append(diags, condDiags...)
if condResult.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Null condition",
Detail: "The condition value is null. Conditions must either be true or false.",
Subject: e.Condition.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Null condition",
Detail: "The condition value is null. Conditions must either be true or false.",
Subject: e.Condition.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.Condition,
EvalContext: ctx,
})
return cty.UnknownVal(resultType), diags
}
@ -491,11 +516,13 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
condResult, err := convert.Convert(condResult, cty.Bool)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Incorrect condition type",
Detail: fmt.Sprintf("The condition expression must be of type bool."),
Subject: e.Condition.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Incorrect condition type",
Detail: fmt.Sprintf("The condition expression must be of type bool."),
Subject: e.Condition.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.Condition,
EvalContext: ctx,
})
return cty.UnknownVal(resultType), diags
}
@ -514,8 +541,10 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
"The true result value has the wrong type: %s.",
err.Error(),
),
Subject: e.TrueResult.Range().Ptr(),
Context: &e.SrcRange,
Subject: e.TrueResult.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.TrueResult,
EvalContext: ctx,
})
trueResult = cty.UnknownVal(resultType)
}
@ -535,8 +564,10 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
"The false result value has the wrong type: %s.",
err.Error(),
),
Subject: e.TrueResult.Range().Ptr(),
Context: &e.SrcRange,
Subject: e.FalseResult.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.FalseResult,
EvalContext: ctx,
})
falseResult = cty.UnknownVal(resultType)
}
@ -573,7 +604,9 @@ func (e *IndexExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
diags = append(diags, collDiags...)
diags = append(diags, keyDiags...)
return hcl.Index(coll, key, &e.SrcRange)
val, diags := hcl.Index(coll, key, &e.SrcRange)
setDiagEvalContext(diags, e, ctx)
return val, diags
}
func (e *IndexExpr) Range() hcl.Range {
@ -676,10 +709,12 @@ func (e *ObjectConsExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics
if key.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Null value as key",
Detail: "Can't use a null value as a key.",
Subject: item.ValueExpr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Null value as key",
Detail: "Can't use a null value as a key.",
Subject: item.ValueExpr.Range().Ptr(),
Expression: item.KeyExpr,
EvalContext: ctx,
})
known = false
continue
@ -689,10 +724,12 @@ func (e *ObjectConsExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics
key, err = convert.Convert(key, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Incorrect key type",
Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()),
Subject: item.ValueExpr.Range().Ptr(),
Severity: hcl.DiagError,
Summary: "Incorrect key type",
Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()),
Subject: item.ValueExpr.Range().Ptr(),
Expression: item.ValueExpr,
EvalContext: ctx,
})
known = false
continue
@ -819,11 +856,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if collVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Iteration over null value",
Detail: "A null value cannot be used as the collection in a 'for' expression.",
Subject: e.CollExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Iteration over null value",
Detail: "A null value cannot be used as the collection in a 'for' expression.",
Subject: e.CollExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CollExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
@ -838,8 +877,10 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
"A value of type %s cannot be used as the collection in a 'for' expression.",
collVal.Type().FriendlyName(),
),
Subject: e.CollExpr.Range().Ptr(),
Context: &e.SrcRange,
Subject: e.CollExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CollExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
@ -847,14 +888,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.DynamicVal, diags
}
childCtx := ctx.NewChild()
childCtx.Variables = map[string]cty.Value{}
// Before we start we'll do an early check to see if any CondExpr we've
// been given is of the wrong type. This isn't 100% reliable (it may
// be DynamicVal until real values are given) but it should catch some
// straightforward cases and prevent a barrage of repeated errors.
if e.CondExpr != nil {
childCtx := ctx.NewChild()
childCtx.Variables = map[string]cty.Value{}
if e.KeyVar != "" {
childCtx.Variables[e.KeyVar] = cty.DynamicVal
}
@ -864,22 +904,26 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
diags = append(diags, condDiags...)
if result.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Condition is null",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Condition is null",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
_, err := convert.Convert(result, cty.Bool)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}
@ -903,6 +947,8 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
known := true
for it.Next() {
k, v := it.Element()
childCtx := ctx.NewChild()
childCtx.Variables = map[string]cty.Value{}
if e.KeyVar != "" {
childCtx.Variables[e.KeyVar] = k
}
@ -914,11 +960,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if includeRaw.IsNull() {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Condition is null",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: childCtx,
})
}
known = false
@ -928,11 +976,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if err != nil {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: childCtx,
})
}
known = false
@ -954,11 +1004,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if keyRaw.IsNull() {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid object key",
Detail: "Key expression in 'for' expression must not produce a null value.",
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid object key",
Detail: "Key expression in 'for' expression must not produce a null value.",
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.KeyExpr,
EvalContext: childCtx,
})
}
known = false
@ -973,11 +1025,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if err != nil {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid object key",
Detail: fmt.Sprintf("The key expression produced an invalid result: %s.", err.Error()),
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid object key",
Detail: fmt.Sprintf("The key expression produced an invalid result: %s.", err.Error()),
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.KeyExpr,
EvalContext: childCtx,
})
}
known = false
@ -997,11 +1051,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
Severity: hcl.DiagError,
Summary: "Duplicate object key",
Detail: fmt.Sprintf(
"Two different items produced the key %q in this for expression. If duplicates are expected, use the ellipsis (...) after the value expression to enable grouping by key.",
"Two different items produced the key %q in this 'for' expression. If duplicates are expected, use the ellipsis (...) after the value expression to enable grouping by key.",
k,
),
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Subject: e.KeyExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.KeyExpr,
EvalContext: childCtx,
})
} else {
vals[key.AsString()] = val
@ -1031,6 +1087,8 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
known := true
for it.Next() {
k, v := it.Element()
childCtx := ctx.NewChild()
childCtx.Variables = map[string]cty.Value{}
if e.KeyVar != "" {
childCtx.Variables[e.KeyVar] = k
}
@ -1042,11 +1100,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if includeRaw.IsNull() {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Condition is null",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: "The value of the 'if' clause must not be null.",
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: childCtx,
})
}
known = false
@ -1064,11 +1124,13 @@ func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if err != nil {
if known {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid 'for' condition",
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
Subject: e.CondExpr.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.CondExpr,
EvalContext: childCtx,
})
}
known = false
@ -1154,11 +1216,13 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if sourceVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Splat of null value",
Detail: "Splat expressions (with the * symbol) cannot be applied to null values.",
Subject: e.Source.Range().Ptr(),
Context: hcl.RangeBetween(e.Source.Range(), e.MarkerRange).Ptr(),
Severity: hcl.DiagError,
Summary: "Splat of null value",
Detail: "Splat expressions (with the * symbol) cannot be applied to null values.",
Subject: e.Source.Range().Ptr(),
Context: hcl.RangeBetween(e.Source.Range(), e.MarkerRange).Ptr(),
Expression: e.Source,
EvalContext: ctx,
})
return cty.DynamicVal, diags
}

@ -149,21 +149,25 @@ func (e *BinaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
lhsVal, err := convert.Convert(givenLHSVal, lhsParam.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for left operand: %s.", err),
Subject: e.LHS.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for left operand: %s.", err),
Subject: e.LHS.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.LHS,
EvalContext: ctx,
})
}
rhsVal, err := convert.Convert(givenRHSVal, rhsParam.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for right operand: %s.", err),
Subject: e.RHS.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for right operand: %s.", err),
Subject: e.RHS.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.RHS,
EvalContext: ctx,
})
}
@ -178,10 +182,12 @@ func (e *BinaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
// FIXME: This diagnostic is useless.
Severity: hcl.DiagError,
Summary: "Operation failed",
Detail: fmt.Sprintf("Error during operation: %s.", err),
Subject: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Operation failed",
Detail: fmt.Sprintf("Error during operation: %s.", err),
Subject: &e.SrcRange,
Expression: e,
EvalContext: ctx,
})
return cty.UnknownVal(e.Op.Type), diags
}
@ -219,11 +225,13 @@ func (e *UnaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
val, err := convert.Convert(givenVal, param.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for unary operand: %s.", err),
Subject: e.Val.Range().Ptr(),
Context: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Invalid operand",
Detail: fmt.Sprintf("Unsuitable value for unary operand: %s.", err),
Subject: e.Val.Range().Ptr(),
Context: &e.SrcRange,
Expression: e.Val,
EvalContext: ctx,
})
}
@ -238,10 +246,12 @@ func (e *UnaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if err != nil {
diags = append(diags, &hcl.Diagnostic{
// FIXME: This diagnostic is useless.
Severity: hcl.DiagError,
Summary: "Operation failed",
Detail: fmt.Sprintf("Error during operation: %s.", err),
Subject: &e.SrcRange,
Severity: hcl.DiagError,
Summary: "Operation failed",
Detail: fmt.Sprintf("Error during operation: %s.", err),
Subject: &e.SrcRange,
Expression: e,
EvalContext: ctx,
})
return cty.UnknownVal(e.Op.Type), diags
}

@ -37,8 +37,10 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
Detail: fmt.Sprintf(
"The expression result is null. Cannot include a null value in a string template.",
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Expression: part,
EvalContext: ctx,
})
continue
}
@ -61,8 +63,10 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
"Cannot include the given value in a string template: %s.",
err.Error(),
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Expression: part,
EvalContext: ctx,
})
continue
}
@ -127,7 +131,9 @@ func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
Detail: fmt.Sprintf(
"An iteration result is null. Cannot include a null value in a string template.",
),
Subject: e.Range().Ptr(),
Subject: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
})
continue
}
@ -143,7 +149,9 @@ func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
"Cannot include one of the interpolation results into the string template: %s.",
err.Error(),
),
Subject: e.Range().Ptr(),
Subject: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
})
continue
}

@ -9,6 +9,10 @@ import (
// 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]
@ -326,6 +330,9 @@ func (a *Attribute) Range() hcl.Range {
// 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,

@ -0,0 +1,118 @@
package hclsyntax
import (
"github.com/hashicorp/hcl2/hcl"
)
// -----------------------------------------------------------------------------
// The methods in this file are all optional extension methods that serve to
// implement the methods of the same name on *hcl.File when its root body
// is provided by this package.
// -----------------------------------------------------------------------------
// BlocksAtPos implements the method of the same name for an *hcl.File that
// is backed by a *Body.
func (b *Body) BlocksAtPos(pos hcl.Pos) []*hcl.Block {
list, _ := b.blocksAtPos(pos, true)
return list
}
// InnermostBlockAtPos implements the method of the same name for an *hcl.File
// that is backed by a *Body.
func (b *Body) InnermostBlockAtPos(pos hcl.Pos) *hcl.Block {
_, innermost := b.blocksAtPos(pos, false)
return innermost.AsHCLBlock()
}
// OutermostBlockAtPos implements the method of the same name for an *hcl.File
// that is backed by a *Body.
func (b *Body) OutermostBlockAtPos(pos hcl.Pos) *hcl.Block {
return b.outermostBlockAtPos(pos).AsHCLBlock()
}
// blocksAtPos is the internal engine of both BlocksAtPos and
// InnermostBlockAtPos, which both need to do the same logic but return a
// differently-shaped result.
//
// list is nil if makeList is false, avoiding an allocation. Innermost is
// always set, and if the returned list is non-nil it will always match the
// final element from that list.
func (b *Body) blocksAtPos(pos hcl.Pos, makeList bool) (list []*hcl.Block, innermost *Block) {
current := b
Blocks:
for current != nil {
for _, block := range current.Blocks {
wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange)
if wholeRange.ContainsPos(pos) {
innermost = block
if makeList {
list = append(list, innermost.AsHCLBlock())
}
current = block.Body
continue Blocks
}
}
// If we fall out here then none of the current body's nested blocks
// contain the position we are looking for, and so we're done.
break
}
return
}
// outermostBlockAtPos is the internal version of OutermostBlockAtPos that
// returns a hclsyntax.Block rather than an hcl.Block, allowing for further
// analysis if necessary.
func (b *Body) outermostBlockAtPos(pos hcl.Pos) *Block {
// This is similar to blocksAtPos, but simpler because we know it only
// ever needs to search the first level of nested blocks.
for _, block := range b.Blocks {
wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange)
if wholeRange.ContainsPos(pos) {
return block
}
}
return nil
}
// AttributeAtPos implements the method of the same name for an *hcl.File
// that is backed by a *Body.
func (b *Body) AttributeAtPos(pos hcl.Pos) *hcl.Attribute {
return b.attributeAtPos(pos).AsHCLAttribute()
}
// attributeAtPos is the internal version of AttributeAtPos that returns a
// hclsyntax.Block rather than an hcl.Block, allowing for further analysis if
// necessary.
func (b *Body) attributeAtPos(pos hcl.Pos) *Attribute {
searchBody := b
_, block := b.blocksAtPos(pos, false)
if block != nil {
searchBody = block.Body
}
for _, attr := range searchBody.Attributes {
if attr.SrcRange.ContainsPos(pos) {
return attr
}
}
return nil
}
// OutermostExprAtPos implements the method of the same name for an *hcl.File
// that is backed by a *Body.
func (b *Body) OutermostExprAtPos(pos hcl.Pos) hcl.Expression {
attr := b.attributeAtPos(pos)
if attr == nil {
return nil
}
if !attr.Expr.Range().ContainsPos(pos) {
return nil
}
return attr.Expr
}

@ -432,7 +432,8 @@ func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
Value: jsonAttr.Name,
SrcRange: jsonAttr.NameRange,
}}).Value(ctx)
val, valDiags := (&expression{src: jsonAttr.Value}).Value(ctx)
valExpr := &expression{src: jsonAttr.Value}
val, valDiags := valExpr.Value(ctx)
diags = append(diags, nameDiags...)
diags = append(diags, valDiags...)
@ -440,19 +441,23 @@ func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
name, err = convert.Convert(name, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid object key expression",
Detail: fmt.Sprintf("Cannot use this expression as an object key: %s.", err),
Subject: &jsonAttr.NameRange,
Severity: hcl.DiagError,
Summary: "Invalid object key expression",
Detail: fmt.Sprintf("Cannot use this expression as an object key: %s.", err),
Subject: &jsonAttr.NameRange,
Expression: valExpr,
EvalContext: ctx,
})
continue
}
if name.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid object key expression",
Detail: "Cannot use null value as an object key.",
Subject: &jsonAttr.NameRange,
Severity: hcl.DiagError,
Summary: "Invalid object key expression",
Detail: "Cannot use null value as an object key.",
Subject: &jsonAttr.NameRange,
Expression: valExpr,
EvalContext: ctx,
})
continue
}
@ -471,10 +476,12 @@ func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
nameStr := name.AsString()
if _, defined := attrs[nameStr]; defined {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate object attribute",
Detail: fmt.Sprintf("An attribute named %q was already defined at %s.", nameStr, attrRanges[nameStr]),
Subject: &jsonAttr.NameRange,
Severity: hcl.DiagError,
Summary: "Duplicate object attribute",
Detail: fmt.Sprintf("An attribute named %q was already defined at %s.", nameStr, attrRanges[nameStr]),
Subject: &jsonAttr.NameRange,
Expression: e,
EvalContext: ctx,
})
continue
}

@ -94,6 +94,16 @@ func RangeOver(a, b Range) Range {
}
}
// ContainsPos returns true if and only if the given position is contained within
// the receiving range.
//
// In the unlikely case that the line/column information disagree with the byte
// offset information in the given position or receiving range, the byte
// offsets are given priority.
func (r Range) ContainsPos(pos Pos) bool {
return r.ContainsOffset(pos.Byte)
}
// ContainsOffset returns true if and only if the given byte offset is within
// the receiving Range.
func (r Range) ContainsOffset(offset int) bool {

@ -0,0 +1,117 @@
package hcl
// -----------------------------------------------------------------------------
// The methods in this file all have the general pattern of making a best-effort
// to find one or more constructs that contain a given source position.
//
// These all operate by delegating to an optional method of the same name and
// signature on the file's root body, allowing each syntax to potentially
// provide its own implementations of these. For syntaxes that don't implement
// them, the result is always nil.
// -----------------------------------------------------------------------------
// BlocksAtPos attempts to find all of the blocks that contain the given
// position, ordered so that the outermost block is first and the innermost
// block is last. This is a best-effort method that may not be able to produce
// a complete result for all positions or for all HCL syntaxes.
//
// If the returned slice is non-empty, the first element is guaranteed to
// represent the same block as would be the result of OutermostBlockAtPos and
// the last element the result of InnermostBlockAtPos. However, the
// implementation may return two different objects describing the same block,
// so comparison by pointer identity is not possible.
//
// The result is nil if no blocks at all contain the given position.
func (f *File) BlocksAtPos(pos Pos) []*Block {
// The root body of the file must implement this interface in order
// to support BlocksAtPos.
type Interface interface {
BlocksAtPos(pos Pos) []*Block
}
impl, ok := f.Body.(Interface)
if !ok {
return nil
}
return impl.BlocksAtPos(pos)
}
// OutermostBlockAtPos attempts to find a top-level block in the receiving file
// that contains the given position. This is a best-effort method that may not
// be able to produce a result for all positions or for all HCL syntaxes.
//
// The result is nil if no single block could be selected for any reason.
func (f *File) OutermostBlockAtPos(pos Pos) *Block {
// The root body of the file must implement this interface in order
// to support OutermostBlockAtPos.
type Interface interface {
OutermostBlockAtPos(pos Pos) *Block
}
impl, ok := f.Body.(Interface)
if !ok {
return nil
}
return impl.OutermostBlockAtPos(pos)
}
// InnermostBlockAtPos attempts to find the most deeply-nested block in the
// receiving file that contains the given position. This is a best-effort
// method that may not be able to produce a result for all positions or for
// all HCL syntaxes.
//
// The result is nil if no single block could be selected for any reason.
func (f *File) InnermostBlockAtPos(pos Pos) *Block {
// The root body of the file must implement this interface in order
// to support InnermostBlockAtPos.
type Interface interface {
InnermostBlockAtPos(pos Pos) *Block
}
impl, ok := f.Body.(Interface)
if !ok {
return nil
}
return impl.InnermostBlockAtPos(pos)
}
// OutermostExprAtPos attempts to find an expression in the receiving file
// that contains the given position. This is a best-effort method that may not
// be able to produce a result for all positions or for all HCL syntaxes.
//
// Since expressions are often nested inside one another, this method returns
// the outermost "root" expression that is not contained by any other.
//
// The result is nil if no single expression could be selected for any reason.
func (f *File) OutermostExprAtPos(pos Pos) Expression {
// The root body of the file must implement this interface in order
// to support OutermostExprAtPos.
type Interface interface {
OutermostExprAtPos(pos Pos) Expression
}
impl, ok := f.Body.(Interface)
if !ok {
return nil
}
return impl.OutermostExprAtPos(pos)
}
// AttributeAtPos attempts to find an attribute definition in the receiving
// file that contains the given position. This is a best-effort method that may
// not be able to produce a result for all positions or for all HCL syntaxes.
//
// The result is nil if no single attribute could be selected for any reason.
func (f *File) AttributeAtPos(pos Pos) *Attribute {
// The root body of the file must implement this interface in order
// to support OutermostExprAtPos.
type Interface interface {
AttributeAtPos(pos Pos) *Attribute
}
impl, ok := f.Body.(Interface)
if !ok {
return nil
}
return impl.AttributeAtPos(pos)
}

@ -3,6 +3,7 @@ package hcldec
import (
"bytes"
"fmt"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
@ -477,6 +478,44 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe
if len(elems) == 0 {
ret = cty.ListValEmpty(s.Nested.impliedType())
} else {
// Since our target is a list, all of the decoded elements must have the
// same type or cty.ListVal will panic below. Different types can arise
// if there is an attribute spec of type cty.DynamicPseudoType in the
// nested spec; all given values must be convertable to a single type
// in order for the result to be considered valid.
etys := make([]cty.Type, len(elems))
for i, v := range elems {
etys[i] = v.Type()
}
ety, convs := convert.UnifyUnsafe(etys)
if ety == cty.NilType {
// FIXME: This is a pretty terrible error message.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
Detail: "Corresponding attributes in all blocks of this type must be the same.",
Subject: &sourceRanges[0],
})
return cty.DynamicVal, diags
}
for i, v := range elems {
if convs[i] != nil {
newV, err := convs[i](v)
if err != nil {
// FIXME: This is a pretty terrible error message.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err),
Subject: &sourceRanges[i],
})
// Bail early here so we won't panic below in cty.ListVal
return cty.DynamicVal, diags
}
elems[i] = newV
}
}
ret = cty.ListVal(elems)
}
@ -508,6 +547,127 @@ func (s *BlockListSpec) sourceRange(content *hcl.BodyContent, blockLabels []bloc
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockTupleSpec is a Spec that produces a cty tuple of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
//
// This is similar to BlockListSpec, but it permits the nested blocks to have
// different result types in situations where cty.DynamicPseudoType attributes
// are present.
type BlockTupleSpec struct {
TypeName string
Nested Spec
MinItems int
MaxItems int
}
func (s *BlockTupleSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node ("Nested" does not use the same body)
}
// blockSpec implementation
func (s *BlockTupleSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: findLabelSpecs(s.Nested),
},
}
}
// blockSpec implementation
func (s *BlockTupleSpec) nestedSpec() Spec {
return s.Nested
}
// specNeedingVariables implementation
func (s *BlockTupleSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
var ret []hcl.Traversal
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
ret = append(ret, Variables(childBlock.Body, s.Nested)...)
}
return ret
}
func (s *BlockTupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockListSpec with no Nested Spec")
}
var elems []cty.Value
var sourceRanges []hcl.Range
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false)
diags = append(diags, childDiags...)
elems = append(elems, val)
sourceRanges = append(sourceRanges, sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested))
}
if len(elems) < s.MinItems {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Insufficient %s blocks", s.TypeName),
Detail: fmt.Sprintf("At least %d %q blocks are required.", s.MinItems, s.TypeName),
Subject: &content.MissingItemRange,
})
} else if s.MaxItems > 0 && len(elems) > s.MaxItems {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Too many %s blocks", s.TypeName),
Detail: fmt.Sprintf("No more than %d %q blocks are allowed", s.MaxItems, s.TypeName),
Subject: &sourceRanges[s.MaxItems],
})
}
var ret cty.Value
if len(elems) == 0 {
ret = cty.EmptyTupleVal
} else {
ret = cty.TupleVal(elems)
}
return ret, diags
}
func (s *BlockTupleSpec) impliedType() cty.Type {
// We can't predict our type, because we don't know how many blocks
// there will be until we decode.
return cty.DynamicPseudoType
}
func (s *BlockTupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range.
var childBlock *hcl.Block
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
childBlock = candidate
break
}
if childBlock == nil {
return content.MissingItemRange
}
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockSetSpec is a Spec that produces a cty set of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
type BlockSetSpec struct {
@ -592,6 +752,44 @@ func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
if len(elems) == 0 {
ret = cty.SetValEmpty(s.Nested.impliedType())
} else {
// Since our target is a set, all of the decoded elements must have the
// same type or cty.SetVal will panic below. Different types can arise
// if there is an attribute spec of type cty.DynamicPseudoType in the
// nested spec; all given values must be convertable to a single type
// in order for the result to be considered valid.
etys := make([]cty.Type, len(elems))
for i, v := range elems {
etys[i] = v.Type()
}
ety, convs := convert.UnifyUnsafe(etys)
if ety == cty.NilType {
// FIXME: This is a pretty terrible error message.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
Detail: "Corresponding attributes in all blocks of this type must be the same.",
Subject: &sourceRanges[0],
})
return cty.DynamicVal, diags
}
for i, v := range elems {
if convs[i] != nil {
newV, err := convs[i](v)
if err != nil {
// FIXME: This is a pretty terrible error message.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err),
Subject: &sourceRanges[i],
})
// Bail early here so we won't panic below in cty.ListVal
return cty.DynamicVal, diags
}
elems[i] = newV
}
}
ret = cty.SetVal(elems)
}
@ -672,7 +870,10 @@ func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockSetSpec with no Nested Spec")
panic("BlockMapSpec with no Nested Spec")
}
if ImpliedType(s).HasDynamicTypes() {
panic("cty.DynamicPseudoType attributes may not be used inside a BlockMapSpec")
}
elems := map[string]interface{}{}
@ -765,6 +966,307 @@ func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []block
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockObjectSpec is a Spec that produces a cty object of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
//
// One level of object structure is created for each of the given label names.
// There must be at least one given label name.
//
// This is similar to BlockMapSpec, but it permits the nested blocks to have
// different result types in situations where cty.DynamicPseudoType attributes
// are present.
type BlockObjectSpec struct {
TypeName string
LabelNames []string
Nested Spec
}
func (s *BlockObjectSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node ("Nested" does not use the same body)
}
// blockSpec implementation
func (s *BlockObjectSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: append(s.LabelNames, findLabelSpecs(s.Nested)...),
},
}
}
// blockSpec implementation
func (s *BlockObjectSpec) nestedSpec() Spec {
return s.Nested
}
// specNeedingVariables implementation
func (s *BlockObjectSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
var ret []hcl.Traversal
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
ret = append(ret, Variables(childBlock.Body, s.Nested)...)
}
return ret
}
func (s *BlockObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockObjectSpec with no Nested Spec")
}
elems := map[string]interface{}{}
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
childLabels := labelsForBlock(childBlock)
val, _, childDiags := decode(childBlock.Body, childLabels[len(s.LabelNames):], ctx, s.Nested, false)
targetMap := elems
for _, key := range childBlock.Labels[:len(s.LabelNames)-1] {
if _, exists := targetMap[key]; !exists {
targetMap[key] = make(map[string]interface{})
}
targetMap = targetMap[key].(map[string]interface{})
}
diags = append(diags, childDiags...)
key := childBlock.Labels[len(s.LabelNames)-1]
if _, exists := targetMap[key]; exists {
labelsBuf := bytes.Buffer{}
for _, label := range childBlock.Labels {
fmt.Fprintf(&labelsBuf, " %q", label)
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %s block", s.TypeName),
Detail: fmt.Sprintf(
"A block for %s%s was already defined. The %s labels must be unique.",
s.TypeName, labelsBuf.String(), s.TypeName,
),
Subject: &childBlock.DefRange,
})
continue
}
targetMap[key] = val
}
if len(elems) == 0 {
return cty.EmptyObjectVal, diags
}
var ctyObj func(map[string]interface{}, int) cty.Value
ctyObj = func(raw map[string]interface{}, depth int) cty.Value {
vals := make(map[string]cty.Value, len(raw))
if depth == 1 {
for k, v := range raw {
vals[k] = v.(cty.Value)
}
} else {
for k, v := range raw {
vals[k] = ctyObj(v.(map[string]interface{}), depth-1)
}
}
return cty.ObjectVal(vals)
}
return ctyObj(elems, len(s.LabelNames)), diags
}
func (s *BlockObjectSpec) impliedType() cty.Type {
// We can't predict our type, since we don't know how many blocks are
// present and what labels they have until we decode.
return cty.DynamicPseudoType
}
func (s *BlockObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range.
var childBlock *hcl.Block
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
childBlock = candidate
break
}
if childBlock == nil {
return content.MissingItemRange
}
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockAttrsSpec is a Spec that interprets a single block as if it were
// a map of some element type. That is, each attribute within the block
// becomes a key in the resulting map and the attribute's value becomes the
// element value, after conversion to the given element type. The resulting
// value is a cty.Map of the given element type.
//
// This spec imposes a validation constraint that there be exactly one block
// of the given type name and that this block may contain only attributes. The
// block does not accept any labels.
//
// This is an alternative to an AttrSpec of a map type for situations where
// block syntax is desired. Note that block syntax does not permit dynamic
// keys, construction of the result via a "for" expression, etc. In most cases
// an AttrSpec is preferred if the desired result is a map whose keys are
// chosen by the user rather than by schema.
type BlockAttrsSpec struct {
TypeName string
ElementType cty.Type
Required bool
}
func (s *BlockAttrsSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node
}
// blockSpec implementation
func (s *BlockAttrsSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: nil,
},
}
}
// blockSpec implementation
func (s *BlockAttrsSpec) nestedSpec() Spec {
// This is an odd case: we aren't actually going to apply a nested spec
// in this case, since we're going to interpret the body directly as
// attributes, but we need to return something non-nil so that the
// decoder will recognize this as a block spec. We won't actually be
// using this for anything at decode time.
return noopSpec{}
}
// specNeedingVariables implementation
func (s *BlockAttrsSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
block, _ := s.findBlock(content)
if block == nil {
return nil
}
var vars []hcl.Traversal
attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
return nil
}
for _, attr := range attrs {
vars = append(vars, attr.Expr.Variables()...)
}
// We'll return the variables references in source order so that any
// error messages that result are also in source order.
sort.Slice(vars, func(i, j int) bool {
return vars[i].SourceRange().Start.Byte < vars[j].SourceRange().Start.Byte
})
return vars
}
func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
block, other := s.findBlock(content)
if block == nil {
if s.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Missing %s block", s.TypeName),
Detail: fmt.Sprintf(
"A block of type %q is required here.", s.TypeName,
),
Subject: &content.MissingItemRange,
})
}
return cty.NullVal(cty.Map(s.ElementType)), diags
}
if other != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %s block", s.TypeName),
Detail: fmt.Sprintf(
"Only one block of type %q is allowed. Previous definition was at %s.",
s.TypeName, block.DefRange.String(),
),
Subject: &other.DefRange,
})
}
attrs, attrDiags := block.Body.JustAttributes()
diags = append(diags, attrDiags...)
if len(attrs) == 0 {
return cty.MapValEmpty(s.ElementType), diags
}
vals := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
attrVal, attrDiags := attr.Expr.Value(ctx)
diags = append(diags, attrDiags...)
attrVal, err := convert.Convert(attrVal, s.ElementType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid attribute value",
Detail: fmt.Sprintf("Invalid value for attribute of %q block: %s.", s.TypeName, err),
Subject: attr.Expr.Range().Ptr(),
})
attrVal = cty.UnknownVal(s.ElementType)
}
vals[name] = attrVal
}
return cty.MapVal(vals), diags
}
func (s *BlockAttrsSpec) impliedType() cty.Type {
return cty.Map(s.ElementType)
}
func (s *BlockAttrsSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
block, _ := s.findBlock(content)
if block == nil {
return content.MissingItemRange
}
return block.DefRange
}
func (s *BlockAttrsSpec) findBlock(content *hcl.BodyContent) (block *hcl.Block, other *hcl.Block) {
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
if block != nil {
return block, candidate
}
block = candidate
}
return block, nil
}
// A BlockLabelSpec is a Spec that returns a cty.String representing the
// label of the block its given body belongs to, if indeed its given body
// belongs to a block. It is a programming error to use this in a non-block
@ -1038,3 +1540,28 @@ func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []
// not super-accurate, because there's nothing better to return.
return s.Wrapped.sourceRange(content, blockLabels)
}
// noopSpec is a placeholder spec that does nothing, used in situations where
// a non-nil placeholder spec is required. It is not exported because there is
// no reason to use it directly; it is always an implementation detail only.
type noopSpec struct {
}
func (s noopSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.NullVal(cty.DynamicPseudoType), nil
}
func (s noopSpec) impliedType() cty.Type {
return cty.DynamicPseudoType
}
func (s noopSpec) visitSameBodyChildren(cb visitFunc) {
// nothing to do
}
func (s noopSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// No useful range for a noopSpec, and nobody should be calling this anyway.
return hcl.Range{
Filename: "noopSpec",
}
}

@ -3,29 +3,25 @@ package hclwrite
import (
"bytes"
"io"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
)
type Node interface {
walkChildNodes(w internalWalkFunc)
Tokens() *TokenSeq
}
type internalWalkFunc func(Node)
type File struct {
Name string
SrcBytes []byte
inTree
Body *Body
AllTokens *TokenSeq
srcBytes []byte
body *node
}
// Body returns the root body of the file, which contains the top-level
// attributes and blocks.
func (f *File) Body() *Body {
return f.body.content.(*Body)
}
// WriteTo writes the tokens underlying the receiving file to the given writer.
func (f *File) WriteTo(wr io.Writer) (int, error) {
return f.AllTokens.WriteTo(wr)
tokens := f.inTree.children.BuildTokens(nil)
return tokens.WriteTo(wr)
}
// Bytes returns a buffer containing the source code resulting from the
@ -37,169 +33,74 @@ func (f *File) Bytes() []byte {
return buf.Bytes()
}
// Format makes in-place modifications to the tokens underlying the receiving
// file in order to change the whitespace to be in canonical form.
func (f *File) Format() {
format(f.Body.AllTokens.Tokens())
}
type Body struct {
// Items may contain Attribute, Block and Unstructured instances.
// Items and AllTokens should be updated only by methods of this type,
// since they must be kept synchronized for correct operation.
Items []Node
AllTokens *TokenSeq
// IndentLevel is the number of spaces that should appear at the start
// of lines added within this body.
IndentLevel int
}
func (n *Body) walkChildNodes(w internalWalkFunc) {
for _, item := range n.Items {
w(item)
}
}
func (n *Body) Tokens() *TokenSeq {
return n.AllTokens
}
type comments struct {
leafNode
func (n *Body) AppendItem(node Node) {
n.Items = append(n.Items, node)
n.AppendUnstructuredTokens(node.Tokens())
parent *node
tokens Tokens
}
func (n *Body) AppendUnstructuredTokens(seq *TokenSeq) {
if n.AllTokens == nil {
new := make(TokenSeq, 0, 1)
n.AllTokens = &new
func newComments(tokens Tokens) *comments {
return &comments{
tokens: tokens,
}
*(n.AllTokens) = append(*(n.AllTokens), seq)
}
// FindAttribute returns the first attribute item from the body that has the
// given name, or returns nil if there is currently no matching attribute.
//
// A valid AST has only one definition of each attribute, but that constraint
// is not enforced in the hclwrite AST, so a tree that has been mutated by
// other calls may contain additional matching attributes that cannot be seen
// by this method.
func (n *Body) FindAttribute(name string) *Attribute {
nameBytes := []byte(name)
for _, item := range n.Items {
if attr, ok := item.(*Attribute); ok {
if attr.NameTokens.IsIdent(nameBytes) {
return attr
}
}
}
return nil
}
// SetAttributeValue either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block.
//
// The value is given as a cty.Value, and must therefore be a literal. To set
// a variable reference or other traversal, use SetAttributeTraversal.
//
// The return value is the attribute that was either modified in-place or
// created.
func (n *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
panic("Body.SetAttributeValue not yet implemented")
}
// 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.
//
// The new expression is given as a hcl.Traversal, which must be an absolute
// traversal. To set a literal value, use SetAttributeValue.
//
// The return value is the attribute that was either modified in-place or
// created.
func (n *Body) SetAttributeTraversal(name string, traversal hcl.Traversal) *Attribute {
panic("Body.SetAttributeTraversal not yet implemented")
func (c *comments) BuildTokens(to Tokens) Tokens {
return c.tokens.BuildTokens(to)
}
type Attribute struct {
AllTokens *TokenSeq
LeadCommentTokens *TokenSeq
NameTokens *TokenSeq
EqualsTokens *TokenSeq
Expr *Expression
LineCommentTokens *TokenSeq
EOLTokens *TokenSeq
}
type identifier struct {
leafNode
func (a *Attribute) walkChildNodes(w internalWalkFunc) {
w(a.Expr)
parent *node
token *Token
}
func (n *Attribute) Tokens() *TokenSeq {
return n.AllTokens
func newIdentifier(token *Token) *identifier {
return &identifier{
token: token,
}
}
type Block struct {
AllTokens *TokenSeq
LeadCommentTokens *TokenSeq
TypeTokens *TokenSeq
LabelTokens []*TokenSeq
LabelTokensFlat *TokenSeq
OBraceTokens *TokenSeq
Body *Body
CBraceTokens *TokenSeq
EOLTokens *TokenSeq
func (i *identifier) BuildTokens(to Tokens) Tokens {
return append(to, i.token)
}
func (n *Block) walkChildNodes(w internalWalkFunc) {
w(n.Body)
func (i *identifier) hasName(name string) bool {
return name == string(i.token.Bytes)
}
func (n *Block) Tokens() *TokenSeq {
return n.AllTokens
}
type number struct {
leafNode
type Expression struct {
AllTokens *TokenSeq
AbsTraversals []*Traversal
parent *node
token *Token
}
func (n *Expression) walkChildNodes(w internalWalkFunc) {
for _, name := range n.AbsTraversals {
w(name)
func newNumber(token *Token) *number {
return &number{
token: token,
}
}
func (n *Expression) Tokens() *TokenSeq {
return n.AllTokens
func (n *number) BuildTokens(to Tokens) Tokens {
return append(to, n.token)
}
type Traversal struct {
AllTokens *TokenSeq
Steps []*Traverser
}
func (n *Traversal) walkChildNodes(w internalWalkFunc) {
for _, step := range n.Steps {
w(step)
}
}
type quoted struct {
leafNode
func (n *Traversal) Tokens() *TokenSeq {
return n.AllTokens
parent *node
tokens Tokens
}
type Traverser struct {
AllTokens *TokenSeq
Logical hcl.Traverser
}
func (n *Traverser) Tokens() *TokenSeq {
return n.AllTokens
func newQuoted(tokens Tokens) *quoted {
return &quoted{
tokens: tokens,
}
}
func (n *Traverser) walkChildNodes(w internalWalkFunc) {
// No child nodes for a traversal step
func (q *quoted) BuildTokens(to Tokens) Tokens {
return q.tokens.BuildTokens(to)
}

@ -0,0 +1,137 @@
package hclwrite
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type Body struct {
inTree
items nodeSet
// indentLevel is the number of spaces that should appear at the start
// of lines added within this body.
indentLevel int
}
func (b *Body) appendItem(c nodeContent) *node {
nn := b.children.Append(c)
b.items.Add(nn)
return nn
}
func (b *Body) appendItemNode(nn *node) *node {
nn.assertUnattached()
b.children.AppendNode(nn)
b.items.Add(nn)
return nn
}
func (b *Body) AppendUnstructuredTokens(ts Tokens) {
b.inTree.children.Append(ts)
}
// 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 {
for n := range b.items {
if attr, isAttr := n.content.(*Attribute); isAttr {
nameObj := attr.name.content.(*identifier)
if nameObj.hasName(name) {
// We've found it!
return attr
}
}
}
return nil
}
// SetAttributeValue either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block.
//
// The value is given as a cty.Value, and must therefore be a literal. To set
// a variable reference or other traversal, use SetAttributeTraversal.
//
// The return value is the attribute that was either modified in-place or
// created.
func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
attr := b.GetAttribute(name)
expr := NewExpressionLiteral(val)
if attr != nil {
attr.expr = attr.expr.ReplaceWith(expr)
} else {
attr := newAttribute()
attr.init(name, expr)
b.appendItem(attr)
}
return attr
}
// 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.
//
// The new expression is given as a hcl.Traversal, which must be an absolute
// traversal. To set a literal value, use SetAttributeValue.
//
// 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")
}
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)
}
type Block struct {
inTree
leadComments *node
typeName *node
labels nodeSet
open *node
body *node
close *node
}

@ -0,0 +1,82 @@
package hclwrite
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
)
type Expression struct {
inTree
absTraversals nodeSet
}
func newExpression() *Expression {
return &Expression{
inTree: newInTree(),
absTraversals: newNodeSet(),
}
}
// NewExpressionLiteral constructs an an expression that represents the given
// literal value.
//
// Since an unknown value cannot be represented in source code, this function
// will panic if the given value is unknown or contains a nested unknown value.
// Use val.IsWhollyKnown before calling to be sure.
//
// HCL native syntax does not directly represent lists, maps, and sets, and
// instead relies on the automatic conversions to those collection types from
// either list or tuple constructor syntax. Therefore converting collection
// values to source code and re-reading them will lose type information, and
// the reader must provide a suitable type at decode time to recover the
// original value.
func NewExpressionLiteral(val cty.Value) *Expression {
toks := TokensForValue(val)
expr := newExpression()
expr.children.AppendUnstructuredTokens(toks)
return expr
}
// 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")
}
type Traversal struct {
inTree
steps nodeSet
}
func newTraversal() *Traversal {
return &Traversal{
inTree: newInTree(),
steps: newNodeSet(),
}
}
type TraverseName struct {
inTree
name *node
}
func newTraverseName() *TraverseName {
return &TraverseName{
inTree: newInTree(),
}
}
type TraverseIndex struct {
inTree
key *node
}
func newTraverseIndex() *TraverseIndex {
return &TraverseIndex{
inTree: newInTree(),
}
}

@ -0,0 +1,231 @@
package hclwrite
import (
"fmt"
"github.com/google/go-cmp/cmp"
)
// node represents a node in the AST.
type node struct {
content nodeContent
list *nodes
before, after *node
}
func newNode(c nodeContent) *node {
return &node{
content: c,
}
}
func (n *node) Equal(other *node) bool {
return cmp.Equal(n.content, other.content)
}
func (n *node) BuildTokens(to Tokens) Tokens {
return n.content.BuildTokens(to)
}
// Detach removes the receiver from the list it currently belongs to. If the
// node is not currently in a list, this is a no-op.
func (n *node) Detach() {
if n.list == nil {
return
}
if n.before != nil {
n.before.after = n.after
}
if n.after != nil {
n.after.before = n.before
}
if n.list.first == n {
n.list.first = n.after
}
if n.list.last == n {
n.list.last = n.before
}
n.list = nil
n.before = nil
n.after = nil
}
// ReplaceWith removes the receiver from the list it currently belongs to and
// inserts a new node with the given content in its place. If the node is not
// currently in a list, this function will panic.
//
// The return value is the newly-constructed node, containing the given content.
// After this function returns, the reciever is no longer attached to a list.
func (n *node) ReplaceWith(c nodeContent) *node {
if n.list == nil {
panic("can't replace node that is not in a list")
}
before := n.before
after := n.after
list := n.list
n.before, n.after, n.list = nil, nil, nil
nn := newNode(c)
nn.before = before
nn.after = after
nn.list = list
if before != nil {
before.after = nn
}
if after != nil {
after.before = nn
}
return nn
}
func (n *node) assertUnattached() {
if n.list != nil {
panic(fmt.Sprintf("attempt to attach already-attached node %#v", n))
}
}
// nodeContent is the interface type implemented by all AST content types.
type nodeContent interface {
walkChildNodes(w internalWalkFunc)
BuildTokens(to Tokens) Tokens
}
// nodes is a list of nodes.
type nodes struct {
first, last *node
}
func (ns *nodes) BuildTokens(to Tokens) Tokens {
for n := ns.first; n != nil; n = n.after {
to = n.BuildTokens(to)
}
return to
}
func (ns *nodes) Append(c nodeContent) *node {
n := &node{
content: c,
}
ns.AppendNode(n)
n.list = ns
return n
}
func (ns *nodes) AppendNode(n *node) {
if ns.last != nil {
n.before = ns.last
ns.last.after = n
}
n.list = ns
ns.last = n
if ns.first == nil {
ns.first = n
}
}
func (ns *nodes) AppendUnstructuredTokens(tokens Tokens) *node {
if len(tokens) == 0 {
return nil
}
n := newNode(tokens)
ns.AppendNode(n)
n.list = ns
return n
}
// nodeSet is an unordered set of nodes. It is used to describe a set of nodes
// that all belong to the same list that have some role or characteristic
// in common.
type nodeSet map[*node]struct{}
func newNodeSet() nodeSet {
return make(nodeSet)
}
func (ns nodeSet) Has(n *node) bool {
if ns == nil {
return false
}
_, exists := ns[n]
return exists
}
func (ns nodeSet) Add(n *node) {
ns[n] = struct{}{}
}
func (ns nodeSet) Remove(n *node) {
delete(ns, n)
}
func (ns nodeSet) List() []*node {
if len(ns) == 0 {
return nil
}
ret := make([]*node, 0, len(ns))
// Determine which list we are working with. We assume here that all of
// the nodes belong to the same list, since that is part of the contract
// for nodeSet.
var list *nodes
for n := range ns {
list = n.list
break
}
// We recover the order by iterating over the whole list. This is not
// the most efficient way to do it, but our node lists should always be
// small so not worth making things more complex.
for n := list.first; n != nil; n = n.after {
if ns.Has(n) {
ret = append(ret, n)
}
}
return ret
}
type internalWalkFunc func(*node)
// inTree can be embedded into a content struct that has child nodes to get
// a standard implementation of the NodeContent interface and a record of
// a potential parent node.
type inTree struct {
parent *node
children *nodes
}
func newInTree() inTree {
return inTree{
children: &nodes{},
}
}
func (it *inTree) assertUnattached() {
if it.parent != nil {
panic(fmt.Sprintf("node is already attached to %T", it.parent.content))
}
}
func (it *inTree) walkChildNodes(w internalWalkFunc) {
for n := it.children.first; n != nil; n = n.after {
w(n)
}
}
func (it *inTree) BuildTokens(to Tokens) Tokens {
for n := it.children.first; n != nil; n = n.after {
to = n.BuildTokens(to)
}
return to
}
// leafNode can be embedded into a content struct to give it a do-nothing
// implementation of walkChildNodes
type leafNode struct {
}
func (n *leafNode) walkChildNodes(w internalWalkFunc) {
}

@ -1,10 +1,12 @@
package hclwrite
import (
"fmt"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
// Our "parser" here is actually not doing any parsing of its own. Instead,
@ -49,18 +51,19 @@ func parse(src []byte, filename string, start hcl.Pos) (*File, hcl.Diagnostics)
}
before, root, after := parseBody(file.Body.(*hclsyntax.Body), from)
ret := &File{
inTree: newInTree(),
return &File{
Name: filename,
SrcBytes: src,
Body: root,
AllTokens: &TokenSeq{
before.Seq(),
root.AllTokens,
after.Seq(),
},
}, nil
srcBytes: src,
body: root,
}
nodes := ret.inTree.children
nodes.Append(before.Tokens())
nodes.AppendNode(root)
nodes.Append(after.Tokens())
return ret, diags
}
type inputTokens struct {
@ -76,6 +79,23 @@ func (it inputTokens) Partition(rng hcl.Range) (before, within, after inputToken
return
}
func (it inputTokens) PartitionType(ty hclsyntax.TokenType) (before, within, after inputTokens) {
for i, t := range it.writerTokens {
if t.Type == ty {
return it.Slice(0, i), it.Slice(i, i+1), it.Slice(i+1, len(it.nativeTokens))
}
}
panic(fmt.Sprintf("didn't find any token of type %s", ty))
}
func (it inputTokens) PartitionTypeSingle(ty hclsyntax.TokenType) (before inputTokens, found *Token, after inputTokens) {
before, within, after := it.PartitionType(ty)
if within.Len() != 1 {
panic("PartitionType found more than one token")
}
return before, within.Tokens()[0], after
}
// PartitionIncludeComments is like Partition except the returned "within"
// range includes any lead and line comments associated with the range.
func (it inputTokens) PartitionIncludingComments(rng hcl.Range) (before, within, after inputTokens) {
@ -133,8 +153,8 @@ func (it inputTokens) Len() int {
return len(it.nativeTokens)
}
func (it inputTokens) Seq() *TokenSeq {
return &TokenSeq{it.writerTokens}
func (it inputTokens) Tokens() Tokens {
return it.writerTokens
}
func (it inputTokens) Types() []hclsyntax.TokenType {
@ -148,7 +168,7 @@ func (it inputTokens) Types() []hclsyntax.TokenType {
// parseBody locates the given body within the given input tokens and returns
// the resulting *Body object as well as the tokens that appeared before and
// after it.
func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *Body, inputTokens) {
func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *node, inputTokens) {
before, within, after := from.PartitionIncludingComments(nativeBody.SrcRange)
// The main AST doesn't retain the original source ordering of the
@ -164,7 +184,10 @@ func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *Body
sort.Sort(nativeNodeSorter{nativeItems})
body := &Body{
IndentLevel: 0, // TODO: deal with this
inTree: newInTree(),
indentLevel: 0, // TODO: deal with this
items: newNodeSet(),
}
remain := within
@ -172,24 +195,24 @@ func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *Body
beforeItem, item, afterItem := parseBodyItem(nativeItem, remain)
if beforeItem.Len() > 0 {
body.AppendUnstructuredTokens(beforeItem.Seq())
body.AppendUnstructuredTokens(beforeItem.Tokens())
}
body.AppendItem(item)
body.appendItemNode(item)
remain = afterItem
}
if remain.Len() > 0 {
body.AppendUnstructuredTokens(remain.Seq())
body.AppendUnstructuredTokens(remain.Tokens())
}
return before, body, after
return before, newNode(body), after
}
func parseBodyItem(nativeItem hclsyntax.Node, from inputTokens) (inputTokens, Node, inputTokens) {
func parseBodyItem(nativeItem hclsyntax.Node, from inputTokens) (inputTokens, *node, inputTokens) {
before, leadComments, within, lineComments, newline, after := from.PartitionBlockItem(nativeItem.Range())
var item Node
var item *node
switch tItem := nativeItem.(type) {
case *hclsyntax.Attribute:
@ -204,90 +227,96 @@ func parseBodyItem(nativeItem hclsyntax.Node, from inputTokens) (inputTokens, No
return before, item, after
}
func parseAttribute(nativeAttr *hclsyntax.Attribute, from, leadComments, lineComments, newline inputTokens) *Attribute {
var allTokens TokenSeq
attr := &Attribute{}
func parseAttribute(nativeAttr *hclsyntax.Attribute, from, leadComments, lineComments, newline inputTokens) *node {
attr := &Attribute{
inTree: newInTree(),
}
children := attr.inTree.children
if leadComments.Len() > 0 {
attr.LeadCommentTokens = leadComments.Seq()
allTokens = append(allTokens, attr.LeadCommentTokens)
{
cn := newNode(newComments(leadComments.Tokens()))
attr.leadComments = cn
children.AppendNode(cn)
}
before, nameTokens, from := from.Partition(nativeAttr.NameRange)
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
{
children.AppendUnstructuredTokens(before.Tokens())
if nameTokens.Len() != 1 {
// Should never happen with valid input
panic("attribute name is not exactly one token")
}
token := nameTokens.Tokens()[0]
in := newNode(newIdentifier(token))
attr.name = in
children.AppendNode(in)
}
attr.NameTokens = nameTokens.Seq()
allTokens = append(allTokens, attr.NameTokens)
before, equalsTokens, from := from.Partition(nativeAttr.EqualsRange)
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
}
attr.EqualsTokens = equalsTokens.Seq()
allTokens = append(allTokens, attr.EqualsTokens)
children.AppendUnstructuredTokens(before.Tokens())
children.AppendUnstructuredTokens(equalsTokens.Tokens())
before, exprTokens, from := from.Partition(nativeAttr.Expr.Range())
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
{
children.AppendUnstructuredTokens(before.Tokens())
exprNode := parseExpression(nativeAttr.Expr, exprTokens)
attr.expr = exprNode
children.AppendNode(exprNode)
}
attr.Expr = parseExpression(nativeAttr.Expr, exprTokens)
allTokens = append(allTokens, attr.Expr.AllTokens)
if lineComments.Len() > 0 {
attr.LineCommentTokens = lineComments.Seq()
allTokens = append(allTokens, attr.LineCommentTokens)
{
cn := newNode(newComments(lineComments.Tokens()))
attr.lineComments = cn
children.AppendNode(cn)
}
if newline.Len() > 0 {
attr.EOLTokens = newline.Seq()
allTokens = append(allTokens, attr.EOLTokens)
}
children.AppendUnstructuredTokens(newline.Tokens())
// Collect any stragglers, though there shouldn't be any
if from.Len() > 0 {
allTokens = append(allTokens, from.Seq())
}
attr.AllTokens = &allTokens
children.AppendUnstructuredTokens(from.Tokens())
return attr
return newNode(attr)
}
func parseBlock(nativeBlock *hclsyntax.Block, from, leadComments, lineComments, newline inputTokens) *Block {
var allTokens TokenSeq
block := &Block{}
func parseBlock(nativeBlock *hclsyntax.Block, from, leadComments, lineComments, newline inputTokens) *node {
block := &Block{
inTree: newInTree(),
labels: newNodeSet(),
}
children := block.inTree.children
if leadComments.Len() > 0 {
block.LeadCommentTokens = leadComments.Seq()
allTokens = append(allTokens, block.LeadCommentTokens)
{
cn := newNode(newComments(leadComments.Tokens()))
block.leadComments = cn
children.AppendNode(cn)
}
before, typeTokens, from := from.Partition(nativeBlock.TypeRange)
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
{
children.AppendUnstructuredTokens(before.Tokens())
if typeTokens.Len() != 1 {
// Should never happen with valid input
panic("block type name is not exactly one token")
}
token := typeTokens.Tokens()[0]
in := newNode(newIdentifier(token))
block.typeName = in
children.AppendNode(in)
}
block.TypeTokens = typeTokens.Seq()
allTokens = append(allTokens, block.TypeTokens)
for _, rng := range nativeBlock.LabelRanges {
var labelTokens inputTokens
before, labelTokens, from = from.Partition(rng)
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
}
seq := labelTokens.Seq()
block.LabelTokens = append(block.LabelTokens, seq)
*(block.LabelTokensFlat) = append(*(block.LabelTokensFlat), seq)
allTokens = append(allTokens, seq)
children.AppendUnstructuredTokens(before.Tokens())
tokens := labelTokens.Tokens()
ln := newNode(newQuoted(tokens))
block.labels.Add(ln)
children.AppendNode(ln)
}
before, oBrace, from := from.Partition(nativeBlock.OpenBraceRange)
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
}
block.OBraceTokens = oBrace.Seq()
allTokens = append(allTokens, block.OBraceTokens)
children.AppendUnstructuredTokens(before.Tokens())
children.AppendUnstructuredTokens(oBrace.Tokens())
// We go a bit out of order here: we go hunting for the closing brace
// so that we have a delimited body, but then we'll deal with the body
@ -295,87 +324,109 @@ func parseBlock(nativeBlock *hclsyntax.Block, from, leadComments, lineComments,
// that appear after it.
bodyTokens, cBrace, from := from.Partition(nativeBlock.CloseBraceRange)
before, body, after := parseBody(nativeBlock.Body, bodyTokens)
children.AppendUnstructuredTokens(before.Tokens())
block.body = body
children.AppendNode(body)
children.AppendUnstructuredTokens(after.Tokens())
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
}
block.Body = body
allTokens = append(allTokens, body.AllTokens)
if after.Len() > 0 {
allTokens = append(allTokens, after.Seq())
}
block.CBraceTokens = cBrace.Seq()
allTokens = append(allTokens, block.CBraceTokens)
children.AppendUnstructuredTokens(cBrace.Tokens())
// stragglers
if after.Len() > 0 {
allTokens = append(allTokens, from.Seq())
}
children.AppendUnstructuredTokens(from.Tokens())
if lineComments.Len() > 0 {
// blocks don't actually have line comments, so we'll just treat
// them as extra stragglers
allTokens = append(allTokens, lineComments.Seq())
}
if newline.Len() > 0 {
block.EOLTokens = newline.Seq()
allTokens = append(allTokens, block.EOLTokens)
children.AppendUnstructuredTokens(lineComments.Tokens())
}
children.AppendUnstructuredTokens(newline.Tokens())
block.AllTokens = &allTokens
return block
return newNode(block)
}
func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *Expression {
var allTokens TokenSeq
nativeVars := nativeExpr.Variables()
var absTraversals []*Traversal
for _, nativeTraversal := range nativeVars {
var traversalTokens TokenSeq
var before, traversalFrom inputTokens
before, traversalFrom, from = from.Partition(nativeTraversal.SourceRange())
if before.Len() > 0 {
allTokens = append(allTokens, before.Seq())
}
var steps []*Traverser
func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node {
expr := newExpression()
children := expr.inTree.children
for _, nativeStep := range nativeTraversal {
var stepFrom inputTokens
before, stepFrom, traversalFrom = traversalFrom.Partition(nativeStep.SourceRange())
stepTokens := stepFrom.Seq()
if before.Len() > 0 {
traversalTokens = append(traversalTokens, before.Seq())
}
traversalTokens = append(traversalTokens, stepTokens)
step := &Traverser{
AllTokens: stepTokens,
Logical: nativeStep,
}
steps = append(steps, step)
}
// Attach any straggler that don't belong to a step to the traversal itself.
if traversalFrom.Len() > 0 {
traversalTokens = append(traversalTokens, traversalFrom.Seq())
}
allTokens = append(allTokens, &traversalTokens)
nativeVars := nativeExpr.Variables()
absTraversals = append(absTraversals, &Traversal{
AllTokens: &traversalTokens,
Steps: steps,
})
for _, nativeTraversal := range nativeVars {
before, traversal, after := parseTraversal(nativeTraversal, from)
children.AppendUnstructuredTokens(before.Tokens())
children.AppendNode(traversal)
expr.absTraversals.Add(traversal)
from = after
}
// Attach any stragglers that don't belong to a traversal to the expression
// itself. In an expression with no traversals at all, this is just the
// entirety of "from".
if from.Len() > 0 {
allTokens = append(allTokens, from.Seq())
children.AppendUnstructuredTokens(from.Tokens())
return newNode(expr)
}
func parseTraversal(nativeTraversal hcl.Traversal, from inputTokens) (before inputTokens, n *node, after inputTokens) {
traversal := newTraversal()
children := traversal.inTree.children
before, from, after = from.Partition(nativeTraversal.SourceRange())
stepAfter := from
for _, nativeStep := range nativeTraversal {
before, step, after := parseTraversalStep(nativeStep, stepAfter)
children.AppendUnstructuredTokens(before.Tokens())
children.AppendNode(step)
stepAfter = after
}
return &Expression{
AllTokens: &allTokens,
AbsTraversals: absTraversals,
return before, newNode(traversal), after
}
func parseTraversalStep(nativeStep hcl.Traverser, from inputTokens) (before inputTokens, n *node, after inputTokens) {
var children *nodes
switch tNativeStep := nativeStep.(type) {
case hcl.TraverseRoot, hcl.TraverseAttr:
step := newTraverseName()
children = step.inTree.children
before, from, after = from.Partition(nativeStep.SourceRange())
inBefore, token, inAfter := from.PartitionTypeSingle(hclsyntax.TokenIdent)
name := newIdentifier(token)
children.AppendUnstructuredTokens(inBefore.Tokens())
step.name = children.Append(name)
children.AppendUnstructuredTokens(inAfter.Tokens())
return before, newNode(step), after
case hcl.TraverseIndex:
step := newTraverseIndex()
children = step.inTree.children
before, from, after = from.Partition(nativeStep.SourceRange())
var inBefore, oBrack, keyTokens, cBrack inputTokens
inBefore, oBrack, from = from.PartitionType(hclsyntax.TokenOBrack)
children.AppendUnstructuredTokens(inBefore.Tokens())
children.AppendUnstructuredTokens(oBrack.Tokens())
keyTokens, cBrack, from = from.PartitionType(hclsyntax.TokenCBrack)
keyVal := tNativeStep.Key
switch keyVal.Type() {
case cty.String:
key := newQuoted(keyTokens.Tokens())
step.key = children.Append(key)
case cty.Number:
valBefore, valToken, valAfter := keyTokens.PartitionTypeSingle(hclsyntax.TokenNumberLit)
children.AppendUnstructuredTokens(valBefore.Tokens())
key := newNumber(valToken)
step.key = children.Append(key)
children.AppendUnstructuredTokens(valAfter.Tokens())
}
children.AppendUnstructuredTokens(cBrack.Tokens())
children.AppendUnstructuredTokens(from.Tokens())
return before, newNode(step), after
default:
panic(fmt.Sprintf("unsupported traversal step type %T", nativeStep))
}
}
// writerTokens takes a sequence of tokens as produced by the main hclsyntax

@ -6,6 +6,20 @@ import (
"github.com/hashicorp/hcl2/hcl"
)
// NewFile creates a new file object that is empty and ready to have constructs
// added t it.
func NewFile() *File {
body := &Body{
inTree: newInTree(),
indentLevel: 0,
}
file := &File{
inTree: newInTree(),
}
file.body = file.inTree.children.Append(body)
return file
}
// ParseConfig interprets the given source bytes into a *hclwrite.File. The
// resulting AST can be used to perform surgical edits on the source code
// before turning it back into bytes again.
@ -25,6 +39,6 @@ func Format(src []byte) []byte {
tokens := lexConfig(src)
format(tokens)
buf := &bytes.Buffer{}
(&TokenSeq{tokens}).WriteTo(buf)
tokens.WriteTo(buf)
return buf.Bytes()
}

@ -8,19 +8,6 @@ import (
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
// TokenGen is an abstract type that can append tokens to a list. It is the
// low-level foundation underlying the hclwrite AST; the AST provides a
// convenient abstraction over raw token sequences to facilitate common tasks,
// but it's also possible to directly manipulate the tree of token generators
// to make changes that the AST API doesn't directly allow.
type TokenGen interface {
EachToken(TokenCallback)
}
// TokenCallback is used with TokenGen implementations to specify the action
// that is to be taken for each token in the flattened token sequence.
type TokenCallback func(*Token)
// Token is a single sequence of bytes annotated with a type. It is similar
// in purpose to hclsyntax.Token, but discards the source position information
// since that is not useful in code generation.
@ -38,17 +25,16 @@ type Token struct {
// Tokens is a flat list of tokens.
type Tokens []*Token
func (ts Tokens) WriteTo(wr io.Writer) (int, error) {
seq := &TokenSeq{ts}
return seq.WriteTo(wr)
}
func (ts Tokens) Bytes() []byte {
buf := &bytes.Buffer{}
ts.WriteTo(buf)
return buf.Bytes()
}
func (ts Tokens) testValue() string {
return string(ts.Bytes())
}
// Columns returns the number of columns (grapheme clusters) the token sequence
// occupies. The result is not meaningful if there are newline or single-line
// comment tokens in the sequence.
@ -62,43 +48,10 @@ func (ts Tokens) Columns() int {
return ret
}
// TokenSeq combines zero or more TokenGens together to produce a flat sequence
// of tokens from a tree of TokenGens.
type TokenSeq []TokenGen
func (t *Token) EachToken(cb TokenCallback) {
cb(t)
}
func (ts Tokens) EachToken(cb TokenCallback) {
for _, t := range ts {
cb(t)
}
}
func (ts *TokenSeq) EachToken(cb TokenCallback) {
if ts == nil {
return
}
for _, gen := range *ts {
gen.EachToken(cb)
}
}
// Tokens returns the flat list of tokens represented by the receiving
// token sequence.
func (ts *TokenSeq) Tokens() Tokens {
var tokens Tokens
ts.EachToken(func(token *Token) {
tokens = append(tokens, token)
})
return tokens
}
// 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 *TokenSeq) WriteTo(wr io.Writer) (int, error) {
func (ts Tokens) WriteTo(wr io.Writer) (int, 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.
@ -109,9 +62,9 @@ func (ts *TokenSeq) WriteTo(wr io.Writer) (int, error) {
var n int
var err error
ts.EachToken(func(token *Token) {
for _, token := range ts {
if err != nil {
return
return n, err
}
for spacesBefore := token.SpacesBefore; spacesBefore > 0; spacesBefore -= len(spaces) {
@ -123,48 +76,29 @@ func (ts *TokenSeq) WriteTo(wr io.Writer) (int, error) {
thisN, err = wr.Write(spaces[:thisChunk])
n += thisN
if err != nil {
return
return n, err
}
}
var thisN int
thisN, err = wr.Write(token.Bytes)
n += thisN
})
}
return n, err
}
// SoloToken returns the single token represented by the receiving sequence,
// or nil if the sequence does not represent exactly one token.
func (ts *TokenSeq) SoloToken() *Token {
var ret *Token
found := false
ts.EachToken(func(tok *Token) {
if ret == nil && !found {
ret = tok
found = true
} else if ret != nil && found {
ret = nil
}
})
return ret
func (ts Tokens) walkChildNodes(w internalWalkFunc) {
// Unstructured tokens have no child nodes
}
// IsIdent returns true if and only if the token sequence represents a single
// ident token whose name matches the given string.
func (ts *TokenSeq) IsIdent(name []byte) bool {
tok := ts.SoloToken()
if tok == nil {
return false
}
if tok.Type != hclsyntax.TokenIdent {
return false
}
return bytes.Equal(tok.Bytes, name)
func (ts Tokens) BuildTokens(to Tokens) Tokens {
return append(to, ts...)
}
// TokenSeqEmpty is a TokenSeq that contains no tokens. It can be used anywhere,
// but its primary purpose is to be assigned as a replacement for a non-empty
// TokenSeq when eliminating a section of an input file.
var TokenSeqEmpty = TokenSeq([]TokenGen(nil))
func newIdentToken(name string) *Token {
return &Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(name),
}
}

56
vendor/vendor.json vendored

@ -1759,70 +1759,70 @@
"revisionTime": "2017-05-04T19:02:34Z"
},
{
"checksumSHA1": "dJPromzLdd492RQjE/09klKRXGs=",
"checksumSHA1": "8q8+Dn1uRDlN4OZ5kqQnUMgSPRk=",
"path": "github.com/hashicorp/hcl2/ext/dynblock",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "IAfC0Xri1iCRgbbiDBgs6ue8/Ic=",
"path": "github.com/hashicorp/hcl2/ext/typeexpr",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "BRJaQcKriVKEirVC7YxBxPufQF0=",
"path": "github.com/hashicorp/hcl2/gohcl",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "3ypdUCoJwZt+XRMV/6FoRhywz8A=",
"checksumSHA1": "LotrMqeWeTv/rNOGUHRs9iVBjoQ=",
"path": "github.com/hashicorp/hcl2/hcl",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "o6XGTmFfazaLQiSVs5cnaNvJhYQ=",
"checksumSHA1": "RNoOVGaFtYqaPMyARZuHc2OejDs=",
"path": "github.com/hashicorp/hcl2/hcl/hclsyntax",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "Cuhv6UBgimVhWWwYm8v7Moisrhg=",
"checksumSHA1": "4Cr8I/nepYf4eRCl5hiazPf+afs=",
"path": "github.com/hashicorp/hcl2/hcl/json",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "iIVMnRuvfOy/tJ1zE9rVcjD/01A=",
"checksumSHA1": "6JRj4T/iQxIe/CoKXHDjPuupmL8=",
"path": "github.com/hashicorp/hcl2/hcldec",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "sySYF9Ew71VS/LfrG+s/0jK+1VQ=",
"path": "github.com/hashicorp/hcl2/hcled",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "IzmftuG99BqNhbFGhxZaGwtiMtM=",
"path": "github.com/hashicorp/hcl2/hclparse",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "UQzPVMlOo/3CJQnShbnNVcTF4EA=",
"path": "github.com/hashicorp/hcl2/hcltest",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "v2ADtHrkDgQdqsroh9SJRsfANTk=",
"checksumSHA1": "+B/DKtBk5ag/s10xr8X1IepziY4=",
"path": "github.com/hashicorp/hcl2/hclwrite",
"revision": "41cff854d8157be197e6b4698a8d9570ced9476b",
"revisionTime": "2018-07-18T22:41:35Z"
"revision": "ed8144cda141c1f06f8ab122baff783b645cfe52",
"revisionTime": "2018-08-22T19:31:30Z"
},
{
"checksumSHA1": "M09yxoBoCEtG7EcHR8aEWLzMMJc=",

Loading…
Cancel
Save