blob: f7ee0bcc75fbf90b6285a6339b90d5ec5e08b793 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package globalref
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/configs/configschema"
)
// walkBlock walks through the block following the given traversal. If it finds
// any invalid steps in the traversal, the walk is halted and the traversal
// until that point is returned. This effectively validates and corrects the
// given traversal.
func walkBlock(block *configschema.Block, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
// then we've reached the end of the traversal, so we won't keep trying
// to walk through the block
return nil
}
// within a block the next step should always be a string attribute, but we
// want to be lenient.
current, ok := coerceToAttribute(traversal[0])
if !ok {
return nil
}
if attr, ok := current.(hcl.TraverseAttr); ok {
// this is the valid case, we're expecting an attribute that's going to
// point to an attribute or a block
if attr, ok := block.Attributes[attr.Name]; ok {
return append(hcl.Traversal{current}, walkAttribute(attr, traversal[1:])...)
}
if block, ok := block.BlockTypes[attr.Name]; ok {
return append(hcl.Traversal{current}, walkBlockType(block, traversal[1:])...)
}
}
// if nothing was triggered, this was an invalid reference so we'll cut the
// traversal here and return nothing.
return nil
}
// walkBlock walks through the block following the given traversal. If it finds
// any invalid steps in the traversal, the walk is halted and the traversal
// until that point is returned. This effectively validates and corrects the
// given traversal.
func walkBlockType(block *configschema.NestedBlock, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
return nil
}
switch block.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
// we don't expect an intermediary step for single or group nested
// blocks, so we can just keep going.
return walkBlock(&block.Block, traversal)
case configschema.NestingList:
// this should be an index type, but we'll tolerate an attribute as long
// as it's a number
current, ok := coerceToIntegerIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkBlock(&block.Block, traversal[1:])...)
case configschema.NestingMap:
// this should be an index type, but we'll tolerate an attribute
current, ok := coerceToStringIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkBlock(&block.Block, traversal[1:])...)
case configschema.NestingSet:
return nil // can't reference into a set
}
// above switch should have been exhaustive
return nil
}
// walkAttribute walks through the attribute following the given traversal. If
// it finds any invalid steps in the traversal, the walk is haled and the
// traversal until that point is returned. This effectively validates and
// corrects the given traversal.
func walkAttribute(attr *configschema.Attribute, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
return nil
}
if attr.NestedType != nil {
return walkNestedAttribute(attr.NestedType, traversal)
}
return walkType(attr.Type, traversal)
}
func walkNestedAttributes(attrs map[string]*configschema.Attribute, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
return nil
}
current, ok := coerceToAttribute(traversal[0])
if !ok {
return nil
}
if attr, ok := attrs[current.(hcl.TraverseAttr).Name]; ok {
return append(hcl.Traversal{current}, walkAttribute(attr, traversal[1:])...)
}
return nil
}
func walkNestedAttribute(attr *configschema.Object, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
return nil
}
switch attr.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
return walkNestedAttributes(attr.Attributes, traversal)
case configschema.NestingList:
// this should be an index type, but we'll tolerate an attribute as long
// as it's a number
current, ok := coerceToIntegerIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkNestedAttributes(attr.Attributes, traversal[1:])...)
case configschema.NestingMap:
// this should be an index type, but we'll tolerate an attribute
current, ok := coerceToStringIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkNestedAttributes(attr.Attributes, traversal[1:])...)
case configschema.NestingSet:
return nil // can't reference into a set
}
// above switch should have been exhaustive
return nil
}
func walkType(t cty.Type, traversal hcl.Traversal) hcl.Traversal {
if len(traversal) == 0 {
return nil
}
switch {
case t.IsPrimitiveType():
return nil // can't traverse into primitives
case t.IsListType():
current, ok := coerceToIntegerIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkType(t.ElementType(), traversal[1:])...)
case t.IsMapType():
current, ok := coerceToStringIndex(traversal[0])
if !ok {
return nil
}
return append(hcl.Traversal{current}, walkType(t.ElementType(), traversal[1:])...)
case t.IsSetType():
return nil // can't traverse into sets
case t.IsObjectType():
current, ok := coerceToAttribute(traversal[0])
if !ok {
return nil
}
key := current.(hcl.TraverseAttr).Name
if !t.HasAttribute(key) {
return nil
}
return append(hcl.Traversal{current}, walkType(t.AttributeType(key), traversal[1:])...)
case t.IsTupleType():
current, ok := coerceToIntegerIndex(traversal[0])
if !ok {
return nil
}
key := current.(hcl.TraverseIndex).Key
if !key.IsKnown() {
return nil // we can't keep traversing if the index is unknown
}
bf := key.AsBigFloat()
if !bf.IsInt() {
return nil
}
ix, _ := bf.Int64()
types := t.TupleElementTypes()
if ix < 0 || ix >= int64(len(types)) {
return nil
}
return append(hcl.Traversal{current}, walkType(types[int(ix)], traversal[1:])...)
}
return nil // the above should have been exhaustive
}
// coerceToAttribute converts the provided step into a hcl.TraverseAttr if
// possible.
func coerceToAttribute(step hcl.Traverser) (hcl.Traverser, bool) {
switch step := step.(type) {
case hcl.TraverseIndex:
if !step.Key.IsKnown() {
// this is the only failure case here - we can't put unknown values
// into attributes so we return false
return step, false
}
name, err := convert.Convert(step.Key, cty.String)
if err != nil {
// integers should be able to be converted into strings, so this
// should never happen
return step, false
}
// all else being good, package this up as an attribute and keep
// going
return hcl.TraverseAttr{
Name: name.AsString(),
SrcRange: step.SrcRange,
}, true
case hcl.TraverseAttr:
return step, true
}
// we'll just give up if we see any other types, but we shouldn't see
// anything except index and attribute traversals by this point.
return step, false
}
// coerceToStringIndex converts the provided step into a string-valued
// hcl.TraverseIndex if possible.
func coerceToStringIndex(step hcl.Traverser) (hcl.Traverser, bool) {
switch step := step.(type) {
case hcl.TraverseIndex:
key, err := convert.Convert(step.Key, cty.String)
if err != nil {
return step, false
}
return hcl.TraverseIndex{
Key: key,
SrcRange: step.SrcRange,
}, true
case hcl.TraverseAttr:
return hcl.TraverseIndex{
Key: cty.StringVal(step.Name),
SrcRange: step.SrcRange,
}, true
}
// we'll just give up if we see any other types, but we shouldn't see
// anything except index and attribute traversals by this point.
return step, false
}
// coerceToIntegerIndex converts the provided step into an int-valued
// hcl.TraverseIndex if possible.
func coerceToIntegerIndex(step hcl.Traverser) (hcl.Traverser, bool) {
switch step := step.(type) {
case hcl.TraverseIndex:
key, err := convert.Convert(step.Key, cty.Number)
if err != nil {
return step, false
}
return hcl.TraverseIndex{
Key: key,
SrcRange: step.SrcRange,
}, true
case hcl.TraverseAttr:
number, err := cty.ParseNumberVal(step.Name)
if err != nil {
return step, false
}
return hcl.TraverseIndex{
Key: number,
SrcRange: step.SrcRange,
}, true
}
// we'll just give up if we see any other types, but we shouldn't see
// anything except index and attribute traversals by this point.
return step, false
}