github.com/hashicorp/hcl/v2@v2.20.0/hclsyntax/expression_template.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hclsyntax
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  
    10  	"github.com/hashicorp/hcl/v2"
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/convert"
    13  )
    14  
    15  type TemplateExpr struct {
    16  	Parts []Expression
    17  
    18  	SrcRange hcl.Range
    19  }
    20  
    21  func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
    22  	for _, part := range e.Parts {
    23  		w(part)
    24  	}
    25  }
    26  
    27  func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
    28  	buf := &bytes.Buffer{}
    29  	var diags hcl.Diagnostics
    30  	isKnown := true
    31  
    32  	// Maintain a set of marks for values used in the template
    33  	marks := make(cty.ValueMarks)
    34  
    35  	for _, part := range e.Parts {
    36  		partVal, partDiags := part.Value(ctx)
    37  		diags = append(diags, partDiags...)
    38  
    39  		if partVal.IsNull() {
    40  			diags = append(diags, &hcl.Diagnostic{
    41  				Severity:    hcl.DiagError,
    42  				Summary:     "Invalid template interpolation value",
    43  				Detail:      "The expression result is null. Cannot include a null value in a string template.",
    44  				Subject:     part.Range().Ptr(),
    45  				Context:     &e.SrcRange,
    46  				Expression:  part,
    47  				EvalContext: ctx,
    48  			})
    49  			continue
    50  		}
    51  
    52  		// Unmark the part and merge its marks into the set
    53  		unmarkedVal, partMarks := partVal.Unmark()
    54  		for k, v := range partMarks {
    55  			marks[k] = v
    56  		}
    57  
    58  		if !partVal.IsKnown() {
    59  			// If any part is unknown then the result as a whole must be
    60  			// unknown too. We'll keep on processing the rest of the parts
    61  			// anyway, because we want to still emit any diagnostics resulting
    62  			// from evaluating those.
    63  			isKnown = false
    64  			continue
    65  		}
    66  
    67  		strVal, err := convert.Convert(unmarkedVal, cty.String)
    68  		if err != nil {
    69  			diags = append(diags, &hcl.Diagnostic{
    70  				Severity: hcl.DiagError,
    71  				Summary:  "Invalid template interpolation value",
    72  				Detail: fmt.Sprintf(
    73  					"Cannot include the given value in a string template: %s.",
    74  					err.Error(),
    75  				),
    76  				Subject:     part.Range().Ptr(),
    77  				Context:     &e.SrcRange,
    78  				Expression:  part,
    79  				EvalContext: ctx,
    80  			})
    81  			continue
    82  		}
    83  
    84  		// If we're just continuing to validate after we found an unknown value
    85  		// then we'll skip appending so that "buf" will contain only the
    86  		// known prefix of the result.
    87  		if isKnown && !diags.HasErrors() {
    88  			buf.WriteString(strVal.AsString())
    89  		}
    90  	}
    91  
    92  	var ret cty.Value
    93  	if !isKnown {
    94  		ret = cty.UnknownVal(cty.String)
    95  		if !diags.HasErrors() { // Invalid input means our partial result buffer is suspect
    96  			if knownPrefix := buf.String(); knownPrefix != "" {
    97  				byteLen := len(knownPrefix)
    98  				// Impose a reasonable upper limit to avoid producing too long a prefix.
    99  				// The 128 B is about 10% of the safety limits in cty's msgpack decoder.
   100  				// @see https://github.com/zclconf/go-cty/blob/v1.13.2/cty/msgpack/unknown.go#L170-L175
   101  				//
   102  				// This operation is safe because StringPrefix removes incomplete trailing grapheme clusters.
   103  				if byteLen > 128 { // arbitrarily-decided threshold
   104  					byteLen = 128
   105  				}
   106  				ret = ret.Refine().StringPrefix(knownPrefix[:byteLen]).NewValue()
   107  			}
   108  		}
   109  	} else {
   110  		ret = cty.StringVal(buf.String())
   111  	}
   112  
   113  	// A template rendering result is never null.
   114  	ret = ret.RefineNotNull()
   115  
   116  	// Apply the full set of marks to the returned value
   117  	return ret.WithMarks(marks), diags
   118  }
   119  
   120  func (e *TemplateExpr) Range() hcl.Range {
   121  	return e.SrcRange
   122  }
   123  
   124  func (e *TemplateExpr) StartRange() hcl.Range {
   125  	return e.Parts[0].StartRange()
   126  }
   127  
   128  // IsStringLiteral returns true if and only if the template consists only of
   129  // single string literal, as would be created for a simple quoted string like
   130  // "foo".
   131  //
   132  // If this function returns true, then calling Value on the same expression
   133  // with a nil EvalContext will return the literal value.
   134  //
   135  // Note that "${"foo"}", "${1}", etc aren't considered literal values for the
   136  // purposes of this method, because the intent of this method is to identify
   137  // situations where the user seems to be explicitly intending literal string
   138  // interpretation, not situations that result in literals as a technicality
   139  // of the template expression unwrapping behavior.
   140  func (e *TemplateExpr) IsStringLiteral() bool {
   141  	if len(e.Parts) != 1 {
   142  		return false
   143  	}
   144  	_, ok := e.Parts[0].(*LiteralValueExpr)
   145  	return ok
   146  }
   147  
   148  // TemplateJoinExpr is used to convert tuples of strings produced by template
   149  // constructs (i.e. for loops) into flat strings, by converting the values
   150  // tos strings and joining them. This AST node is not used directly; it's
   151  // produced as part of the AST of a "for" loop in a template.
   152  type TemplateJoinExpr struct {
   153  	Tuple Expression
   154  }
   155  
   156  func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) {
   157  	w(e.Tuple)
   158  }
   159  
   160  func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
   161  	tuple, diags := e.Tuple.Value(ctx)
   162  
   163  	if tuple.IsNull() {
   164  		// This indicates a bug in the code that constructed the AST.
   165  		panic("TemplateJoinExpr got null tuple")
   166  	}
   167  	if tuple.Type() == cty.DynamicPseudoType {
   168  		return cty.UnknownVal(cty.String), diags
   169  	}
   170  	if !tuple.Type().IsTupleType() {
   171  		// This indicates a bug in the code that constructed the AST.
   172  		panic("TemplateJoinExpr got non-tuple tuple")
   173  	}
   174  	if !tuple.IsKnown() {
   175  		return cty.UnknownVal(cty.String), diags
   176  	}
   177  
   178  	tuple, marks := tuple.Unmark()
   179  	allMarks := []cty.ValueMarks{marks}
   180  	buf := &bytes.Buffer{}
   181  	it := tuple.ElementIterator()
   182  	for it.Next() {
   183  		_, val := it.Element()
   184  
   185  		if val.IsNull() {
   186  			diags = append(diags, &hcl.Diagnostic{
   187  				Severity: hcl.DiagError,
   188  				Summary:  "Invalid template interpolation value",
   189  				Detail: fmt.Sprintf(
   190  					"An iteration result is null. Cannot include a null value in a string template.",
   191  				),
   192  				Subject:     e.Range().Ptr(),
   193  				Expression:  e,
   194  				EvalContext: ctx,
   195  			})
   196  			continue
   197  		}
   198  		if val.Type() == cty.DynamicPseudoType {
   199  			return cty.UnknownVal(cty.String).WithMarks(marks), diags
   200  		}
   201  		strVal, err := convert.Convert(val, cty.String)
   202  		if err != nil {
   203  			diags = append(diags, &hcl.Diagnostic{
   204  				Severity: hcl.DiagError,
   205  				Summary:  "Invalid template interpolation value",
   206  				Detail: fmt.Sprintf(
   207  					"Cannot include one of the interpolation results into the string template: %s.",
   208  					err.Error(),
   209  				),
   210  				Subject:     e.Range().Ptr(),
   211  				Expression:  e,
   212  				EvalContext: ctx,
   213  			})
   214  			continue
   215  		}
   216  		if !val.IsKnown() {
   217  			return cty.UnknownVal(cty.String).WithMarks(marks), diags
   218  		}
   219  
   220  		strVal, strValMarks := strVal.Unmark()
   221  		if len(strValMarks) > 0 {
   222  			allMarks = append(allMarks, strValMarks)
   223  		}
   224  		buf.WriteString(strVal.AsString())
   225  	}
   226  
   227  	return cty.StringVal(buf.String()).WithMarks(allMarks...), diags
   228  }
   229  
   230  func (e *TemplateJoinExpr) Range() hcl.Range {
   231  	return e.Tuple.Range()
   232  }
   233  
   234  func (e *TemplateJoinExpr) StartRange() hcl.Range {
   235  	return e.Tuple.StartRange()
   236  }
   237  
   238  // TemplateWrapExpr is used instead of a TemplateExpr when a template
   239  // consists _only_ of a single interpolation sequence. In that case, the
   240  // template's result is the single interpolation's result, verbatim with
   241  // no type conversions.
   242  type TemplateWrapExpr struct {
   243  	Wrapped Expression
   244  
   245  	SrcRange hcl.Range
   246  }
   247  
   248  func (e *TemplateWrapExpr) walkChildNodes(w internalWalkFunc) {
   249  	w(e.Wrapped)
   250  }
   251  
   252  func (e *TemplateWrapExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
   253  	return e.Wrapped.Value(ctx)
   254  }
   255  
   256  func (e *TemplateWrapExpr) Range() hcl.Range {
   257  	return e.SrcRange
   258  }
   259  
   260  func (e *TemplateWrapExpr) StartRange() hcl.Range {
   261  	return e.SrcRange
   262  }