hcl: AbsTraversalForExpr and RelTraversalForExpr

These functions permit a calling application to recognize when an
expression represents a static absolute traversal and obtain that
traversal. This allows for the unusual-but-valid case where an application
wishes to access the expression source rather than its resulting value,
when the expression source is something that can be understood as a
traversal.

An example use-case is an attribute that takes a list of other attributes
it depends on, expressed as traversals. In this case the calling
application needs to access the attribute names themselves rather than
their values, e.g. to build some sort of dependency graph to gradually
populate the scope for evaluation.
This commit is contained in:
Martin Atkins 2018-01-12 22:58:55 -08:00
parent 44bad6dbf5
commit 0949d55133
6 changed files with 232 additions and 1 deletions

View File

@ -70,6 +70,11 @@ func (e *ScopeTraversalExpr) StartRange() hcl.Range {
return e.SrcRange
}
// Implementation for hcl.AbsTraversalForExpr.
func (e *ScopeTraversalExpr) AsTraversal() hcl.Traversal {
return e.Traversal
}
// RelativeTraversalExpr is an Expression that retrieves a value from another
// value using a _relative_ traversal.
type RelativeTraversalExpr struct {

View File

@ -1087,3 +1087,17 @@ func TestFunctionCallExprValue(t *testing.T) {
})
}
}
func TestExpressionAsTraversal(t *testing.T) {
expr, _ := ParseExpression([]byte("a.b[0]"), "", hcl.Pos{})
traversal, diags := hcl.AbsTraversalForExpr(expr)
if len(diags) != 0 {
t.Fatalf("unexpected diagnostics")
}
if len(traversal) != 3 {
t.Fatalf("wrong traversal %#v; want length 3", traversal)
}
if traversal.RootName() != "a" {
t.Fatalf("wrong root name %q; want %q", traversal.RootName(), "a")
}
}

View File

@ -3,8 +3,8 @@ package json
import (
"fmt"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
@ -353,3 +353,20 @@ func (e *expression) Range() hcl.Range {
func (e *expression) StartRange() hcl.Range {
return e.src.StartRange()
}
// Implementation for hcl.AbsTraversalForExpr.
func (e *expression) AsTraversal() hcl.Traversal {
// In JSON-based syntax a traversal is given as a string containing
// traversal syntax as defined by hclsyntax.ParseTraversalAbs.
switch v := e.src.(type) {
case *stringVal:
traversal, diags := hclsyntax.ParseTraversalAbs([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
if diags.HasErrors() {
return nil
}
return traversal
default:
return nil
}
}

View File

@ -741,3 +741,15 @@ func TestJustAttributes(t *testing.T) {
})
}
}
func TestExpressionAsTraversal(t *testing.T) {
e := &expression{
src: &stringVal{
Value: "foo.bar[0]",
},
}
traversal := e.AsTraversal()
if len(traversal) != 3 {
t.Fatalf("incorrect traversal %#v; want length 3", traversal)
}
}

55
hcl/traversal_for_expr.go Normal file
View File

@ -0,0 +1,55 @@
package hcl
// AbsTraversalForExpr attempts to interpret the given expression as
// an absolute traversal, or returns error diagnostic(s) if that is
// not possible for the given expression.
//
// A particular Expression implementation can support this function by
// offering a method called AsTraversal that takes no arguments and
// returns either a valid absolute traversal or nil to indicate that
// no traversal is possible.
//
// In most cases the calling application is interested in the value
// that results from an expression, but in rarer cases the application
// needs to see the the name of the variable and subsequent
// attributes/indexes itself, for example to allow users to give references
// to the variables themselves rather than to their values. An implementer
// of this function should at least support attribute and index steps.
func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
type asTraversal interface {
AsTraversal() Traversal
}
if asT, supported := expr.(asTraversal); supported {
if traversal := asT.AsTraversal(); traversal != nil {
return traversal, nil
}
}
return nil, Diagnostics{
&Diagnostic{
Severity: DiagError,
Summary: "Invalid expression",
Detail: "A static variable reference is required.",
Subject: expr.Range().Ptr(),
},
}
}
// RelTraversalForExpr is similar to AbsTraversalForExpr but it returns
// a relative traversal instead. Due to the nature of ZCL expressions, the
// first element of the returned traversal is always a TraverseAttr, and
// then it will be followed by zero or more other expressions.
//
// Any expression accepted by AbsTraversalForExpr is also accepted by
// RelTraversalForExpr.
func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
traversal, diags := AbsTraversalForExpr(expr)
if len(traversal) > 0 {
root := traversal[0].(TraverseRoot)
traversal[0] = TraverseAttr{
Name: root.Name,
SrcRange: root.SrcRange,
}
}
return traversal, diags
}

View File

@ -0,0 +1,128 @@
package hcl
import (
"testing"
)
type asTraversalSupported struct {
staticExpr
RootName string
}
type asTraversalNotSupported struct {
staticExpr
}
type asTraversalDeclined struct {
staticExpr
}
func (e asTraversalSupported) AsTraversal() Traversal {
return Traversal{
TraverseRoot{
Name: e.RootName,
},
}
}
func (e asTraversalDeclined) AsTraversal() Traversal {
return nil
}
func TestAbsTraversalForExpr(t *testing.T) {
tests := []struct {
Expr Expression
WantRootName string
}{
{
asTraversalSupported{RootName: "foo"},
"foo",
},
{
asTraversalNotSupported{},
"",
},
{
asTraversalDeclined{},
"",
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
got, diags := AbsTraversalForExpr(test.Expr)
switch {
case got != nil:
if test.WantRootName == "" {
t.Fatalf("traversal was returned; want error")
}
if len(got) != 1 {
t.Fatalf("wrong traversal length %d; want 1", len(got))
}
gotRoot, ok := got[0].(TraverseRoot)
if !ok {
t.Fatalf("first traversal step is %T; want hcl.TraverseRoot", got[0])
}
if gotRoot.Name != test.WantRootName {
t.Errorf("wrong root name %q; want %q", gotRoot.Name, test.WantRootName)
}
default:
if !diags.HasErrors() {
t.Errorf("returned nil traversal without error diagnostics")
}
if test.WantRootName != "" {
t.Errorf("traversal was not returned; want TraverseRoot(%q)", test.WantRootName)
}
}
})
}
}
func TestRelTraversalForExpr(t *testing.T) {
tests := []struct {
Expr Expression
WantFirstName string
}{
{
asTraversalSupported{RootName: "foo"},
"foo",
},
{
asTraversalNotSupported{},
"",
},
{
asTraversalDeclined{},
"",
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
got, diags := RelTraversalForExpr(test.Expr)
switch {
case got != nil:
if test.WantFirstName == "" {
t.Fatalf("traversal was returned; want error")
}
if len(got) != 1 {
t.Fatalf("wrong traversal length %d; want 1", len(got))
}
gotRoot, ok := got[0].(TraverseAttr)
if !ok {
t.Fatalf("first traversal step is %T; want hcl.TraverseAttr", got[0])
}
if gotRoot.Name != test.WantFirstName {
t.Errorf("wrong root name %q; want %q", gotRoot.Name, test.WantFirstName)
}
default:
if !diags.HasErrors() {
t.Errorf("returned nil traversal without error diagnostics")
}
if test.WantFirstName != "" {
t.Errorf("traversal was not returned; want TraverseAttr(%q)", test.WantFirstName)
}
}
})
}
}