1package tfdiags
2
3import (
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/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.
22type 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.
47func (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.
101func (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.
120func (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()
137func (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.
154func (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.
173func (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.
188func (diags Diagnostics) Sort() {
189	sort.Stable(sortDiagnostics(diags))
190}
191
192type diagnosticsAsError struct {
193	Diagnostics
194}
195
196func (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.
226func (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.
241type NonFatalError struct {
242	Diagnostics
243}
244
245func (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
278type sortDiagnostics []Diagnostic
279
280var _ sort.Interface = sortDiagnostics(nil)
281
282func (sd sortDiagnostics) Len() int {
283	return len(sd)
284}
285
286func (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
328func (sd sortDiagnostics) Swap(i, j int) {
329	sd[i], sd[j] = sd[j], sd[i]
330}
331