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