Skip to content

Commit

Permalink
hclsyntax: TemplateExpr can refine its unknown results
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed May 31, 2023
1 parent adb8823 commit e0058a2
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 14 deletions.
23 changes: 17 additions & 6 deletions hclsyntax/expression_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)

if partVal.IsNull() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"The expression result is null. Cannot include a null value in a string template.",
),
Severity: hcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: "The expression result is null. Cannot include a null value in a string template.",
Subject: part.Range().Ptr(),
Context: &e.SrcRange,
Expression: part,
Expand Down Expand Up @@ -83,16 +81,29 @@ func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
continue
}

buf.WriteString(strVal.AsString())
// If we're just continuing to validate after we found an unknown value
// then we'll skip appending so that "buf" will contain only the
// known prefix of the result.
if isKnown && !diags.HasErrors() {
buf.WriteString(strVal.AsString())
}
}

var ret cty.Value
if !isKnown {
ret = cty.UnknownVal(cty.String)
if !diags.HasErrors() { // Invalid input means our partial result buffer is suspect
if knownPrefix := buf.String(); knownPrefix != "" {
ret = ret.Refine().StringPrefix(knownPrefix).NewValue()
}
}
} else {
ret = cty.StringVal(buf.String())
}

// A template rendering result is never null.
ret = ret.RefineNotNull()

// Apply the full set of marks to the returned value
return ret.WithMarks(marks), diags
}
Expand Down
38 changes: 33 additions & 5 deletions hclsyntax/expression_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ trim`,
{
`%{ of true ~} hello %{~ endif}`,
nil,
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String).RefineNotNull(),
2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected
},
{
Expand Down Expand Up @@ -277,15 +277,36 @@ trim`,
{
`%{ endif }`,
nil,
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String).RefineNotNull(),
1, // Unexpected endif directive
},
{
`%{ endfor }`,
nil,
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String).RefineNotNull(),
1, // Unexpected endfor directive
},
{ // can preserve a static prefix as a refinement of an unknown result
`test_${unknown}`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.String),
},
},
cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_").NewValue(),
0,
},
{ // can preserve a dynamic known prefix as a refinement of an unknown result
`test_${known}_${unknown}`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"known": cty.StringVal("known"),
"unknown": cty.UnknownVal(cty.String),
},
},
cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_known_").NewValue(),
0,
},
{ // marks from uninterpolated values are ignored
`hello%{ if false } ${target}%{ endif }`,
&hcl.EvalContext{
Expand Down Expand Up @@ -368,7 +389,7 @@ trim`,
"target": cty.UnknownVal(cty.String).Mark("sensitive"),
},
},
cty.UnknownVal(cty.String).Mark("sensitive"),
cty.UnknownVal(cty.String).Mark("sensitive").Refine().NotNull().StringPrefixFull("test_").NewValue(),
0,
},
}
Expand All @@ -377,7 +398,14 @@ trim`,
t.Run(test.input, func(t *testing.T) {
expr, parseDiags := ParseTemplate([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})

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

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

Expand Down
6 changes: 3 additions & 3 deletions json/structure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,7 @@ func TestExpressionValue_Diags(t *testing.T) {
{
name: "string: unhappy",
src: `{"v": "happy ${UNKNOWN}"}`,
expected: cty.UnknownVal(cty.String),
expected: cty.UnknownVal(cty.String).RefineNotNull(),
error: "Unknown variable",
},
{
Expand All @@ -1447,7 +1447,7 @@ func TestExpressionValue_Diags(t *testing.T) {
name: "object_val: unhappy",
src: `{"v": {"key": "happy ${UNKNOWN}"}}`,
expected: cty.ObjectVal(map[string]cty.Value{
"key": cty.UnknownVal(cty.String),
"key": cty.UnknownVal(cty.String).RefineNotNull(),
}),
error: "Unknown variable",
},
Expand All @@ -1472,7 +1472,7 @@ func TestExpressionValue_Diags(t *testing.T) {
{
name: "array: unhappy",
src: `{"v": ["happy ${UNKNOWN}"]}`,
expected: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
expected: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String).RefineNotNull()}),
error: "Unknown variable",
},
}
Expand Down

0 comments on commit e0058a2

Please sign in to comment.