tfdiags: Diagnostics.ToHCL

Most of the time we're converting from HCL diagnostics to tfdiags as we
expose diagnostics directly from HCL, but occasionally we need to to the
reverse.

For example, our configs package uses hcl.Diagnostics by convention
because it's primarily working with HCL, but sometimes it interacts with
functions elsewhere (like in the "addrs" package) that return
tfdiags.Diagnostics, where they need to be adapted to return in an HCL
shape.

This should be used with some care because, similar to Diagnostics.ForRPC,
it forces immediate flattening of all of the diagnostics to a single
type and so can potentially lose internal tracking information that
appears in other tfdiags.Diagnostic information, such as the additional
metadata tracked in the ConsolidateWarnings result to allow later
appending to existing groups.
This commit is contained in:
Martin Atkins 2020-03-12 10:45:42 -07:00
parent 1c78b26012
commit a851566c56
2 changed files with 153 additions and 0 deletions

View File

@ -1,6 +1,8 @@
package tfdiags
import (
"fmt"
"github.com/hashicorp/hcl/v2"
)
@ -85,3 +87,55 @@ func (r SourceRange) ToHCL() hcl.Range {
},
}
}
// ToHCL constructs a hcl.Diagnostics containing the same diagnostic messages
// as the receiving tfdiags.Diagnostics.
//
// This conversion preserves the data that HCL diagnostics are able to
// preserve but would be lossy in a round trip from tfdiags to HCL and then
// back to tfdiags, because it will lose the specific type information of
// the source diagnostics. In most cases this will not be a significant
// problem, but could produce an awkward result in some special cases such
// as converting the result of ConsolidateWarnings, which will force the
// resulting warning groups to be flattened early.
func (d Diagnostics) ToHCL() hcl.Diagnostics {
if len(d) == 0 {
return nil
}
ret := make(hcl.Diagnostics, len(d))
for i, diag := range d {
severity := diag.Severity()
desc := diag.Description()
source := diag.Source()
fromExpr := diag.FromExpr()
hclDiag := &hcl.Diagnostic{
Summary: desc.Summary,
Detail: desc.Detail,
}
switch severity {
case Warning:
hclDiag.Severity = hcl.DiagWarning
case Error:
hclDiag.Severity = hcl.DiagError
default:
// The above should always be exhaustive for all of the valid
// Severity values in this package.
panic(fmt.Sprintf("unknown diagnostic severity %s", severity))
}
if source.Subject != nil {
hclDiag.Subject = source.Subject.ToHCL().Ptr()
}
if source.Context != nil {
hclDiag.Context = source.Context.ToHCL().Ptr()
}
if fromExpr != nil {
hclDiag.Expression = fromExpr.Expression
hclDiag.EvalContext = fromExpr.EvalContext
}
ret[i] = hclDiag
}
return ret
}

99
tfdiags/hcl_test.go Normal file
View File

@ -0,0 +1,99 @@
package tfdiags
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
func TestDiagnosticsToHCL(t *testing.T) {
var diags Diagnostics
diags = diags.Append(Sourceless(
Error,
"A sourceless diagnostic",
"...that has a detail",
))
diags = diags.Append(fmt.Errorf("a diagnostic promoted from an error"))
diags = diags.Append(SimpleWarning("A diagnostic from a simple warning"))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "A diagnostic from HCL",
Detail: "...that has a detail and source information",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
End: hcl.Pos{Line: 1, Column: 3, Byte: 2},
},
Context: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
},
EvalContext: &hcl.EvalContext{},
Expression: &fakeHCLExpression{},
})
got := diags.ToHCL()
want := hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "A sourceless diagnostic",
Detail: "...that has a detail",
},
{
Severity: hcl.DiagError,
Summary: "a diagnostic promoted from an error",
},
{
Severity: hcl.DiagWarning,
Summary: "A diagnostic from a simple warning",
},
{
Severity: hcl.DiagWarning,
Summary: "A diagnostic from HCL",
Detail: "...that has a detail and source information",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
End: hcl.Pos{Line: 1, Column: 3, Byte: 2},
},
Context: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
},
EvalContext: &hcl.EvalContext{},
Expression: &fakeHCLExpression{},
},
}
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(hcl.EvalContext{})); diff != "" {
t.Errorf("incorrect result\n%s", diff)
}
}
// We have this here just to give us something easy to compare in the test
// above, because we only care that the expression passes through, not about
// how exactly it is shaped.
type fakeHCLExpression struct {
}
func (e *fakeHCLExpression) Range() hcl.Range {
return hcl.Range{}
}
func (e *fakeHCLExpression) StartRange() hcl.Range {
return hcl.Range{}
}
func (e *fakeHCLExpression) Variables() []hcl.Traversal {
return nil
}
func (e *fakeHCLExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.DynamicVal, nil
}