876b4237a0
If a template expression interpolates values which have marks, we should apply all of those marks to the output value. This allows template expressions to function like native cty functions with respect to marks.
234 lines
6.0 KiB
Go
234 lines
6.0 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
|
|
|
|
// Maintain a set of marks for values used in the template
|
|
marks := make(cty.ValueMarks)
|
|
|
|
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
|
|
}
|
|
|
|
// Unmark the part and merge its marks into the set
|
|
unmarked, partMarks := strVal.Unmark()
|
|
for k, v := range partMarks {
|
|
marks[k] = v
|
|
}
|
|
|
|
buf.WriteString(unmarked.AsString())
|
|
}
|
|
|
|
var ret cty.Value
|
|
if !isKnown {
|
|
ret = cty.UnknownVal(cty.String)
|
|
} else {
|
|
ret = cty.StringVal(buf.String())
|
|
}
|
|
|
|
// Apply the full set of marks to the returned value
|
|
return ret.WithMarks(marks), 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
|
|
}
|