From 34e27c038a4418ef2fb345bfe8fe907dab6620fa Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 27 Jan 2018 09:03:44 -0800 Subject: [PATCH] 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. --- ext/dynblock/expr_wrap.go | 26 ++----------- hcl/expr_list.go | 11 +++++- hcl/expr_unwrap.go | 68 ++++++++++++++++++++++++++++++++++ hcl/traversal_for_expr.go | 11 +++++- hcl/traversal_for_expr_test.go | 23 ++++++++++++ 5 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 hcl/expr_unwrap.go diff --git a/ext/dynblock/expr_wrap.go b/ext/dynblock/expr_wrap.go index f29d5a6..6916fc1 100644 --- a/ext/dynblock/expr_wrap.go +++ b/ext/dynblock/expr_wrap.go @@ -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 } diff --git a/hcl/expr_list.go b/hcl/expr_list.go index b06b197..d05cca0 100644 --- a/hcl/expr_list.go +++ b/hcl/expr_list.go @@ -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 } diff --git a/hcl/expr_unwrap.go b/hcl/expr_unwrap.go new file mode 100644 index 0000000..6d5d205 --- /dev/null +++ b/hcl/expr_unwrap.go @@ -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 + } + } +} diff --git a/hcl/traversal_for_expr.go b/hcl/traversal_for_expr.go index 4d2bd47..86fc624 100644 --- a/hcl/traversal_for_expr.go +++ b/hcl/traversal_for_expr.go @@ -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 } diff --git a/hcl/traversal_for_expr_test.go b/hcl/traversal_for_expr_test.go index c6ba29a..6fc9f79 100644 --- a/hcl/traversal_for_expr_test.go +++ b/hcl/traversal_for_expr_test.go @@ -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 {