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 }