Skip to content

Commit 1f5c492

Browse files
ElaborateFromConfigBody handles deeply nested paths more precisely
1 parent 7f79371 commit 1f5c492

File tree

5 files changed

+136
-93
lines changed

5 files changed

+136
-93
lines changed

internal/deprecation/deprecation.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/hashicorp/hcl/v2"
1010
"github.com/hashicorp/terraform/internal/addrs"
11+
"github.com/hashicorp/terraform/internal/configs/configschema"
1112
"github.com/hashicorp/terraform/internal/lang/marks"
1213
"github.com/hashicorp/terraform/internal/tfdiags"
1314
"github.com/zclconf/go-cty/cty"
@@ -85,7 +86,7 @@ func (d *Deprecations) deprecationMarksToDiagnostics(deprecationMarks []marks.De
8586
// ValidateAsConfig checks the given value for deprecation marks and returns diagnostics
8687
// for each deprecation found, unless deprecation warnings are suppressed for the given module.
8788
// It checks for deeply nested deprecation marks as well.
88-
func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tfdiags.Diagnostics {
89+
func (d *Deprecations) ValidateAsConfig(value cty.Value, schema *configschema.Block, module addrs.Module) tfdiags.Diagnostics {
8990
var diags tfdiags.Diagnostics
9091
_, pvms := value.UnmarkDeepWithPaths()
9192

internal/terraform/node_action_instance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (n *NodeActionDeclarationInstance) Execute(ctx EvalContext, _ walkOperation
7171
valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema)
7272
diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String()))
7373

74-
deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath())
74+
deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Schema.ConfigSchema, n.ModulePath())
7575
diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String()))
7676

7777
if diags.HasErrors() {

internal/terraform/node_action_partialexp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (n *NodeActionDeclarationPartialExpanded) Execute(ctx EvalContext, op walkO
6969
return diags
7070
}
7171

72-
deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ActionAddr().Module)
72+
deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Schema.ConfigSchema, n.ActionAddr().Module)
7373
diags = diags.Append(deprecationDiags)
7474
if diags.HasErrors() {
7575
return diags

internal/tfdiags/contextual.go

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string
156156
// presence of errors where performance isn't a concern.
157157

158158
traverse := d.attrPath[:]
159-
final := d.attrPath[len(d.attrPath)-1]
160159

161160
// Index should never be the first step
162161
// as indexing of top blocks (such as resources & data sources)
@@ -167,54 +166,19 @@ func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string
167166
return &ret
168167
}
169168

170-
// Process index separately
171-
idxStep, hasIdx := final.(cty.IndexStep)
172-
if hasIdx {
173-
final = d.attrPath[len(d.attrPath)-2]
174-
traverse = d.attrPath[:len(d.attrPath)-1]
175-
}
176-
177169
// If we have more than one step after removing index
178170
// then we'll first try to traverse to a child body
179171
// corresponding to the requested path.
172+
remaining := traverse
180173
if len(traverse) > 1 {
181-
body = traversePathSteps(traverse, body)
174+
body, remaining = getDeepestBodyFromPath(body, traverse)
182175
}
183176

184177
// Default is to indicate a missing item in the deepest body we reached
185178
// while traversing.
186-
subject := SourceRangeFromHCL(body.MissingItemRange())
179+
subject := SourceRangeFromHCL(rangeOfDeepestAttributeValueFromPath(body, remaining))
187180
ret.subject = &subject
188181

189-
// Once we get here, "final" should be a GetAttr step that maps to an
190-
// attribute in our current body.
191-
finalStep, isAttr := final.(cty.GetAttrStep)
192-
if !isAttr {
193-
return &ret
194-
}
195-
196-
content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
197-
Attributes: []hcl.AttributeSchema{
198-
{
199-
Name: finalStep.Name,
200-
Required: true,
201-
},
202-
},
203-
})
204-
if contentDiags.HasErrors() {
205-
return &ret
206-
}
207-
208-
if attr, ok := content.Attributes[finalStep.Name]; ok {
209-
hclRange := attr.Expr.Range()
210-
if hasIdx {
211-
// Try to be more precise by finding index range
212-
hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr)
213-
}
214-
subject = SourceRangeFromHCL(hclRange)
215-
ret.subject = &subject
216-
}
217-
218182
return &ret
219183
}
220184

@@ -243,13 +207,15 @@ func (d *attributeDiagnostic) Equals(otherDiag ComparableDiagnostic) bool {
243207
return sourceRangeEquals(d.subject, od.subject)
244208
}
245209

