github.com/magodo/terraform@v0.11.12-beta1/tfdiags/diagnostics.go (about) 1 package tfdiags 2 3 import ( 4 "bytes" 5 "fmt" 6 7 "github.com/hashicorp/errwrap" 8 multierror "github.com/hashicorp/go-multierror" 9 "github.com/hashicorp/hcl2/hcl" 10 ) 11 12 // Diagnostics is a list of diagnostics. Diagnostics is intended to be used 13 // where a Go "error" might normally be used, allowing richer information 14 // to be conveyed (more context, support for warnings). 15 // 16 // A nil Diagnostics is a valid, empty diagnostics list, thus allowing 17 // heap allocation to be avoided in the common case where there are no 18 // diagnostics to report at all. 19 type Diagnostics []Diagnostic 20 21 // Append is the main interface for constructing Diagnostics lists, taking 22 // an existing list (which may be nil) and appending the new objects to it 23 // after normalizing them to be implementations of Diagnostic. 24 // 25 // The usual pattern for a function that natively "speaks" diagnostics is: 26 // 27 // // Create a nil Diagnostics at the start of the function 28 // var diags diag.Diagnostics 29 // 30 // // At later points, build on it if errors / warnings occur: 31 // foo, err := DoSomethingRisky() 32 // if err != nil { 33 // diags = diags.Append(err) 34 // } 35 // 36 // // Eventually return the result and diagnostics in place of error 37 // return result, diags 38 // 39 // Append accepts a variety of different diagnostic-like types, including 40 // native Go errors and HCL diagnostics. It also knows how to unwrap 41 // a multierror.Error into separate error diagnostics. It can be passed 42 // another Diagnostics to concatenate the two lists. If given something 43 // it cannot handle, this function will panic. 44 func (diags Diagnostics) Append(new ...interface{}) Diagnostics { 45 for _, item := range new { 46 if item == nil { 47 continue 48 } 49 50 switch ti := item.(type) { 51 case Diagnostic: 52 diags = append(diags, ti) 53 case Diagnostics: 54 diags = append(diags, ti...) // flatten 55 case diagnosticsAsError: 56 diags = diags.Append(ti.Diagnostics) // unwrap 57 case hcl.Diagnostics: 58 for _, hclDiag := range ti { 59 diags = append(diags, hclDiagnostic{hclDiag}) 60 } 61 case *hcl.Diagnostic: 62 diags = append(diags, hclDiagnostic{ti}) 63 case *multierror.Error: 64 for _, err := range ti.Errors { 65 diags = append(diags, nativeError{err}) 66 } 67 case error: 68 switch { 69 case errwrap.ContainsType(ti, Diagnostics(nil)): 70 // If we have an errwrap wrapper with a Diagnostics hiding 71 // inside then we'll unpick it here to get access to the 72 // individual diagnostics. 73 diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil))) 74 case errwrap.ContainsType(ti, hcl.Diagnostics(nil)): 75 // Likewise, if we have HCL diagnostics we'll unpick that too. 76 diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil))) 77 default: 78 diags = append(diags, nativeError{ti}) 79 } 80 default: 81 panic(fmt.Errorf("can't construct diagnostic(s) from %T", item)) 82 } 83 } 84 85 // Given the above, we should never end up with a non-nil empty slice 86 // here, but we'll make sure of that so callers can rely on empty == nil 87 if len(diags) == 0 { 88 return nil 89 } 90 91 return diags 92 } 93 94 // HasErrors returns true if any of the diagnostics in the list have 95 // a severity of Error. 96 func (diags Diagnostics) HasErrors() bool { 97 for _, diag := range diags { 98 if diag.Severity() == Error { 99 return true 100 } 101 } 102 return false 103 } 104 105 // ForRPC returns a version of the receiver that has been simplified so that 106 // it is friendly to RPC protocols. 107 // 108 // Currently this means that it can be serialized with encoding/gob and 109 // subsequently re-inflated. It may later grow to include other serialization 110 // formats. 111 // 112 // Note that this loses information about the original objects used to 113 // construct the diagnostics, so e.g. the errwrap API will not work as 114 // expected on an error-wrapped Diagnostics that came from ForRPC. 115 func (diags Diagnostics) ForRPC() Diagnostics { 116 ret := make(Diagnostics, len(diags)) 117 for i := range diags { 118 ret[i] = makeRPCFriendlyDiag(diags[i]) 119 } 120 return ret 121 } 122 123 // Err flattens a diagnostics list into a single Go error, or to nil 124 // if the diagnostics list does not include any error-level diagnostics. 125 // 126 // This can be used to smuggle diagnostics through an API that deals in 127 // native errors, but unfortunately it will lose naked warnings (warnings 128 // that aren't accompanied by at least one error) since such APIs have no 129 // mechanism through which to report these. 130 // 131 // return result, diags.Error() 132 func (diags Diagnostics) Err() error { 133 if !diags.HasErrors() { 134 return nil 135 } 136 return diagnosticsAsError{diags} 137 } 138 139 type diagnosticsAsError struct { 140 Diagnostics 141 } 142 143 func (dae diagnosticsAsError) Error() string { 144 diags := dae.Diagnostics 145 switch { 146 case len(diags) == 0: 147 // should never happen, since we don't create this wrapper if 148 // there are no diagnostics in the list. 149 return "no errors" 150 case len(diags) == 1: 151 desc := diags[0].Description() 152 if desc.Detail == "" { 153 return desc.Summary 154 } 155 return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail) 156 default: 157 var ret bytes.Buffer 158 fmt.Fprintf(&ret, "%d problems:\n", len(diags)) 159 for _, diag := range dae.Diagnostics { 160 desc := diag.Description() 161 if desc.Detail == "" { 162 fmt.Fprintf(&ret, "\n- %s", desc.Summary) 163 } else { 164 fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail) 165 } 166 } 167 return ret.String() 168 } 169 } 170 171 // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped 172 // diagnostics object can be picked apart by errwrap-aware code. 173 func (dae diagnosticsAsError) WrappedErrors() []error { 174 var errs []error 175 for _, diag := range dae.Diagnostics { 176 if wrapper, isErr := diag.(nativeError); isErr { 177 errs = append(errs, wrapper.err) 178 } 179 } 180 return errs 181 }