github.com/hashicorp/hcl/v2@v2.20.0/hclsyntax/expression_template_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hclsyntax 5 6 import ( 7 "strings" 8 "testing" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/zclconf/go-cty/cty" 12 ) 13 14 func TestTemplateExprParseAndValue(t *testing.T) { 15 // This is a combo test that exercises both the parser and the Value 16 // method, with the focus on the latter but indirectly testing the former. 17 tests := []struct { 18 input string 19 ctx *hcl.EvalContext 20 want cty.Value 21 diagCount int 22 }{ 23 { 24 `1`, 25 nil, 26 cty.StringVal("1"), 27 0, 28 }, 29 { 30 `(1)`, 31 nil, 32 cty.StringVal("(1)"), 33 0, 34 }, 35 { 36 `true`, 37 nil, 38 cty.StringVal("true"), 39 0, 40 }, 41 { 42 ` 43 hello world 44 `, 45 nil, 46 cty.StringVal("\nhello world\n"), 47 0, 48 }, 49 { 50 `hello ${"world"}`, 51 nil, 52 cty.StringVal("hello world"), 53 0, 54 }, 55 { 56 `hello\nworld`, // backslash escapes not supported in bare templates 57 nil, 58 cty.StringVal("hello\\nworld"), 59 0, 60 }, 61 { 62 `hello ${12.5}`, 63 nil, 64 cty.StringVal("hello 12.5"), 65 0, 66 }, 67 { 68 `silly ${"${"nesting"}"}`, 69 nil, 70 cty.StringVal("silly nesting"), 71 0, 72 }, 73 { 74 `silly ${"${true}"}`, 75 nil, 76 cty.StringVal("silly true"), 77 0, 78 }, 79 { 80 `hello $${escaped}`, 81 nil, 82 cty.StringVal("hello ${escaped}"), 83 0, 84 }, 85 { 86 `hello $$nonescape`, 87 nil, 88 cty.StringVal("hello $$nonescape"), 89 0, 90 }, 91 { 92 `hello %${"world"}`, 93 nil, 94 cty.StringVal("hello %world"), 95 0, 96 }, 97 { 98 `${true}`, 99 nil, 100 cty.True, // any single expression is unwrapped without stringification 101 0, 102 }, 103 { 104 `trim ${~ "trim"}`, 105 nil, 106 cty.StringVal("trimtrim"), 107 0, 108 }, 109 { 110 `${"trim" ~} trim`, 111 nil, 112 cty.StringVal("trimtrim"), 113 0, 114 }, 115 { 116 `trim 117 ${~"trim"~} 118 trim`, 119 nil, 120 cty.StringVal("trimtrimtrim"), 121 0, 122 }, 123 { 124 ` ${~ true ~} `, 125 nil, 126 cty.StringVal("true"), // can't trim space to reduce to a single expression 127 0, 128 }, 129 { 130 `${"hello "}${~"trim"~}${" hello"}`, 131 nil, 132 cty.StringVal("hello trim hello"), // trimming can't reach into a neighboring interpolation 133 0, 134 }, 135 { 136 `${true}${~"trim"~}${true}`, 137 nil, 138 cty.StringVal("truetrimtrue"), // trimming is no-op of neighbors aren't literal strings 139 0, 140 }, 141 142 { 143 `%{ if true ~} hello %{~ endif }`, 144 nil, 145 cty.StringVal("hello"), 146 0, 147 }, 148 { 149 `%{ if false ~} hello %{~ endif}`, 150 nil, 151 cty.StringVal(""), 152 0, 153 }, 154 { 155 `%{ if true ~} hello %{~ else ~} goodbye %{~ endif }`, 156 nil, 157 cty.StringVal("hello"), 158 0, 159 }, 160 { 161 `%{ if false ~} hello %{~ else ~} goodbye %{~ endif }`, 162 nil, 163 cty.StringVal("goodbye"), 164 0, 165 }, 166 { 167 `%{ if true ~} %{~ if false ~} hello %{~ else ~} goodbye %{~ endif ~} %{~ endif }`, 168 nil, 169 cty.StringVal("goodbye"), 170 0, 171 }, 172 { 173 `%{ if false ~} %{~ if false ~} hello %{~ else ~} goodbye %{~ endif ~} %{~ endif }`, 174 nil, 175 cty.StringVal(""), 176 0, 177 }, 178 { 179 `%{ of true ~} hello %{~ endif}`, 180 nil, 181 cty.UnknownVal(cty.String).RefineNotNull(), 182 2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected 183 }, 184 { 185 `%{ for v in ["a", "b", "c"] }${v}%{ endfor }`, 186 nil, 187 cty.StringVal("abc"), 188 0, 189 }, 190 { 191 `%{ for v in ["a", "b", "c"] } ${v} %{ endfor }`, 192 nil, 193 cty.StringVal(" a b c "), 194 0, 195 }, 196 { 197 `%{ for v in ["a", "b", "c"] ~} ${v} %{~ endfor }`, 198 nil, 199 cty.StringVal("abc"), 200 0, 201 }, 202 { 203 `%{ for v in [] }${v}%{ endfor }`, 204 nil, 205 cty.StringVal(""), 206 0, 207 }, 208 { 209 `%{ for i, v in ["a", "b", "c"] }${i}${v}%{ endfor }`, 210 nil, 211 cty.StringVal("0a1b2c"), 212 0, 213 }, 214 { 215 `%{ for k, v in {"A" = "a", "B" = "b", "C" = "c"} }${k}${v}%{ endfor }`, 216 nil, 217 cty.StringVal("AaBbCc"), 218 0, 219 }, 220 { 221 `%{ for v in ["a", "b", "c"] }${v}${nl}%{ endfor }`, 222 &hcl.EvalContext{ 223 Variables: map[string]cty.Value{ 224 "nl": cty.StringVal("\n"), 225 }, 226 }, 227 cty.StringVal("a\nb\nc\n"), 228 0, 229 }, 230 { 231 `\n`, // backslash escapes are not interpreted in template literals 232 nil, 233 cty.StringVal("\\n"), 234 0, 235 }, 236 { 237 `\uu1234`, // backslash escapes are not interpreted in template literals 238 nil, // (this is intentionally an invalid one to ensure we don't produce an error) 239 cty.StringVal("\\uu1234"), 240 0, 241 }, 242 { 243 `$`, 244 nil, 245 cty.StringVal("$"), 246 0, 247 }, 248 { 249 `$$`, 250 nil, 251 cty.StringVal("$$"), 252 0, 253 }, 254 { 255 `%`, 256 nil, 257 cty.StringVal("%"), 258 0, 259 }, 260 { 261 `%%`, 262 nil, 263 cty.StringVal("%%"), 264 0, 265 }, 266 { 267 `hello %%{ if true }world%%{ endif }`, 268 nil, 269 cty.StringVal(`hello %{ if true }world%{ endif }`), 270 0, 271 }, 272 { 273 `hello $%{ if true }world%{ endif }`, 274 nil, 275 cty.StringVal("hello $world"), 276 0, 277 }, 278 { 279 `%{ endif }`, 280 nil, 281 cty.UnknownVal(cty.String).RefineNotNull(), 282 1, // Unexpected endif directive 283 }, 284 { 285 `%{ endfor }`, 286 nil, 287 cty.UnknownVal(cty.String).RefineNotNull(), 288 1, // Unexpected endfor directive 289 }, 290 { // can preserve a static prefix as a refinement of an unknown result 291 `test_${unknown}`, 292 &hcl.EvalContext{ 293 Variables: map[string]cty.Value{ 294 "unknown": cty.UnknownVal(cty.String), 295 }, 296 }, 297 cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_").NewValue(), 298 0, 299 }, 300 { // can preserve a dynamic known prefix as a refinement of an unknown result 301 `test_${known}_${unknown}`, 302 &hcl.EvalContext{ 303 Variables: map[string]cty.Value{ 304 "known": cty.StringVal("known"), 305 "unknown": cty.UnknownVal(cty.String), 306 }, 307 }, 308 cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_known_").NewValue(), 309 0, 310 }, 311 { // can preserve a static prefix as a refinement, but the length is limited to 128 B 312 strings.Repeat("_", 130) + `${unknown}`, 313 &hcl.EvalContext{ 314 Variables: map[string]cty.Value{ 315 "unknown": cty.UnknownVal(cty.String), 316 }, 317 }, 318 cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull(strings.Repeat("_", 128)).NewValue(), 319 0, 320 }, 321 { // marks from uninterpolated values are ignored 322 `hello%{ if false } ${target}%{ endif }`, 323 &hcl.EvalContext{ 324 Variables: map[string]cty.Value{ 325 "target": cty.StringVal("world").Mark("sensitive"), 326 }, 327 }, 328 cty.StringVal("hello"), 329 0, 330 }, 331 { // marks from interpolated values are passed through 332 `${greeting} ${target}`, 333 &hcl.EvalContext{ 334 Variables: map[string]cty.Value{ 335 "greeting": cty.StringVal("hello").Mark("english"), 336 "target": cty.StringVal("world").Mark("sensitive"), 337 }, 338 }, 339 cty.StringVal("hello world").WithMarks(cty.NewValueMarks("english", "sensitive")), 340 0, 341 }, 342 { // can use marks by traversing complex values 343 `Authenticate with "${secrets.passphrase}"`, 344 &hcl.EvalContext{ 345 Variables: map[string]cty.Value{ 346 "secrets": cty.MapVal(map[string]cty.Value{ 347 "passphrase": cty.StringVal("my voice is my passport").Mark("sensitive"), 348 }).Mark("sensitive"), 349 }, 350 }, 351 cty.StringVal(`Authenticate with "my voice is my passport"`).WithMarks(cty.NewValueMarks("sensitive")), 352 0, 353 }, 354 { // can loop over marked collections 355 `%{ for s in secrets }${s}%{ endfor }`, 356 &hcl.EvalContext{ 357 Variables: map[string]cty.Value{ 358 "secrets": cty.ListVal([]cty.Value{ 359 cty.StringVal("foo"), 360 cty.StringVal("bar"), 361 cty.StringVal("baz"), 362 }).Mark("sensitive"), 363 }, 364 }, 365 cty.StringVal("foobarbaz").Mark("sensitive"), 366 0, 367 }, 368 { // marks on individual elements propagate to the result 369 `%{ for s in secrets }${s}%{ endfor }`, 370 &hcl.EvalContext{ 371 Variables: map[string]cty.Value{ 372 "secrets": cty.ListVal([]cty.Value{ 373 cty.StringVal("foo"), 374 cty.StringVal("bar").Mark("sensitive"), 375 cty.StringVal("baz"), 376 }), 377 }, 378 }, 379 cty.StringVal("foobarbaz").Mark("sensitive"), 380 0, 381 }, 382 { // lots of marks! 383 `%{ for s in secrets }${s}%{ endfor }`, 384 &hcl.EvalContext{ 385 Variables: map[string]cty.Value{ 386 "secrets": cty.ListVal([]cty.Value{ 387 cty.StringVal("foo").Mark("x"), 388 cty.StringVal("bar").Mark("y"), 389 cty.StringVal("baz").Mark("z"), 390 }).Mark("x"), // second instance of x 391 }, 392 }, 393 cty.StringVal("foobarbaz").WithMarks(cty.NewValueMarks("x", "y", "z")), 394 0, 395 }, 396 { // marks from unknown values are maintained 397 `test_${target}`, 398 &hcl.EvalContext{ 399 Variables: map[string]cty.Value{ 400 "target": cty.UnknownVal(cty.String).Mark("sensitive"), 401 }, 402 }, 403 cty.UnknownVal(cty.String).Mark("sensitive").Refine().NotNull().StringPrefixFull("test_").NewValue(), 404 0, 405 }, 406 } 407 408 for _, test := range tests { 409 t.Run(test.input, func(t *testing.T) { 410 expr, parseDiags := ParseTemplate([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) 411 412 // We'll skip evaluating if there were parse errors because it 413 // isn't reasonable to evaluate a syntactically-invalid template; 414 // it'll produce strange results that we don't care about. 415 got := test.want 416 var valDiags hcl.Diagnostics 417 if !parseDiags.HasErrors() { 418 got, valDiags = expr.Value(test.ctx) 419 } 420 421 diagCount := len(parseDiags) + len(valDiags) 422 423 if diagCount != test.diagCount { 424 t.Errorf("wrong number of diagnostics %d; want %d", diagCount, test.diagCount) 425 for _, diag := range parseDiags { 426 t.Logf(" - %s", diag.Error()) 427 } 428 for _, diag := range valDiags { 429 t.Logf(" - %s", diag.Error()) 430 } 431 } 432 433 if !got.RawEquals(test.want) { 434 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 435 } 436 }) 437 } 438 439 } 440 441 func TestTemplateExprIsStringLiteral(t *testing.T) { 442 tests := map[string]bool{ 443 // A simple string value is a string literal 444 "a": true, 445 446 // Strings containing escape characters or escape sequences are 447 // tokenized into multiple string literals, but this should be 448 // corrected by the parser 449 "a$b": true, 450 "a%%b": true, 451 "a\nb": true, 452 "a$${\"b\"}": true, 453 454 // Wrapped values (HIL-like) are not treated as string literals for 455 // legacy reasons 456 "${1}": false, 457 "${\"b\"}": false, 458 459 // Even template expressions containing only literal values do not 460 // count as string literals 461 "a${1}": false, 462 "a${\"b\"}": false, 463 } 464 for input, want := range tests { 465 t.Run(input, func(t *testing.T) { 466 expr, diags := ParseTemplate([]byte(input), "", hcl.InitialPos) 467 if len(diags) != 0 { 468 t.Fatalf("unexpected diags: %s", diags.Error()) 469 } 470 471 if tmplExpr, ok := expr.(*TemplateExpr); ok { 472 got := tmplExpr.IsStringLiteral() 473 474 if got != want { 475 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) 476 } 477 } 478 }) 479 } 480 }