diff --git a/tfdiags/hcl.go b/tfdiags/hcl.go index 8c781611a..37fb0d1ae 100644 --- a/tfdiags/hcl.go +++ b/tfdiags/hcl.go @@ -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 +} diff --git a/tfdiags/hcl_test.go b/tfdiags/hcl_test.go new file mode 100644 index 000000000..784562f9b --- /dev/null +++ b/tfdiags/hcl_test.go @@ -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 +}