hcl: UnwrapExpression and UnwrapExpressionUntil

A pattern has emerged of wrapping Expression instances with other
Expressions in order to subtly modify their behavior. A key example of
this is in ext/dynblock, where wrap an expression in order to introduce
our additional iteration variable for expressions in dynamic blocks.

Rather than having each wrapper expression implement wrapping
implementations for our various syntax-level-analysis functions (like
ExprList and AbsTraversalForExpr), instead we define a standard mechanism
to unwrap expressions back to the lowest-level object -- usually an AST
node -- and then use this in all of our analyses that look at the
expression's structure rather than its value.
This commit is contained in:
Martin Atkins 2018-01-27 09:03:44 -08:00
parent 60b539d5d7
commit 34e27c038a
5 changed files with 113 additions and 26 deletions

View File

@ -35,26 +35,8 @@ func (e exprWrap) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return e.Expression.Value(extCtx)
}
// Passthrough implementation for hcl.ExprList
func (e exprWrap) ExprList() []hcl.Expression {
type exprList interface {
ExprList() []hcl.Expression
}
if el, supported := e.Expression.(exprList); supported {
return el.ExprList()
}
return nil
}
// Passthrough implementation for hcl.AbsTraversalForExpr and hcl.RelTraversalForExpr
func (e exprWrap) AsTraversal() hcl.Traversal {
type asTraversal interface {
AsTraversal() hcl.Traversal
}
if at, supported := e.Expression.(asTraversal); supported {
return at.AsTraversal()
}
return nil
// UnwrapExpression returns the expression being wrapped by this instance.
// This allows the original expression to be recovered by hcl.UnwrapExpression.
func (e exprWrap) UnwrapExpression() hcl.Expression {
return e.Expression
}

View File

@ -8,13 +8,20 @@ package hcl
// A particular Expression implementation can support this function by
// offering a method called ExprList that takes no arguments and returns
// []Expression. This method should return nil if a static list cannot
// be extracted.
// be extracted. Alternatively, an implementation can support
// UnwrapExpression to delegate handling of this function to a wrapped
// Expression object.
func ExprList(expr Expression) ([]Expression, Diagnostics) {
type exprList interface {
ExprList() []Expression
}
if exL, supported := expr.(exprList); supported {
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
_, supported := expr.(exprList)
return supported
})
if exL, supported := physExpr.(exprList); supported {
if list := exL.ExprList(); list != nil {
return list, nil
}

68
hcl/expr_unwrap.go Normal file
View File

@ -0,0 +1,68 @@
package hcl
type unwrapExpression interface {
UnwrapExpression() Expression
}
// UnwrapExpression removes any "wrapper" expressions from the given expression,
// to recover the representation of the physical expression given in source
// code.
//
// Sometimes wrapping expressions are used to modify expression behavior, e.g.
// in extensions that need to make some local variables available to certain
// sub-trees of the configuration. This can make it difficult to reliably
// type-assert on the physical AST types used by the underlying syntax.
//
// Unwrapping an expression may modify its behavior by stripping away any
// additional constraints or capabilities being applied to the Value and
// Variables methods, so this function should generally only be used prior
// to operations that concern themselves with the static syntax of the input
// configuration, and not with the effective value of the expression.
//
// Wrapper expression types must support unwrapping by implementing a method
// called UnwrapExpression that takes no arguments and returns the embedded
// Expression. Implementations of this method should peel away only one level
// of wrapping, if multiple are present. This method may return nil to
// indicate _dynamically_ that no wrapped expression is available, for
// expression types that might only behave as wrappers in certain cases.
func UnwrapExpression(expr Expression) Expression {
for {
unwrap, wrapped := expr.(unwrapExpression)
if !wrapped {
return expr
}
innerExpr := unwrap.UnwrapExpression()
if innerExpr == nil {
return expr
}
expr = innerExpr
}
}
// UnwrapExpressionUntil is similar to UnwrapExpression except it gives the
// caller an opportunity to test each level of unwrapping to see each a
// particular expression is accepted.
//
// This could be used, for example, to unwrap until a particular other
// interface is satisfied, regardless of wrap wrapping level it is satisfied
// at.
//
// The given callback function must return false to continue wrapping, or
// true to accept and return the proposed expression given. If the callback
// function rejects even the final, physical expression then the result of
// this function is nil.
func UnwrapExpressionUntil(expr Expression, until func(Expression) bool) Expression {
for {
if until(expr) {
return expr
}
unwrap, wrapped := expr.(unwrapExpression)
if !wrapped {
return nil
}
expr = unwrap.UnwrapExpression()
if expr == nil {
return nil
}
}
}

View File

@ -7,7 +7,9 @@ package hcl
// 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.
// no traversal is possible. Alternatively, an implementation can support
// UnwrapExpression to delegate handling of this function to a wrapped
// Expression object.
//
// In most cases the calling application is interested in the value
// that results from an expression, but in rarer cases the application
@ -20,7 +22,12 @@ func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
AsTraversal() Traversal
}
if asT, supported := expr.(asTraversal); supported {
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
_, supported := expr.(asTraversal)
return supported
})
if asT, supported := physExpr.(asTraversal); supported {
if traversal := asT.AsTraversal(); traversal != nil {
return traversal, nil
}

View File

@ -17,6 +17,11 @@ type asTraversalDeclined struct {
staticExpr
}
type asTraversalWrappedDelegated struct {
original Expression
staticExpr
}
func (e asTraversalSupported) AsTraversal() Traversal {
return Traversal{
TraverseRoot{
@ -29,6 +34,10 @@ func (e asTraversalDeclined) AsTraversal() Traversal {
return nil
}
func (e asTraversalWrappedDelegated) UnwrapExpression() Expression {
return e.original
}
func TestAbsTraversalForExpr(t *testing.T) {
tests := []struct {
Expr Expression
@ -46,6 +55,20 @@ func TestAbsTraversalForExpr(t *testing.T) {
asTraversalDeclined{},
"",
},
{
asTraversalWrappedDelegated{
original: asTraversalSupported{RootName: "foo"},
},
"foo",
},
{
asTraversalWrappedDelegated{
original: asTraversalWrappedDelegated{
original: asTraversalSupported{RootName: "foo"},
},
},
"foo",
},
}
for _, test := range tests {