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 }