]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
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 | } |