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  }