]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/tfdiags/contextual.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / tfdiags / contextual.go
1 package tfdiags
2
3 import (
4 "github.com/hashicorp/hcl2/hcl"
5 "github.com/zclconf/go-cty/cty"
6 "github.com/zclconf/go-cty/cty/gocty"
7 )
8
9 // The "contextual" family of diagnostics are designed to allow separating
10 // the detection of a problem from placing that problem in context. For
11 // example, some code that is validating an object extracted from configuration
12 // may not have access to the configuration that generated it, but can still
13 // report problems within that object which the caller can then place in
14 // context by calling IsConfigBody on the returned diagnostics.
15 //
16 // When contextual diagnostics are used, the documentation for a method must
17 // be very explicit about what context is implied for any diagnostics returned,
18 // to help ensure the expected result.
19
20 // contextualFromConfig is an interface type implemented by diagnostic types
21 // that can elaborate themselves when given information about the configuration
22 // body they are embedded in.
23 //
24 // Usually this entails extracting source location information in order to
25 // populate the "Subject" range.
26 type contextualFromConfigBody interface {
27 ElaborateFromConfigBody(hcl.Body) Diagnostic
28 }
29
30 // InConfigBody returns a copy of the receiver with any config-contextual
31 // diagnostics elaborated in the context of the given body.
32 func (d Diagnostics) InConfigBody(body hcl.Body) Diagnostics {
33 if len(d) == 0 {
34 return nil
35 }
36
37 ret := make(Diagnostics, len(d))
38 for i, srcDiag := range d {
39 if cd, isCD := srcDiag.(contextualFromConfigBody); isCD {
40 ret[i] = cd.ElaborateFromConfigBody(body)
41 } else {
42 ret[i] = srcDiag
43 }
44 }
45
46 return ret
47 }
48
49 // AttributeValue returns a diagnostic about an attribute value in an implied current
50 // configuration context. This should be returned only from functions whose
51 // interface specifies a clear configuration context that this will be
52 // resolved in.
53 //
54 // The given path is relative to the implied configuration context. To describe
55 // a top-level attribute, it should be a single-element cty.Path with a
56 // cty.GetAttrStep. It's assumed that the path is returning into a structure
57 // that would be produced by our conventions in the configschema package; it
58 // may return unexpected results for structures that can't be represented by
59 // configschema.
60 //
61 // Since mapping attribute paths back onto configuration is an imprecise
62 // operation (e.g. dynamic block generation may cause the same block to be
63 // evaluated multiple times) the diagnostic detail should include the attribute
64 // name and other context required to help the user understand what is being
65 // referenced in case the identified source range is not unique.
66 //
67 // The returned attribute will not have source location information until
68 // context is applied to the containing diagnostics using diags.InConfigBody.
69 // After context is applied, the source location is the value assigned to the
70 // named attribute, or the containing body's "missing item range" if no
71 // value is present.
72 func AttributeValue(severity Severity, summary, detail string, attrPath cty.Path) Diagnostic {
73 return &attributeDiagnostic{
74 diagnosticBase: diagnosticBase{
75 severity: severity,
76 summary: summary,
77 detail: detail,
78 },
79 attrPath: attrPath,
80 }
81 }
82
83 // GetAttribute extracts an attribute cty.Path from a diagnostic if it contains
84 // one. Normally this is not accessed directly, and instead the config body is
85 // added to the Diagnostic to create a more complete message for the user. In
86 // some cases however, we may want to know just the name of the attribute that
87 // generated the Diagnostic message.
88 // This returns a nil cty.Path if it does not exist in the Diagnostic.
89 func GetAttribute(d Diagnostic) cty.Path {
90 if d, ok := d.(*attributeDiagnostic); ok {
91 return d.attrPath
92 }
93 return nil
94 }
95
96 type attributeDiagnostic struct {
97 diagnosticBase
98 attrPath cty.Path
99 subject *SourceRange // populated only after ElaborateFromConfigBody
100 }
101
102 // ElaborateFromConfigBody finds the most accurate possible source location
103 // for a diagnostic's attribute path within the given body.
104 //
105 // Backing out from a path back to a source location is not always entirely
106 // possible because we lose some information in the decoding process, so
107 // if an exact position cannot be found then the returned diagnostic will
108 // refer to a position somewhere within the containing body, which is assumed
109 // to be better than no location at all.
110 //
111 // If possible it is generally better to report an error at a layer where
112 // source location information is still available, for more accuracy. This
113 // is not always possible due to system architecture, so this serves as a
114 // "best effort" fallback behavior for such situations.
115 func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body) Diagnostic {
116 if len(d.attrPath) < 1 {
117 // Should never happen, but we'll allow it rather than crashing.
118 return d
119 }
120
121 if d.subject != nil {
122 // Don't modify an already-elaborated diagnostic.
123 return d
124 }
125
126 ret := *d
127
128 // This function will often end up re-decoding values that were already
129 // decoded by an earlier step. This is non-ideal but is architecturally
130 // more convenient than arranging for source location information to be
131 // propagated to every place in Terraform, and this happens only in the
132 // presence of errors where performance isn't a concern.
133
134 traverse := d.attrPath[:]
135 final := d.attrPath[len(d.attrPath)-1]
136
137 // Index should never be the first step
138 // as indexing of top blocks (such as resources & data sources)
139 // is handled elsewhere
140 if _, isIdxStep := traverse[0].(cty.IndexStep); isIdxStep {
141 subject := SourceRangeFromHCL(body.MissingItemRange())
142 ret.subject = &subject
143 return &ret
144 }
145
146 // Process index separately
147 idxStep, hasIdx := final.(cty.IndexStep)
148 if hasIdx {
149 final = d.attrPath[len(d.attrPath)-2]
150 traverse = d.attrPath[:len(d.attrPath)-1]
151 }
152
153 // If we have more than one step after removing index
154 // then we'll first try to traverse to a child body
155 // corresponding to the requested path.
156 if len(traverse) > 1 {
157 body = traversePathSteps(traverse, body)
158 }
159
160 // Default is to indicate a missing item in the deepest body we reached
161 // while traversing.
162 subject := SourceRangeFromHCL(body.MissingItemRange())
163 ret.subject = &subject
164
165 // Once we get here, "final" should be a GetAttr step that maps to an
166 // attribute in our current body.
167 finalStep, isAttr := final.(cty.GetAttrStep)
168 if !isAttr {
169 return &ret
170 }
171
172 content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
173 Attributes: []hcl.AttributeSchema{
174 {
175 Name: finalStep.Name,
176 Required: true,
177 },
178 },
179 })
180 if contentDiags.HasErrors() {
181 return &ret
182 }
183
184 if attr, ok := content.Attributes[finalStep.Name]; ok {
185 hclRange := attr.Expr.Range()
186 if hasIdx {
187 // Try to be more precise by finding index range
188 hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr)
189 }
190 subject = SourceRangeFromHCL(hclRange)
191 ret.subject = &subject
192 }
193
194 return &ret
195 }
196
197 func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
198 for i := 0; i < len(traverse); i++ {
199 step := traverse[i]
200
201 switch tStep := step.(type) {
202 case cty.GetAttrStep:
203
204 var next cty.PathStep
205 if i < (len(traverse) - 1) {
206 next = traverse[i+1]
207 }
208
209 // Will be indexing into our result here?
210 var indexType cty.Type
211 var indexVal cty.Value
212 if nextIndex, ok := next.(cty.IndexStep); ok {
213 indexVal = nextIndex.Key
214 indexType = indexVal.Type()
215 i++ // skip over the index on subsequent iterations
216 }
217
218 var blockLabelNames []string
219 if indexType == cty.String {
220 // Map traversal means we expect one label for the key.
221 blockLabelNames = []string{"key"}
222 }
223
224 // For intermediate steps we expect to be referring to a child
225 // block, so we'll attempt decoding under that assumption.
226 content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
227 Blocks: []hcl.BlockHeaderSchema{
228 {
229 Type: tStep.Name,
230 LabelNames: blockLabelNames,
231 },
232 },
233 })
234 if contentDiags.HasErrors() {
235 return body
236 }
237 filtered := make([]*hcl.Block, 0, len(content.Blocks))
238 for _, block := range content.Blocks {
239 if block.Type == tStep.Name {
240 filtered = append(filtered, block)
241 }
242 }
243 if len(filtered) == 0 {
244 // Step doesn't refer to a block
245 continue
246 }
247
248 switch indexType {
249 case cty.NilType: // no index at all
250 if len(filtered) != 1 {
251 return body
252 }
253 body = filtered[0].Body
254 case cty.Number:
255 var idx int
256 err := gocty.FromCtyValue(indexVal, &idx)
257 if err != nil || idx >= len(filtered) {
258 return body
259 }
260 body = filtered[idx].Body
261 case cty.String:
262 key := indexVal.AsString()
263 var block *hcl.Block
264 for _, candidate := range filtered {
265 if candidate.Labels[0] == key {
266 block = candidate
267 break
268 }
269 }
270 if block == nil {
271 // No block with this key, so we'll just indicate a
272 // missing item in the containing block.
273 return body
274 }
275 body = block.Body
276 default:
277 // Should never happen, because only string and numeric indices
278 // are supported by cty collections.
279 return body
280 }
281
282 default:
283 // For any other kind of step, we'll just return our current body
284 // as the subject and accept that this is a little inaccurate.
285 return body
286 }
287 }
288 return body
289 }
290
291 func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range {
292 switch idxStep.Key.Type() {
293 case cty.Number:
294 var idx int
295 err := gocty.FromCtyValue(idxStep.Key, &idx)
296 items, diags := hcl.ExprList(attr.Expr)
297 if diags.HasErrors() {
298 return attr.Expr.Range()
299 }
300 if err != nil || idx >= len(items) {
301 return attr.NameRange
302 }
303 return items[idx].Range()
304 case cty.String:
305 pairs, diags := hcl.ExprMap(attr.Expr)
306 if diags.HasErrors() {
307 return attr.Expr.Range()
308 }
309 stepKey := idxStep.Key.AsString()
310 for _, kvPair := range pairs {
311 key, err := kvPair.Key.Value(nil)
312 if err != nil {
313 return attr.Expr.Range()
314 }
315 if key.AsString() == stepKey {
316 startRng := kvPair.Value.StartRange()
317 return startRng
318 }
319 }
320 return attr.NameRange
321 }
322 return attr.Expr.Range()
323 }
324
325 func (d *attributeDiagnostic) Source() Source {
326 return Source{
327 Subject: d.subject,
328 }
329 }
330
331 // WholeContainingBody returns a diagnostic about the body that is an implied
332 // current configuration context. This should be returned only from
333 // functions whose interface specifies a clear configuration context that this
334 // will be resolved in.
335 //
336 // The returned attribute will not have source location information until
337 // context is applied to the containing diagnostics using diags.InConfigBody.
338 // After context is applied, the source location is currently the missing item
339 // range of the body. In future, this may change to some other suitable
340 // part of the containing body.
341 func WholeContainingBody(severity Severity, summary, detail string) Diagnostic {
342 return &wholeBodyDiagnostic{
343 diagnosticBase: diagnosticBase{
344 severity: severity,
345 summary: summary,
346 detail: detail,
347 },
348 }
349 }
350
351 type wholeBodyDiagnostic struct {
352 diagnosticBase
353 subject *SourceRange // populated only after ElaborateFromConfigBody
354 }
355
356 func (d *wholeBodyDiagnostic) ElaborateFromConfigBody(body hcl.Body) Diagnostic {
357 if d.subject != nil {
358 // Don't modify an already-elaborated diagnostic.
359 return d
360 }
361
362 ret := *d
363 rng := SourceRangeFromHCL(body.MissingItemRange())
364 ret.subject = &rng
365 return &ret
366 }
367
368 func (d *wholeBodyDiagnostic) Source() Source {
369 return Source{
370 Subject: d.subject,
371 }
372 }