github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/diagnostic_extra.go (about)

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