diff --git a/internal/earlyconfig/diagnostics.go b/internal/earlyconfig/diagnostics.go index 9a6b266363..15adad5638 100644 --- a/internal/earlyconfig/diagnostics.go +++ b/internal/earlyconfig/diagnostics.go @@ -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 +} diff --git a/internal/tfdiags/consolidate_warnings.go b/internal/tfdiags/consolidate_warnings.go index 06f3d52cc0..08d36d60b6 100644 --- a/internal/tfdiags/consolidate_warnings.go +++ b/internal/tfdiags/consolidate_warnings.go @@ -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") diff --git a/internal/tfdiags/diagnostic.go b/internal/tfdiags/diagnostic.go index c241ab4220..f988f398b6 100644 --- a/internal/tfdiags/diagnostic.go +++ b/internal/tfdiags/diagnostic.go @@ -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 diff --git a/internal/tfdiags/diagnostic_base.go b/internal/tfdiags/diagnostic_base.go index 04e56773de..88495290e7 100644 --- a/internal/tfdiags/diagnostic_base.go +++ b/internal/tfdiags/diagnostic_base.go @@ -31,3 +31,7 @@ func (d diagnosticBase) Source() Source { func (d diagnosticBase) FromExpr() *FromExpr { return nil } + +func (d diagnosticBase) ExtraInfo() interface{} { + return nil +} diff --git a/internal/tfdiags/diagnostic_extra.go b/internal/tfdiags/diagnostic_extra.go new file mode 100644 index 0000000000..dfa746ec03 --- /dev/null +++ b/internal/tfdiags/diagnostic_extra.go @@ -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{} +} diff --git a/internal/tfdiags/error.go b/internal/tfdiags/error.go index 13f7a714f4..1e26bf9680 100644 --- a/internal/tfdiags/error.go +++ b/internal/tfdiags/error.go @@ -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 +} diff --git a/internal/tfdiags/hcl.go b/internal/tfdiags/hcl.go index ad0d8220f9..edf16b5b4d 100644 --- a/internal/tfdiags/hcl.go +++ b/internal/tfdiags/hcl.go @@ -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 { diff --git a/internal/tfdiags/rpc_friendly.go b/internal/tfdiags/rpc_friendly.go index 485063b0c0..4c627bf98a 100644 --- a/internal/tfdiags/rpc_friendly.go +++ b/internal/tfdiags/rpc_friendly.go @@ -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)) } diff --git a/internal/tfdiags/simple_warning.go b/internal/tfdiags/simple_warning.go index b0f1ecd46c..3c18f19247 100644 --- a/internal/tfdiags/simple_warning.go +++ b/internal/tfdiags/simple_warning.go @@ -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 +}