github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/diagnostics.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 import ( 9 "bytes" 10 "fmt" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/hashicorp/errwrap" 16 multierror "github.com/hashicorp/go-multierror" 17 "github.com/hashicorp/hcl/v2" 18 ) 19 20 // Diagnostics is a list of diagnostics. Diagnostics is intended to be used 21 // where a Go "error" might normally be used, allowing richer information 22 // to be conveyed (more context, support for warnings). 23 // 24 // A nil Diagnostics is a valid, empty diagnostics list, thus allowing 25 // heap allocation to be avoided in the common case where there are no 26 // diagnostics to report at all. 27 type Diagnostics []Diagnostic 28 29 // Append is the main interface for constructing Diagnostics lists, taking 30 // an existing list (which may be nil) and appending the new objects to it 31 // after normalizing them to be implementations of Diagnostic. 32 // 33 // The usual pattern for a function that natively "speaks" diagnostics is: 34 // 35 // // Create a nil Diagnostics at the start of the function 36 // var diags diag.Diagnostics 37 // 38 // // At later points, build on it if errors / warnings occur: 39 // foo, err := DoSomethingRisky() 40 // if err != nil { 41 // diags = diags.Append(err) 42 // } 43 // 44 // // Eventually return the result and diagnostics in place of error 45 // return result, diags 46 // 47 // Append accepts a variety of different diagnostic-like types, including 48 // native Go errors and HCL diagnostics. It also knows how to unwrap 49 // a multierror.Error into separate error diagnostics. It can be passed 50 // another Diagnostics to concatenate the two lists. If given something 51 // it cannot handle, this function will panic. 52 func (diags Diagnostics) Append(new ...interface{}) Diagnostics { 53 for _, item := range new { 54 if item == nil { 55 continue 56 } 57 58 switch ti := item.(type) { 59 case Diagnostic: 60 diags = append(diags, ti) 61 case Diagnostics: 62 diags = append(diags, ti...) // flatten 63 case diagnosticsAsError: 64 diags = diags.Append(ti.Diagnostics) // unwrap 65 case NonFatalError: 66 diags = diags.Append(ti.Diagnostics) // unwrap 67 case hcl.Diagnostics: 68 for _, hclDiag := range ti { 69 diags = append(diags, hclDiagnostic{hclDiag}) 70 } 71 case *hcl.Diagnostic: 72 diags = append(diags, hclDiagnostic{ti}) 73 case *multierror.Error: 74 for _, err := range ti.Errors { 75 diags = append(diags, nativeError{err}) 76 } 77 case error: 78 switch { 79 case errwrap.ContainsType(ti, Diagnostics(nil)): 80 // If we have an errwrap wrapper with a Diagnostics hiding 81 // inside then we'll unpick it here to get access to the 82 // individual diagnostics. 83 diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil))) 84 case errwrap.ContainsType(ti, hcl.Diagnostics(nil)): 85 // Likewise, if we have HCL diagnostics we'll unpick that too. 86 diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil))) 87 default: 88 diags = append(diags, nativeError{ti}) 89 } 90 default: 91 panic(fmt.Errorf("can't construct diagnostic(s) from %T", item)) 92 } 93 } 94 95 // Given the above, we should never end up with a non-nil empty slice 96 // here, but we'll make sure of that so callers can rely on empty == nil 97 if len(diags) == 0 { 98 return nil 99 } 100 101 return diags 102 } 103 104 // HasErrors returns true if any of the diagnostics in the list have 105 // a severity of Error. 106 func (diags Diagnostics) HasErrors() bool { 107 for _, diag := range diags { 108 if diag.Severity() == Error { 109 return true 110 } 111 } 112 return false 113 } 114 115 // ForRPC returns a version of the receiver that has been simplified so that 116 // it is friendly to RPC protocols. 117 // 118 // Currently this means that it can be serialized with encoding/gob and 119 // subsequently re-inflated. It may later grow to include other serialization 120 // formats. 121 // 122 // Note that this loses information about the original objects used to 123 // construct the diagnostics, so e.g. the errwrap API will not work as 124 // expected on an error-wrapped Diagnostics that came from ForRPC. 125 func (diags Diagnostics) ForRPC() Diagnostics { 126 ret := make(Diagnostics, len(diags)) 127 for i := range diags { 128 ret[i] = makeRPCFriendlyDiag(diags[i]) 129 } 130 return ret 131 } 132 133 // Err flattens a diagnostics list into a single Go error, or to nil 134 // if the diagnostics list does not include any error-level diagnostics. 135 // 136 // This can be used to smuggle diagnostics through an API that deals in 137 // native errors, but unfortunately it will lose any warnings that aren't 138 // accompanied by at least one error since such APIs have no mechanism through 139 // which to report those. 140 // 141 // return result, diags.Error() 142 func (diags Diagnostics) Err() error { 143 if !diags.HasErrors() { 144 return nil 145 } 146 return diagnosticsAsError{diags} 147 } 148 149 // ErrWithWarnings is similar to Err except that it will also return a non-nil 150 // error if the receiver contains only warnings. 151 // 152 // In the warnings-only situation, the result is guaranteed to be of dynamic 153 // type NonFatalError, allowing diagnostics-aware callers to type-assert 154 // and unwrap it, treating it as non-fatal. 155 // 156 // This should be used only in contexts where the caller is able to recognize 157 // and handle NonFatalError. For normal callers that expect a lack of errors 158 // to be signaled by nil, use just Diagnostics.Err. 159 func (diags Diagnostics) ErrWithWarnings() error { 160 if len(diags) == 0 { 161 return nil 162 } 163 if diags.HasErrors() { 164 return diags.Err() 165 } 166 return NonFatalError{diags} 167 } 168 169 // NonFatalErr is similar to Err except that it always returns either nil 170 // (if there are no diagnostics at all) or NonFatalError. 171 // 172 // This allows diagnostics to be returned over an error return channel while 173 // being explicit that the diagnostics should not halt processing. 174 // 175 // This should be used only in contexts where the caller is able to recognize 176 // and handle NonFatalError. For normal callers that expect a lack of errors 177 // to be signaled by nil, use just Diagnostics.Err. 178 func (diags Diagnostics) NonFatalErr() error { 179 if len(diags) == 0 { 180 return nil 181 } 182 return NonFatalError{diags} 183 } 184 185 // Sort applies an ordering to the diagnostics in the receiver in-place. 186 // 187 // The ordering is: warnings before errors, sourceless before sourced, 188 // short source paths before long source paths, and then ordering by 189 // position within each file. 190 // 191 // Diagnostics that do not differ by any of these sortable characteristics 192 // will remain in the same relative order after this method returns. 193 func (diags Diagnostics) Sort() { 194 sort.Stable(sortDiagnostics(diags)) 195 } 196 197 type diagnosticsAsError struct { 198 Diagnostics 199 } 200 201 func (dae diagnosticsAsError) Error() string { 202 diags := dae.Diagnostics 203 switch { 204 case len(diags) == 0: 205 // should never happen, since we don't create this wrapper if 206 // there are no diagnostics in the list. 207 return "no errors" 208 case len(diags) == 1: 209 desc := diags[0].Description() 210 if desc.Detail == "" { 211 return desc.Summary 212 } 213 return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail) 214 default: 215 var ret bytes.Buffer 216 fmt.Fprintf(&ret, "%d problems:\n", len(diags)) 217 for _, diag := range dae.Diagnostics { 218 desc := diag.Description() 219 if desc.Detail == "" { 220 fmt.Fprintf(&ret, "\n- %s", desc.Summary) 221 } else { 222 fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail) 223 } 224 } 225 return ret.String() 226 } 227 } 228 229 // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped 230 // diagnostics object can be picked apart by errwrap-aware code. 231 func (dae diagnosticsAsError) WrappedErrors() []error { 232 var errs []error 233 for _, diag := range dae.Diagnostics { 234 if wrapper, isErr := diag.(nativeError); isErr { 235 errs = append(errs, wrapper.err) 236 } 237 } 238 return errs 239 } 240 241 // NonFatalError is a special error type, returned by 242 // Diagnostics.ErrWithWarnings and Diagnostics.NonFatalErr, 243 // that indicates that the wrapped diagnostics should be treated as non-fatal. 244 // Callers can conditionally type-assert an error to this type in order to 245 // detect the non-fatal scenario and handle it in a different way. 246 type NonFatalError struct { 247 Diagnostics 248 } 249 250 func (woe NonFatalError) Error() string { 251 diags := woe.Diagnostics 252 switch { 253 case len(diags) == 0: 254 // should never happen, since we don't create this wrapper if 255 // there are no diagnostics in the list. 256 return "no errors or warnings" 257 case len(diags) == 1: 258 desc := diags[0].Description() 259 if desc.Detail == "" { 260 return desc.Summary 261 } 262 return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail) 263 default: 264 var ret bytes.Buffer 265 if diags.HasErrors() { 266 fmt.Fprintf(&ret, "%d problems:\n", len(diags)) 267 } else { 268 fmt.Fprintf(&ret, "%d warnings:\n", len(diags)) 269 } 270 for _, diag := range woe.Diagnostics { 271 desc := diag.Description() 272 if desc.Detail == "" { 273 fmt.Fprintf(&ret, "\n- %s", desc.Summary) 274 } else { 275 fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail) 276 } 277 } 278 return ret.String() 279 } 280 } 281 282 // sortDiagnostics is an implementation of sort.Interface 283 type sortDiagnostics []Diagnostic 284 285 var _ sort.Interface = sortDiagnostics(nil) 286 287 func (sd sortDiagnostics) Len() int { 288 return len(sd) 289 } 290 291 func (sd sortDiagnostics) Less(i, j int) bool { 292 iD, jD := sd[i], sd[j] 293 iSev, jSev := iD.Severity(), jD.Severity() 294 iSrc, jSrc := iD.Source(), jD.Source() 295 296 switch { 297 298 case iSev != jSev: 299 return iSev == Warning 300 301 case (iSrc.Subject == nil) != (jSrc.Subject == nil): 302 return iSrc.Subject == nil 303 304 case iSrc.Subject != nil && *iSrc.Subject != *jSrc.Subject: 305 iSubj := iSrc.Subject 306 jSubj := jSrc.Subject 307 switch { 308 case iSubj.Filename != jSubj.Filename: 309 // Path with fewer segments goes first if they are different lengths 310 sep := string(filepath.Separator) 311 iCount := strings.Count(iSubj.Filename, sep) 312 jCount := strings.Count(jSubj.Filename, sep) 313 if iCount != jCount { 314 return iCount < jCount 315 } 316 return iSubj.Filename < jSubj.Filename 317 case iSubj.Start.Byte != jSubj.Start.Byte: 318 return iSubj.Start.Byte < jSubj.Start.Byte 319 case iSubj.End.Byte != jSubj.End.Byte: 320 return iSubj.End.Byte < jSubj.End.Byte 321 } 322 fallthrough 323 324 default: 325 // The remaining properties do not have a defined ordering, so 326 // we'll leave it unspecified. Since we use sort.Stable in 327 // the caller of this, the ordering of remaining items will 328 // be preserved. 329 return false 330 } 331 } 332 333 func (sd sortDiagnostics) Swap(i, j int) { 334 sd[i], sd[j] = sd[j], sd[i] 335 }