mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
tfdiags: Expose the "extra information" concept from HCL
HCL's diagnostic model now includes the idea of "extra information" which works by attaching an initially-opaque interface value to each diagnostic and then asking callers to type-assert against that value to sniff for particular interfaces in order to discover additional machine-readable context about a certain diagnostic message. This commit echoes that idea into our tfdiags API, for now only for diagnostics that are backed by an hcl.Diagnostic. All other implementations of the diagnostic interface just always return nil, which means they never carry any "extra information". As is typical for our wrapping abstraction, we have here also a modified copy of HCL's helper function for conveniently probing a diagnostic for information of a particular type, designed to work with our diagnostic interface instead of HCL's concrete diagnostic type.
This commit is contained in:
parent
4927d51224
commit
8405f46bc5
@ -76,3 +76,7 @@ func (d wrappedDiagnostic) Source() tfdiags.Source {
|
||||
func (d wrappedDiagnostic) FromExpr() *tfdiags.FromExpr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d wrappedDiagnostic) ExtraInfo() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
@ -119,6 +119,10 @@ func (wg *warningGroup) FromExpr() *FromExpr {
|
||||
return wg.Warnings[0].FromExpr()
|
||||
}
|
||||
|
||||
func (wg *warningGroup) ExtraInfo() interface{} {
|
||||
return wg.Warnings[0].ExtraInfo()
|
||||
}
|
||||
|
||||
func (wg *warningGroup) Append(diag Diagnostic) {
|
||||
if diag.Severity() != Warning {
|
||||
panic("can't append a non-warning diagnostic to a warningGroup")
|
||||
|
@ -15,6 +15,13 @@ type Diagnostic interface {
|
||||
// available. Returns nil if the diagnostic is not related to an
|
||||
// expression evaluation.
|
||||
FromExpr() *FromExpr
|
||||
|
||||
// ExtraInfo returns the raw extra information value. This is a low-level
|
||||
// API which requires some work on the part of the caller to properly
|
||||
// access associated information, so in most cases it'll be more convienient
|
||||
// to use the package-level ExtraInfo function to try to unpack a particular
|
||||
// specialized interface from this value.
|
||||
ExtraInfo() interface{}
|
||||
}
|
||||
|
||||
type Severity rune
|
||||
|
@ -31,3 +31,7 @@ func (d diagnosticBase) Source() Source {
|
||||
func (d diagnosticBase) FromExpr() *FromExpr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d diagnosticBase) ExtraInfo() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
103
internal/tfdiags/diagnostic_extra.go
Normal file
103
internal/tfdiags/diagnostic_extra.go
Normal file
@ -0,0 +1,103 @@
|
||||
package tfdiags
|
||||
|
||||
// This "Extra" idea is something we've inherited from HCL's diagnostic model,
|
||||
// and so it's primarily to expose that functionality from wrapped HCL
|
||||
// diagnostics but other diagnostic types could potentially implement this
|
||||
// protocol too, if needed.
|
||||
|
||||
// ExtraInfo tries to retrieve extra information of interface type T from
|
||||
// the given diagnostic.
|
||||
//
|
||||
// "Extra information" is situation-specific additional contextual data which
|
||||
// might allow for some special tailored reporting of particular
|
||||
// diagnostics in the UI. Conventionally the extra information is provided
|
||||
// as a hidden type that implements one or more interfaces which a caller
|
||||
// can pass as type parameter T to retrieve a value of that type when the
|
||||
// diagnostic has such an implementation.
|
||||
//
|
||||
// If the given diagnostic's extra value has an implementation of interface T
|
||||
// then ExtraInfo returns a non-nil interface value. If there is no such
|
||||
// implementation, ExtraInfo returns a nil T.
|
||||
//
|
||||
// Although the signature of this function does not constrain T to be an
|
||||
// interface type, our convention is to only use interface types to access
|
||||
// extra info in order to allow for alternative or wrapping implementations
|
||||
// of the interface.
|
||||
func ExtraInfo[T any](diag Diagnostic) T {
|
||||
extra := diag.ExtraInfo()
|
||||
if ret, ok := extra.(T); ok {
|
||||
return ret
|
||||
}
|
||||
|
||||
// If "extra" doesn't implement T directly then we'll delegate to
|
||||
// our ExtraInfoNext helper to try iteratively unwrapping it.
|
||||
return ExtraInfoNext[T](extra)
|
||||
}
|
||||
|
||||
// ExtraInfoNext takes a value previously returned by ExtraInfo and attempts
|
||||
// to find an implementation of interface T wrapped inside of it. The return
|
||||
// value meaning is the same as for ExtraInfo.
|
||||
//
|
||||
// This is to help with the less common situation where a particular "extra"
|
||||
// value might be wrapping another value implementing the same interface,
|
||||
// and so callers can peel away one layer at a time until there are no more
|
||||
// nested layers.
|
||||
//
|
||||
// Because this function is intended for searching for _nested_ implementations
|
||||
// of T, ExtraInfoNext does not consider whether value "previous" directly
|
||||
// implements interface T, on the assumption that the previous call to ExtraInfo
|
||||
// with the same T caused "previous" to already be that result.
|
||||
func ExtraInfoNext[T any](previous interface{}) T {
|
||||
// As long as T is an interface type as documented, zero will always be
|
||||
// a nil interface value for us to return in the non-matching case.
|
||||
var zero T
|
||||
|
||||
unwrapper, ok := previous.(DiagnosticExtraUnwrapper)
|
||||
// If the given value isn't unwrappable then it can't possibly have
|
||||
// any other info nested inside of it.
|
||||
if !ok {
|
||||
return zero
|
||||
}
|
||||
|
||||
extra := unwrapper.UnwrapDiagnosticExtra()
|
||||
|
||||
// We'll keep unwrapping until we either find the interface we're
|
||||
// looking for or we run out of layers of unwrapper.
|
||||
for {
|
||||
if ret, ok := extra.(T); ok {
|
||||
return ret
|
||||
}
|
||||
|
||||
if unwrapper, ok := extra.(DiagnosticExtraUnwrapper); ok {
|
||||
extra = unwrapper.UnwrapDiagnosticExtra()
|
||||
} else {
|
||||
return zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DiagnosticExtraUnwrapper is an interface implemented by values in the
|
||||
// Extra field of Diagnostic when they are wrapping another "Extra" value that
|
||||
// was generated downstream.
|
||||
//
|
||||
// Diagnostic recipients which want to examine "Extra" values to sniff for
|
||||
// particular types of extra data can either type-assert this interface
|
||||
// directly and repeatedly unwrap until they recieve nil, or can use the
|
||||
// helper function DiagnosticExtra.
|
||||
//
|
||||
// This interface intentionally matches hcl.DiagnosticExtraUnwrapper, so that
|
||||
// wrapping extra values implemented using HCL's API will also work with the
|
||||
// tfdiags API, but that non-HCL uses of this will not need to implement HCL
|
||||
// just to get this interface.
|
||||
type DiagnosticExtraUnwrapper interface {
|
||||
// If the reciever is wrapping another "diagnostic extra" value, returns
|
||||
// that value. Otherwise returns nil to indicate dynamically that nothing
|
||||
// is wrapped.
|
||||
//
|
||||
// The "nothing is wrapped" condition can be signalled either by this
|
||||
// method returning nil or by a type not implementing this interface at all.
|
||||
//
|
||||
// Implementers should never create unwrap "cycles" where a nested extra
|
||||
// value returns a value that was also wrapping it.
|
||||
UnwrapDiagnosticExtra() interface{}
|
||||
}
|
@ -26,3 +26,8 @@ func (e nativeError) FromExpr() *FromExpr {
|
||||
// Native errors are not expression-related
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e nativeError) ExtraInfo() interface{} {
|
||||
// Native errors don't carry any "extra information".
|
||||
return nil
|
||||
}
|
||||
|
@ -50,6 +50,10 @@ func (d hclDiagnostic) FromExpr() *FromExpr {
|
||||
}
|
||||
}
|
||||
|
||||
func (d hclDiagnostic) ExtraInfo() interface{} {
|
||||
return d.diag.Extra
|
||||
}
|
||||
|
||||
// SourceRangeFromHCL constructs a SourceRange from the corresponding range
|
||||
// type within the HCL package.
|
||||
func SourceRangeFromHCL(hclRange hcl.Range) SourceRange {
|
||||
|
@ -54,6 +54,11 @@ func (d rpcFriendlyDiag) FromExpr() *FromExpr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d rpcFriendlyDiag) ExtraInfo() interface{} {
|
||||
// RPC-friendly diagnostics always discard any "extra information".
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register((*rpcFriendlyDiag)(nil))
|
||||
}
|
||||
|
@ -28,3 +28,8 @@ func (e simpleWarning) FromExpr() *FromExpr {
|
||||
// Simple warnings are not expression-related
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e simpleWarning) ExtraInfo() interface{} {
|
||||
// Simple warnings cannot carry extra information.
|
||||
return nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user