246-
func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
210+
func getDeepestBodyFromPath(body hcl.Body, traverse []cty.PathStep) (hcl.Body, []cty.PathStep) {
211+
lastProcessedIndex := -1
212+
213+
LOOP:
247214
for i := 0; i < len(traverse); i++ {
248215
step := traverse[i]
249216

250217
switch tStep := step.(type) {
251218
case cty.GetAttrStep:
252-
253219
var next cty.PathStep
254220
if i < (len(traverse) - 1) {
255221
next = traverse[i+1]
@@ -281,7 +247,7 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
281247
},
282248
})
283249
if contentDiags.HasErrors() {
284-
return body
250+
break LOOP
285251
}
286252
filtered := make([]*hcl.Block, 0, len(content.Blocks))
287253
for _, block := range content.Blocks {
@@ -291,22 +257,24 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
291257
}
292258
if len(filtered) == 0 {
293259
// Step doesn't refer to a block
294-
continue
260+
break LOOP
295261
}
296262

297263
switch indexType {
298264
case cty.NilType: // no index at all
299265
if len(filtered) != 1 {
300-
return body
266+
break LOOP
301267
}
302268
body = filtered[0].Body
269+
lastProcessedIndex = i
303270
case cty.Number:
304271
var idx int
305272
err := gocty.FromCtyValue(indexVal, &idx)
306273
if err != nil || idx >= len(filtered) {
307-
return body
274+
break LOOP
308275
}
309276
body = filtered[idx].Body
277+
lastProcessedIndex = i
310278
case cty.String:
311279
key := indexVal.AsString()
312280
var block *hcl.Block
@@ -319,56 +287,109 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
319287
if block == nil {
320288
// No block with this key, so we'll just indicate a
321289
// missing item in the containing block.
322-
return body
290+
break LOOP
323291
}
324292
body = block.Body
293+
lastProcessedIndex = i
325294
default:
326295
// Should never happen, because only string and numeric indices
327296
// are supported by cty collections.
328-
return body
297+
break LOOP
329298
}
330299

331300
default:
332301
// For any other kind of step, we'll just return our current body
333302
// as the subject and accept that this is a little inaccurate.
334-
return body
303+
break LOOP
335304
}
336305
}
337-
return body
306+
return body, traverse[lastProcessedIndex+1:]
338307
}
339308

340-
func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range {
341-
switch idxStep.Key.Type() {
342-
case cty.Number:
343-
var idx int
344-
err := gocty.FromCtyValue(idxStep.Key, &idx)
345-
items, diags := hcl.ExprList(attr.Expr)
346-
if diags.HasErrors() {
347-
return attr.Expr.Range()
309+
func rangeOfDeepestAttributeValueFromPath(body hcl.Body, traverse cty.Path) hcl.Range {
310+
if len(traverse) == 0 {
311+
return body.MissingItemRange()
312+
}
313+
// First we need to use the first traverse item to get the final attribute
314+
// expression.
315+
current, rest := traverse[0], traverse[1:]
316+
317+
currentGetAttr, ok := current.(cty.GetAttrStep)
318+
if !ok {
319+
// If the remaining basis is not an attribute access something went wrong.
320+
// We can't do anything better than returning the bodies missing item range.
321+
return body.MissingItemRange()
322+
}
323+
324+
content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
325+
Attributes: []hcl.AttributeSchema{
326+
{
327+
Name: currentGetAttr.Name,
328+
Required: true,
329+
},
330+
},
331+
})
332+
if contentDiags.HasErrors() {
333+
return body.MissingItemRange()
334+
}
335+
attr, ok := content.Attributes[currentGetAttr.Name]
336+
if !ok {
337+
// We could not find the attribute, this should have emitted a diag above, but just in case
338+
return body.MissingItemRange()
339+
}
340+
341+
// Now we need to loop through the rest of the path and progressively introspect
342+
// the HCL expression.
343+
currentExpr := attr.Expr
344+
345+
STEP_ITERATION:
346+
for _, step := range rest {
347+
// We treat cty.IndexStep[type=String] and cty.GetAttrStep the same, so we just
348+
// need to deal with list indexes first
349+
if idxStep, ok := step.(cty.IndexStep); ok && idxStep.Key.Type() == cty.Number {
350+
var idx int
351+
err := gocty.FromCtyValue(idxStep.Key, &idx)
352+
items, diags := hcl.ExprList(currentExpr)
353+
if diags.HasErrors() {
354+
return currentExpr.Range()
355+
}
356+
if err != nil || idx >= len(items) {
357+
return attr.NameRange
358+
}
359+
currentExpr = items[idx]
360+
continue STEP_ITERATION
348361
}
349-
if err != nil || idx >= len(items) {
350-
return attr.NameRange
362+
363+
var stepKey string
364+
switch s := step.(type) {
365+
case cty.GetAttrStep:
366+
stepKey = s.Name
367+
case cty.IndexStep:
368+
stepKey = s.Key.AsString()
369+
default: // should not happen
370+
return currentExpr.Range()
351371
}
352-
return items[idx].Range()
353-
case cty.String:
354-
pairs, diags := hcl.ExprMap(attr.Expr)
372+
373+
pairs, diags := hcl.ExprMap(currentExpr)
355374
if diags.HasErrors() {
356-
return attr.Expr.Range()
375+
return currentExpr.Range()
357376
}
358-
stepKey := idxStep.Key.AsString()
377+
359378
for _, kvPair := range pairs {
360379
key, diags := kvPair.Key.Value(nil)
361380
if diags.HasErrors() {
362-
return attr.Expr.Range()
381+
return currentExpr.Range()
363382
}
364383
if key.AsString() == stepKey {
365-
startRng := kvPair.Value.StartRange()
366-
return startRng
384+
currentExpr = kvPair.Value
385+
continue STEP_ITERATION
367386
}
368387
}
388+
// If we could not find the item return early
369389
return attr.NameRange
370390
}
371-
return attr.Expr.Range()
391+
392+
return currentExpr.Range()
372393
}
373394

374395
func (d *attributeDiagnostic) Source() Source {

0 commit comments

Comments
 (0)