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 }