hcl/traversal.go
Martin Atkins 6c4344623b Unfold the "hcl" directory up into the root
The main HCL package is more visible this way, and so it's easier than
having to pick it out from dozens of other package directories.
2019-09-09 16:08:19 -07:00

294 lines
7.9 KiB
Go

package hcl
import (
"fmt"
"github.com/zclconf/go-cty/cty"
)
// A Traversal is a description of traversing through a value through a
// series of operations such as attribute lookup, index lookup, etc.
//
// It is used to look up values in scopes, for example.
//
// The traversal operations are implementations of interface Traverser.
// This is a closed set of implementations, so the interface cannot be
// implemented from outside this package.
//
// A traversal can be absolute (its first value is a symbol name) or relative
// (starts from an existing value).
type Traversal []Traverser
// TraversalJoin appends a relative traversal to an absolute traversal to
// produce a new absolute traversal.
func TraversalJoin(abs Traversal, rel Traversal) Traversal {
if abs.IsRelative() {
panic("first argument to TraversalJoin must be absolute")
}
if !rel.IsRelative() {
panic("second argument to TraversalJoin must be relative")
}
ret := make(Traversal, len(abs)+len(rel))
copy(ret, abs)
copy(ret[len(abs):], rel)
return ret
}
// TraverseRel applies the receiving traversal to the given value, returning
// the resulting value. This is supported only for relative traversals,
// and will panic if applied to an absolute traversal.
func (t Traversal) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
if !t.IsRelative() {
panic("can't use TraverseRel on an absolute traversal")
}
current := val
var diags Diagnostics
for _, tr := range t {
var newDiags Diagnostics
current, newDiags = tr.TraversalStep(current)
diags = append(diags, newDiags...)
if newDiags.HasErrors() {
return cty.DynamicVal, diags
}
}
return current, diags
}
// TraverseAbs applies the receiving traversal to the given eval context,
// returning the resulting value. This is supported only for absolute
// traversals, and will panic if applied to a relative traversal.
func (t Traversal) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
if t.IsRelative() {
panic("can't use TraverseAbs on a relative traversal")
}
split := t.SimpleSplit()
root := split.Abs[0].(TraverseRoot)
name := root.Name
thisCtx := ctx
hasNonNil := false
for thisCtx != nil {
if thisCtx.Variables == nil {
thisCtx = thisCtx.parent
continue
}
hasNonNil = true
val, exists := thisCtx.Variables[name]
if exists {
return split.Rel.TraverseRel(val)
}
thisCtx = thisCtx.parent
}
if !hasNonNil {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Variables not allowed",
Detail: "Variables may not be used here.",
Subject: &root.SrcRange,
},
}
}
suggestions := make([]string, 0, len(ctx.Variables))
thisCtx = ctx
for thisCtx != nil {
for k := range thisCtx.Variables {
suggestions = append(suggestions, k)
}
thisCtx = thisCtx.parent
}
suggestion := nameSuggestion(name, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Unknown variable",
Detail: fmt.Sprintf("There is no variable named %q.%s", name, suggestion),
Subject: &root.SrcRange,
},
}
}
// IsRelative returns true if the receiver is a relative traversal, or false
// otherwise.
func (t Traversal) IsRelative() bool {
if len(t) == 0 {
return true
}
if _, firstIsRoot := t[0].(TraverseRoot); firstIsRoot {
return false
}
return true
}
// SimpleSplit returns a TraversalSplit where the name lookup is the absolute
// part and the remainder is the relative part. Supported only for
// absolute traversals, and will panic if applied to a relative traversal.
//
// This can be used by applications that have a relatively-simple variable
// namespace where only the top-level is directly populated in the scope, with
// everything else handled by relative lookups from those initial values.
func (t Traversal) SimpleSplit() TraversalSplit {
if t.IsRelative() {
panic("can't use SimpleSplit on a relative traversal")
}
return TraversalSplit{
Abs: t[0:1],
Rel: t[1:],
}
}
// RootName returns the root name for a absolute traversal. Will panic if
// called on a relative traversal.
func (t Traversal) RootName() string {
if t.IsRelative() {
panic("can't use RootName on a relative traversal")
}
return t[0].(TraverseRoot).Name
}
// SourceRange returns the source range for the traversal.
func (t Traversal) SourceRange() Range {
if len(t) == 0 {
// Nothing useful to return here, but we'll return something
// that's correctly-typed at least.
return Range{}
}
return RangeBetween(t[0].SourceRange(), t[len(t)-1].SourceRange())
}
// TraversalSplit represents a pair of traversals, the first of which is
// an absolute traversal and the second of which is relative to the first.
//
// This is used by calling applications that only populate prefixes of the
// traversals in the scope, with Abs representing the part coming from the
// scope and Rel representing the remaining steps once that part is
// retrieved.
type TraversalSplit struct {
Abs Traversal
Rel Traversal
}
// TraverseAbs traverses from a scope to the value resulting from the
// absolute traversal.
func (t TraversalSplit) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
return t.Abs.TraverseAbs(ctx)
}
// TraverseRel traverses from a given value, assumed to be the result of
// TraverseAbs on some scope, to a final result for the entire split traversal.
func (t TraversalSplit) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
return t.Rel.TraverseRel(val)
}
// Traverse is a convenience function to apply TraverseAbs followed by
// TraverseRel.
func (t TraversalSplit) Traverse(ctx *EvalContext) (cty.Value, Diagnostics) {
v1, diags := t.TraverseAbs(ctx)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
v2, newDiags := t.TraverseRel(v1)
diags = append(diags, newDiags...)
return v2, diags
}
// Join concatenates together the Abs and Rel parts to produce a single
// absolute traversal.
func (t TraversalSplit) Join() Traversal {
return TraversalJoin(t.Abs, t.Rel)
}
// RootName returns the root name for the absolute part of the split.
func (t TraversalSplit) RootName() string {
return t.Abs.RootName()
}
// A Traverser is a step within a Traversal.
type Traverser interface {
TraversalStep(cty.Value) (cty.Value, Diagnostics)
SourceRange() Range
isTraverserSigil() isTraverser
}
// Embed this in a struct to declare it as a Traverser
type isTraverser struct {
}
func (tr isTraverser) isTraverserSigil() isTraverser {
return isTraverser{}
}
// TraverseRoot looks up a root name in a scope. It is used as the first step
// of an absolute Traversal, and cannot itself be traversed directly.
type TraverseRoot struct {
isTraverser
Name string
SrcRange Range
}
// TraversalStep on a TraverseName immediately panics, because absolute
// traversals cannot be directly traversed.
func (tn TraverseRoot) TraversalStep(cty.Value) (cty.Value, Diagnostics) {
panic("Cannot traverse an absolute traversal")
}
func (tn TraverseRoot) SourceRange() Range {
return tn.SrcRange
}
// TraverseAttr looks up an attribute in its initial value.
type TraverseAttr struct {
isTraverser
Name string
SrcRange Range
}
func (tn TraverseAttr) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
return GetAttr(val, tn.Name, &tn.SrcRange)
}
func (tn TraverseAttr) SourceRange() Range {
return tn.SrcRange
}
// TraverseIndex applies the index operation to its initial value.
type TraverseIndex struct {
isTraverser
Key cty.Value
SrcRange Range
}
func (tn TraverseIndex) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
return Index(val, tn.Key, &tn.SrcRange)
}
func (tn TraverseIndex) SourceRange() Range {
return tn.SrcRange
}
// TraverseSplat applies the splat operation to its initial value.
type TraverseSplat struct {
isTraverser
Each Traversal
SrcRange Range
}
func (tn TraverseSplat) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
panic("TraverseSplat not yet implemented")
}
func (tn TraverseSplat) SourceRange() Range {
return tn.SrcRange
}