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