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