github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/diagnostic_extra.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package tfdiags
     5  
     6  // This "Extra" idea is something we've inherited from HCL's diagnostic model,
     7  // and so it's primarily to expose that functionality from wrapped HCL
     8  // diagnostics but other diagnostic types could potentially implement this
     9  // protocol too, if needed.
    10  
    11  // ExtraInfo tries to retrieve extra information of interface type T from
    12  // the given diagnostic.
    13  //
    14  // "Extra information" is situation-specific additional contextual data which
    15  // might allow for some special tailored reporting of particular
    16  // diagnostics in the UI. Conventionally the extra information is provided
    17  // as a hidden type that implements one or more interfaces which a caller
    18  // can pass as type parameter T to retrieve a value of that type when the
    19  // diagnostic has such an implementation.
    20  //
    21  // If the given diagnostic's extra value has an implementation of interface T
    22  // then ExtraInfo returns a non-nil interface value. If there is no such
    23  // implementation, ExtraInfo returns a nil T.
    24  //
    25  // Although the signature of this function does not constrain T to be an
    26  // interface type, our convention is to only use interface types to access
    27  // extra info in order to allow for alternative or wrapping implementations
    28  // of the interface.
    29  func ExtraInfo[T any](diag Diagnostic) T {
    30  	extra := diag.ExtraInfo()
    31  	if ret, ok := extra.(T); ok {
    32  		return ret
    33  	}
    34  
    35  	// If "extra" doesn't implement T directly then we'll delegate to
    36  	// our ExtraInfoNext helper to try iteratively unwrapping it.
    37  	return ExtraInfoNext[T](extra)
    38  }
    39  
    40  // ExtraInfoNext takes a value previously returned by ExtraInfo and attempts
    41  // to find an implementation of interface T wrapped inside of it. The return
    42  // value meaning is the same as for ExtraInfo.
    43  //
    44  // This is to help with the less common situation where a particular "extra"
    45  // value might be wrapping another value implementing the same interface,
    46  // and so callers can peel away one layer at a time until there are no more
    47  // nested layers.
    48  //
    49  // Because this function is intended for searching for _nested_ implementations
    50  // of T, ExtraInfoNext does not consider whether value "previous" directly
    51  // implements interface T, on the assumption that the previous call to ExtraInfo
    52  // with the same T caused "previous" to already be that result.
    53  func ExtraInfoNext[T any](previous interface{}) T {
    54  	// As long as T is an interface type as documented, zero will always be
    55  	// a nil interface value for us to return in the non-matching case.
    56  	var zero T
    57  
    58  	unwrapper, ok := previous.(DiagnosticExtraUnwrapper)
    59  	// If the given value isn't unwrappable then it can't possibly have
    60  	// any other info nested inside of it.
    61  	if !ok {
    62  		return zero
    63  	}
    64  
    65  	extra := unwrapper.UnwrapDiagnosticExtra()
    66  
    67  	// We'll keep unwrapping until we either find the interface we're
    68  	// looking for or we run out of layers of unwrapper.
    69  	for {
    70  		if ret, ok := extra.(T); ok {
    71  			return ret
    72  		}
    73  
    74  		if unwrapper, ok := extra.(DiagnosticExtraUnwrapper); ok {
    75  			extra = unwrapper.UnwrapDiagnosticExtra()
    76  		} else {
    77  			return zero
    78  		}
    79  	}
    80  }
    81  
    82  // DiagnosticExtraUnwrapper is an interface implemented by values in the
    83  // Extra field of Diagnostic when they are wrapping another "Extra" value that
    84  // was generated downstream.
    85  //
    86  // Diagnostic recipients which want to examine "Extra" values to sniff for
    87  // particular types of extra data can either type-assert this interface
    88  // directly and repeatedly unwrap until they recieve nil, or can use the
    89  // helper function DiagnosticExtra.
    90  //
    91  // This interface intentionally matches hcl.DiagnosticExtraUnwrapper, so that
    92  // wrapping extra values implemented using HCL's API will also work with the
    93  // tfdiags API, but that non-HCL uses of this will not need to implement HCL
    94  // just to get this interface.
    95  type DiagnosticExtraUnwrapper interface {
    96  	// If the reciever is wrapping another "diagnostic extra" value, returns
    97  	// that value. Otherwise returns nil to indicate dynamically that nothing
    98  	// is wrapped.
    99  	//
   100  	// The "nothing is wrapped" condition can be signalled either by this
   101  	// method returning nil or by a type not implementing this interface at all.
   102  	//
   103  	// Implementers should never create unwrap "cycles" where a nested extra
   104  	// value returns a value that was also wrapping it.
   105  	UnwrapDiagnosticExtra() interface{}
   106  }
   107  
   108  // DiagnosticExtraWrapper is an interface implemented by values that can be
   109  // dynamically updated to wrap other extra info.
   110  type DiagnosticExtraWrapper interface {
   111  	// WrapDiagnosticExtra accepts an ExtraInfo that it should add within the
   112  	// current ExtraInfo.
   113  	WrapDiagnosticExtra(inner interface{})
   114  }
   115  
   116  // DiagnosticExtraBecauseUnknown is an interface implemented by values in
   117  // the Extra field of Diagnostic when the diagnostic is potentially caused by
   118  // the presence of unknown values in an expression evaluation.
   119  //
   120  // Just implementing this interface is not sufficient signal, though. Callers
   121  // must also call the DiagnosticCausedByUnknown method in order to confirm
   122  // the result, or use the package-level function DiagnosticCausedByUnknown
   123  // as a convenient wrapper.
   124  type DiagnosticExtraBecauseUnknown interface {
   125  	// DiagnosticCausedByUnknown returns true if the associated diagnostic
   126  	// was caused by the presence of unknown values during an expression
   127  	// evaluation, or false otherwise.
   128  	//
   129  	// Callers might use this to tailor what contextual information they show
   130  	// alongside an error report in the UI, to avoid potential confusion
   131  	// caused by talking about the presence of unknown values if that was
   132  	// immaterial to the error.
   133  	DiagnosticCausedByUnknown() bool
   134  }
   135  
   136  // DiagnosticCausedByUnknown returns true if the given diagnostic has an
   137  // indication that it was caused by the presence of unknown values during
   138  // an expression evaluation.
   139  //
   140  // This is a wrapper around checking if the diagnostic's extra info implements
   141  // interface DiagnosticExtraBecauseUnknown and then calling its method if so.
   142  func DiagnosticCausedByUnknown(diag Diagnostic) bool {
   143  	maybe := ExtraInfo[DiagnosticExtraBecauseUnknown](diag)
   144  	if maybe == nil {
   145  		return false
   146  	}
   147  	return maybe.DiagnosticCausedByUnknown()
   148  }
   149  
   150  // DiagnosticExtraBecauseSensitive is an interface implemented by values in
   151  // the Extra field of Diagnostic when the diagnostic is potentially caused by
   152  // the presence of sensitive values in an expression evaluation.
   153  //
   154  // Just implementing this interface is not sufficient signal, though. Callers
   155  // must also call the DiagnosticCausedBySensitive method in order to confirm
   156  // the result, or use the package-level function DiagnosticCausedBySensitive
   157  // as a convenient wrapper.
   158  type DiagnosticExtraBecauseSensitive interface {
   159  	// DiagnosticCausedBySensitive returns true if the associated diagnostic
   160  	// was caused by the presence of sensitive values during an expression
   161  	// evaluation, or false otherwise.
   162  	//
   163  	// Callers might use this to tailor what contextual information they show
   164  	// alongside an error report in the UI, to avoid potential confusion
   165  	// caused by talking about the presence of sensitive values if that was
   166  	// immaterial to the error.
   167  	DiagnosticCausedBySensitive() bool
   168  }
   169  
   170  // DiagnosticCausedBySensitive returns true if the given diagnostic has an
   171  // indication that it was caused by the presence of sensitive values during
   172  // an expression evaluation.
   173  //
   174  // This is a wrapper around checking if the diagnostic's extra info implements
   175  // interface DiagnosticExtraBecauseSensitive and then calling its method if so.
   176  func DiagnosticCausedBySensitive(diag Diagnostic) bool {
   177  	maybe := ExtraInfo[DiagnosticExtraBecauseSensitive](diag)
   178  	if maybe == nil {
   179  		return false
   180  	}
   181  	return maybe.DiagnosticCausedBySensitive()
   182  }
   183  
   184  // DiagnosticExtraDoNotConsolidate tells the Diagnostics.ConsolidateWarnings
   185  // function not to consolidate this diagnostic if it otherwise would.
   186  type DiagnosticExtraDoNotConsolidate interface {
   187  	// DoNotConsolidateDiagnostic returns true if the associated diagnostic
   188  	// should not be consolidated by the Diagnostics.ConsolidateWarnings
   189  	// function.
   190  	DoNotConsolidateDiagnostic() bool
   191  }
   192  
   193  // DoNotConsolidateDiagnostic returns true if the given diagnostic should not
   194  // be consolidated by the Diagnostics.ConsolidateWarnings function.
   195  func DoNotConsolidateDiagnostic(diag Diagnostic) bool {
   196  	maybe := ExtraInfo[DiagnosticExtraDoNotConsolidate](diag)
   197  	if maybe == nil {
   198  		return false
   199  	}
   200  	return maybe.DoNotConsolidateDiagnostic()
   201  }