tfdiags: new package for normalizing error and warning messages
Currently we lean heavily on the Go error type as our primary means of
describing errors, and along with that use several more specialized
implementations of it in different spots for additional capabilities such
as multiple errors in one object, source code range references, etc.
We also have a rather ad-hoc approach of returning an array of warnings
from certain functions along with one or multiple errors.
This rather-disorganized approach makes it hard for us to present
user-facing error messages consistently. As a step towards mitigating
this, package tfdiags provides a model for user-facing error and warning
messages and helper functions for creating them from various other
error and warning types used elsewhere in Terraform.
This mechanism is intended to be used to report errors and warnings where
the audience is the Terraform user, and so it may go a few layers deep
down the call stack into codepaths like config parsing, interpolation, etc
but is primarily a UX concern. The deepest reaches of Terraform core will
continue using "error" as normal, with higher layers preparing error
messages for presentation to the user.
To avoid needing to change the interface of every function that might
generate error diagnostics, the Diagnostics type can be "smuggled" via
an error value through other APIs and then unwrapped at the other end,
though it will lose any naked warnings (without at least one error) along
the way, and so codepaths that are expected to generate warnings
(validation, primarily) should use the concrete Diagnostics type
throughout the call chain.
2017-10-04 19:13:29 -05:00
|
|
|
package tfdiags
|
|
|
|
|
|
|
|
import (
|
2020-03-12 12:45:42 -05:00
|
|
|
"fmt"
|
|
|
|
|
2019-09-09 17:58:44 -05:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
tfdiags: new package for normalizing error and warning messages
Currently we lean heavily on the Go error type as our primary means of
describing errors, and along with that use several more specialized
implementations of it in different spots for additional capabilities such
as multiple errors in one object, source code range references, etc.
We also have a rather ad-hoc approach of returning an array of warnings
from certain functions along with one or multiple errors.
This rather-disorganized approach makes it hard for us to present
user-facing error messages consistently. As a step towards mitigating
this, package tfdiags provides a model for user-facing error and warning
messages and helper functions for creating them from various other
error and warning types used elsewhere in Terraform.
This mechanism is intended to be used to report errors and warnings where
the audience is the Terraform user, and so it may go a few layers deep
down the call stack into codepaths like config parsing, interpolation, etc
but is primarily a UX concern. The deepest reaches of Terraform core will
continue using "error" as normal, with higher layers preparing error
messages for presentation to the user.
To avoid needing to change the interface of every function that might
generate error diagnostics, the Diagnostics type can be "smuggled" via
an error value through other APIs and then unwrapped at the other end,
though it will lose any naked warnings (without at least one error) along
the way, and so codepaths that are expected to generate warnings
(validation, primarily) should use the concrete Diagnostics type
throughout the call chain.
2017-10-04 19:13:29 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// hclDiagnostic is a Diagnostic implementation that wraps a HCL Diagnostic
|
|
|
|
type hclDiagnostic struct {
|
|
|
|
diag *hcl.Diagnostic
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Diagnostic = hclDiagnostic{}
|
|
|
|
|
|
|
|
func (d hclDiagnostic) Severity() Severity {
|
|
|
|
switch d.diag.Severity {
|
|
|
|
case hcl.DiagWarning:
|
|
|
|
return Warning
|
|
|
|
default:
|
|
|
|
return Error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d hclDiagnostic) Description() Description {
|
|
|
|
return Description{
|
|
|
|
Summary: d.diag.Summary,
|
|
|
|
Detail: d.diag.Detail,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d hclDiagnostic) Source() Source {
|
|
|
|
var ret Source
|
|
|
|
if d.diag.Subject != nil {
|
2017-10-10 18:19:36 -05:00
|
|
|
rng := SourceRangeFromHCL(*d.diag.Subject)
|
|
|
|
ret.Subject = &rng
|
tfdiags: new package for normalizing error and warning messages
Currently we lean heavily on the Go error type as our primary means of
describing errors, and along with that use several more specialized
implementations of it in different spots for additional capabilities such
as multiple errors in one object, source code range references, etc.
We also have a rather ad-hoc approach of returning an array of warnings
from certain functions along with one or multiple errors.
This rather-disorganized approach makes it hard for us to present
user-facing error messages consistently. As a step towards mitigating
this, package tfdiags provides a model for user-facing error and warning
messages and helper functions for creating them from various other
error and warning types used elsewhere in Terraform.
This mechanism is intended to be used to report errors and warnings where
the audience is the Terraform user, and so it may go a few layers deep
down the call stack into codepaths like config parsing, interpolation, etc
but is primarily a UX concern. The deepest reaches of Terraform core will
continue using "error" as normal, with higher layers preparing error
messages for presentation to the user.
To avoid needing to change the interface of every function that might
generate error diagnostics, the Diagnostics type can be "smuggled" via
an error value through other APIs and then unwrapped at the other end,
though it will lose any naked warnings (without at least one error) along
the way, and so codepaths that are expected to generate warnings
(validation, primarily) should use the concrete Diagnostics type
throughout the call chain.
2017-10-04 19:13:29 -05:00
|
|
|
}
|
|
|
|
if d.diag.Context != nil {
|
2017-10-10 18:19:36 -05:00
|
|
|
rng := SourceRangeFromHCL(*d.diag.Context)
|
|
|
|
ret.Context = &rng
|
tfdiags: new package for normalizing error and warning messages
Currently we lean heavily on the Go error type as our primary means of
describing errors, and along with that use several more specialized
implementations of it in different spots for additional capabilities such
as multiple errors in one object, source code range references, etc.
We also have a rather ad-hoc approach of returning an array of warnings
from certain functions along with one or multiple errors.
This rather-disorganized approach makes it hard for us to present
user-facing error messages consistently. As a step towards mitigating
this, package tfdiags provides a model for user-facing error and warning
messages and helper functions for creating them from various other
error and warning types used elsewhere in Terraform.
This mechanism is intended to be used to report errors and warnings where
the audience is the Terraform user, and so it may go a few layers deep
down the call stack into codepaths like config parsing, interpolation, etc
but is primarily a UX concern. The deepest reaches of Terraform core will
continue using "error" as normal, with higher layers preparing error
messages for presentation to the user.
To avoid needing to change the interface of every function that might
generate error diagnostics, the Diagnostics type can be "smuggled" via
an error value through other APIs and then unwrapped at the other end,
though it will lose any naked warnings (without at least one error) along
the way, and so codepaths that are expected to generate warnings
(validation, primarily) should use the concrete Diagnostics type
throughout the call chain.
2017-10-04 19:13:29 -05:00
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
2017-10-10 18:19:36 -05:00
|
|
|
|
2018-10-17 19:47:13 -05:00
|
|
|
func (d hclDiagnostic) FromExpr() *FromExpr {
|
|
|
|
if d.diag.Expression == nil || d.diag.EvalContext == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &FromExpr{
|
|
|
|
Expression: d.diag.Expression,
|
|
|
|
EvalContext: d.diag.EvalContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-10 18:19:36 -05:00
|
|
|
// SourceRangeFromHCL constructs a SourceRange from the corresponding range
|
|
|
|
// type within the HCL package.
|
|
|
|
func SourceRangeFromHCL(hclRange hcl.Range) SourceRange {
|
|
|
|
return SourceRange{
|
|
|
|
Filename: hclRange.Filename,
|
|
|
|
Start: SourcePos{
|
|
|
|
Line: hclRange.Start.Line,
|
|
|
|
Column: hclRange.Start.Column,
|
|
|
|
Byte: hclRange.Start.Byte,
|
|
|
|
},
|
|
|
|
End: SourcePos{
|
|
|
|
Line: hclRange.End.Line,
|
|
|
|
Column: hclRange.End.Column,
|
|
|
|
Byte: hclRange.End.Byte,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToHCL constructs a HCL Range from the receiving SourceRange. This is the
|
|
|
|
// opposite of SourceRangeFromHCL.
|
|
|
|
func (r SourceRange) ToHCL() hcl.Range {
|
|
|
|
return hcl.Range{
|
|
|
|
Filename: r.Filename,
|
|
|
|
Start: hcl.Pos{
|
|
|
|
Line: r.Start.Line,
|
|
|
|
Column: r.Start.Column,
|
|
|
|
Byte: r.Start.Byte,
|
|
|
|
},
|
|
|
|
End: hcl.Pos{
|
|
|
|
Line: r.End.Line,
|
|
|
|
Column: r.End.Column,
|
|
|
|
Byte: r.End.Byte,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2020-03-12 12:45:42 -05:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|