]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package tfdiags |
2 | ||
3 | import ( | |
4 | "bytes" | |
5 | "fmt" | |
107c1cdb ND |
6 | "path/filepath" |
7 | "sort" | |
8 | "strings" | |
15c0b25d AP |
9 | |
10 | "github.com/hashicorp/errwrap" | |
11 | multierror "github.com/hashicorp/go-multierror" | |
12 | "github.com/hashicorp/hcl2/hcl" | |
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 | |
107c1cdb ND |
60 | case NonFatalError: |
61 | diags = diags.Append(ti.Diagnostics) // unwrap | |
15c0b25d AP |
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 naked warnings (warnings | |
133 | // that aren't accompanied by at least one error) since such APIs have no | |
134 | // mechanism through which to report these. | |
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 | ||
107c1cdb ND |
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 | ||
15c0b25d AP |
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 | } | |
107c1cdb ND |
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 | } |