Skip to content

Commit e0058a2

Browse files
hclsyntax: TemplateExpr can refine its unknown results
If we encounter an interpolated unknown value during template rendering, we can report the partial buffer we've completed so far as the refined prefix of the resulting unknown value, which can then potentially allow downstream comparisons to produce a known false result instead of unknown if the prefix is sufficient to satisfy them.
1 parent adb8823 commit e0058a2

File tree

3 files changed

+53
-14
lines changed

3 files changed

+53
-14
lines changed

hclsyntax/expression_template.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
3838

3939
if partVal.IsNull() {
4040
diags = append(diags, &hcl.Diagnostic{
41-
Severity: hcl.DiagError,
42-
Summary: "Invalid template interpolation value",
43-
Detail: fmt.Sprintf(
44-
"The expression result is null. Cannot include a null value in a string template.",
45-
),
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.",
4644
Subject: part.Range().Ptr(),
4745
Context: &e.SrcRange,
4846
Expression: part,
@@ -83,16 +81,29 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
8381
continue
8482
}
8583

86-
buf.WriteString(strVal.AsString())
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+
}
8790
}
8891

8992
var ret cty.Value
9093
if !isKnown {
9194
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+
ret = ret.Refine().StringPrefix(knownPrefix).NewValue()
98+
}
99+
}
92100
} else {
93101
ret = cty.StringVal(buf.String())
94102
}
95103

104+
// A template rendering result is never null.
105+
ret = ret.RefineNotNull()
106+
96107
// Apply the full set of marks to the returned value
97108
return ret.WithMarks(marks), diags
98109
}

hclsyntax/expression_template_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ trim`,
177177
{
178178
`%{ of true ~} hello %{~ endif}`,
179179
nil,
180-
cty.UnknownVal(cty.String),
180+
cty.UnknownVal(cty.String).RefineNotNull(),
181181
2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected
182182
},
183183
{
@@ -277,15 +277,36 @@ trim`,
277277
{
278278
`%{ endif }`,
279279
nil,
280-
cty.UnknownVal(cty.String),
280+
cty.UnknownVal(cty.String).RefineNotNull(),
281281
1, // Unexpected endif directive
282282
},
283283
{
284284
`%{ endfor }`,
285285
nil,
286-
cty.UnknownVal(cty.String),
286+
cty.UnknownVal(cty.String).RefineNotNull(),
287287
1, // Unexpected endfor directive
288288
},
289+
{ // can preserve a static prefix as a refinement of an unknown result
290+
`test_${unknown}`,
291+
&hcl.EvalContext{
292+
Variables: map[string]cty.Value{
293+
"unknown": cty.UnknownVal(cty.String),
294+
},
295+
},
296+
cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_").NewValue(),
297+
0,
298+
},
299+
{ // can preserve a dynamic known prefix as a refinement of an unknown result
300+
`test_${known}_${unknown}`,
301+
&hcl.EvalContext{
302+
Variables: map[string]cty.Value{
303+
"known": cty.StringVal("known"),
304+
"unknown": cty.UnknownVal(cty.String),
305+
},
306+
},
307+
cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_known_").NewValue(),
308+
0,
309+
},
289310
{ // marks from uninterpolated values are ignored
290311
`hello%{ if false } ${target}%{ endif }`,
291312
&hcl.EvalContext{
@@ -368,7 +389,7 @@ trim`,
368389
"target": cty.UnknownVal(cty.String).Mark("sensitive"),
369390
},
370391
},
371-
cty.UnknownVal(cty.String).Mark("sensitive"),
392+
cty.UnknownVal(cty.String).Mark("sensitive").Refine().NotNull().StringPrefixFull("test_").NewValue(),
372393
0,
373394
},
374395
}
@@ -377,7 +398,14 @@ trim`,
377398
t.Run(test.input, func(t *testing.T) {
378399
expr, parseDiags := ParseTemplate([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
379400

380-
got, valDiags := expr.Value(test.ctx)
401+
// We'll skip evaluating if there were parse errors because it
402+
// isn't reasonable to evaluate a syntactically-invalid template;
403+
// it'll produce strange results that we don't care about.
404+
got := test.want
405+
var valDiags hcl.Diagnostics
406+
if !parseDiags.HasErrors() {
407+
got, valDiags = expr.Value(test.ctx)
408+
}
381409

382410
diagCount := len(parseDiags) + len(valDiags)
383411

json/structure_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,7 +1433,7 @@ func TestExpressionValue_Diags(t *testing.T) {
14331433
{
14341434
name: "string: unhappy",
14351435
src: `{"v": "happy ${UNKNOWN}"}`,
1436-
expected: cty.UnknownVal(cty.String),
1436+
expected: cty.UnknownVal(cty.String).RefineNotNull(),
14371437
error: "Unknown variable",
14381438
},
14391439
{
@@ -1447,7 +1447,7 @@ func TestExpressionValue_Diags(t *testing.T) {
14471447
name: "object_val: unhappy",
14481448
src: `{"v": {"key": "happy ${UNKNOWN}"}}`,
14491449
expected: cty.ObjectVal(map[string]cty.Value{
1450-
"key": cty.UnknownVal(cty.String),
1450+
"key": cty.UnknownVal(cty.String).RefineNotNull(),
14511451
}),
14521452
error: "Unknown variable",
14531453
},
@@ -1472,7 +1472,7 @@ func TestExpressionValue_Diags(t *testing.T) {
14721472
{
14731473
name: "array: unhappy",
14741474
src: `{"v": ["happy ${UNKNOWN}"]}`,
1475-
expected: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
1475+
expected: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String).RefineNotNull()}),
14761476
error: "Unknown variable",
14771477
},
14781478
}

0 commit comments

Comments
 (0)