From 8405f46bc5b17403ff6fd6ef3284e68505cf3ad7 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 21 Jun 2022 15:38:55 -0700 Subject: [PATCH] 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. --- internal/earlyconfig/diagnostics.go | 4 + internal/tfdiags/consolidate_warnings.go | 4 + internal/tfdiags/diagnostic.go | 7 ++ internal/tfdiags/diagnostic_base.go | 4 + internal/tfdiags/diagnostic_extra.go | 103 +++++++++++++++++++++++ internal/tfdiags/error.go | 5 ++ internal/tfdiags/hcl.go | 4 + internal/tfdiags/rpc_friendly.go | 5 ++ internal/tfdiags/simple_warning.go | 5 ++ 9 files changed, 141 insertions(+) create mode 100644 internal/tfdiags/diagnostic_extra.go 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 +}