hcl/hclsyntax/expression_template.go

221 lines
5.6 KiB
Go

package hclsyntax
import (
"bytes"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
type TemplateExpr struct {
Parts []Expression
SrcRange hcl.Range
}
func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
for _, part := range e.Parts {
w(part)
}
}
func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
buf := &bytes.Buffer{}
var diags hcl.Diagnostics
isKnown := true
for _, part := range e.Parts {
partVal, partDiags := part.Value(ctx)
diags = append(diags, partDiags...)
if partVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"The expression result is null. Cannot include a null value in a string template.",
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Expression: part,
EvalContext: ctx,
})
continue
}
if !partVal.IsKnown() {
// If any part is unknown then the result as a whole must be
// unknown too. We'll keep on processing the rest of the parts
// anyway, because we want to still emit any diagnostics resulting
// from evaluating those.
isKnown = false
continue
}
strVal, err := convert.Convert(partVal, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"Cannot include the given value in a string template: %s.",
err.Error(),
),
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Expression: part,
EvalContext: ctx,
})
continue
}
buf.WriteString(strVal.AsString())
}
if !isKnown {
return cty.UnknownVal(cty.String), diags
}
return cty.StringVal(buf.String()), diags
}
func (e *TemplateExpr) Range() hcl.Range {
return e.SrcRange
}
func (e *TemplateExpr) StartRange() hcl.Range {
return e.Parts[0].StartRange()
}
// IsStringLiteral returns true if and only if the template consists only of
// single string literal, as would be created for a simple quoted string like
// "foo".
//
// If this function returns true, then calling Value on the same expression
// with a nil EvalContext will return the literal value.
//
// Note that "${"foo"}", "${1}", etc aren't considered literal values for the
// purposes of this method, because the intent of this method is to identify
// situations where the user seems to be explicitly intending literal string
// interpretation, not situations that result in literals as a technicality
// of the template expression unwrapping behavior.
func (e *TemplateExpr) IsStringLiteral() bool {
if len(e.Parts) != 1 {
return false
}
_, ok := e.Parts[0].(*LiteralValueExpr)
return ok
}
// TemplateJoinExpr is used to convert tuples of strings produced by template
// constructs (i.e. for loops) into flat strings, by converting the values
// tos strings and joining them. This AST node is not used directly; it's
// produced as part of the AST of a "for" loop in a template.
type TemplateJoinExpr struct {
Tuple Expression
}
func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) {
w(e.Tuple)
}
func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
tuple, diags := e.Tuple.Value(ctx)
if tuple.IsNull() {
// This indicates a bug in the code that constructed the AST.
panic("TemplateJoinExpr got null tuple")
}
if tuple.Type() == cty.DynamicPseudoType {
return cty.UnknownVal(cty.String), diags
}
if !tuple.Type().IsTupleType() {
// This indicates a bug in the code that constructed the AST.
panic("TemplateJoinExpr got non-tuple tuple")
}
if !tuple.IsKnown() {
return cty.UnknownVal(cty.String), diags
}
buf := &bytes.Buffer{}
it := tuple.ElementIterator()
for it.Next() {
_, val := it.Element()
if val.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"An iteration result is null. Cannot include a null value in a string template.",
),
Subject: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
})
continue
}
if val.Type() == cty.DynamicPseudoType {
return cty.UnknownVal(cty.String), diags
}
strVal, err := convert.Convert(val, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"Cannot include one of the interpolation results into the string template: %s.",
err.Error(),
),
Subject: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
})
continue
}
if !val.IsKnown() {
return cty.UnknownVal(cty.String), diags
}
buf.WriteString(strVal.AsString())
}
return cty.StringVal(buf.String()), diags
}
func (e *TemplateJoinExpr) Range() hcl.Range {
return e.Tuple.Range()
}
func (e *TemplateJoinExpr) StartRange() hcl.Range {
return e.Tuple.StartRange()
}
// TemplateWrapExpr is used instead of a TemplateExpr when a template
// consists _only_ of a single interpolation sequence. In that case, the
// template's result is the single interpolation's result, verbatim with
// no type conversions.
type TemplateWrapExpr struct {
Wrapped Expression
SrcRange hcl.Range
}
func (e *TemplateWrapExpr) walkChildNodes(w internalWalkFunc) {
w(e.Wrapped)
}
func (e *TemplateWrapExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return e.Wrapped.Value(ctx)
}
func (e *TemplateWrapExpr) Range() hcl.Range {
return e.SrcRange
}
func (e *TemplateWrapExpr) StartRange() hcl.Range {
return e.SrcRange
}