f2f7dd7632
If individual template expressions in a loop have marks, merge those marks into the final result when joining.
240 lines
6.2 KiB
Go
240 lines
6.2 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
|
|
}
|
|
|
|
tuple, marks := tuple.Unmark()
|
|
allMarks := []cty.ValueMarks{marks}
|
|
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).WithMarks(marks), 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).WithMarks(marks), diags
|
|
}
|
|
|
|
strVal, strValMarks := strVal.Unmark()
|
|
if len(strValMarks) > 0 {
|
|
allMarks = append(allMarks, strValMarks)
|
|
}
|
|
buf.WriteString(strVal.AsString())
|
|
}
|
|
|
|
return cty.StringVal(buf.String()).WithMarks(allMarks...), 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
|
|
}
|