hcl/hclpack/expression.go
Martin Atkins 30da06ec3f hclpack: Implement UnwrapExpression for our expressions
This allows the static analysis functions in the main HCL package to dig
through our wrapper to get the native expression object needed for most
analyses.

For example, this allows an expression with a native expression source
type whose source contains valid tuple constructor syntax to be used with
hcl.ExprList.
2018-11-11 08:34:44 -08:00

161 lines
5.4 KiB
Go

package hclpack
import (
"fmt"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
// Expression is an implementation of hcl.Expression in terms of some raw
// expression source code. The methods of this type will first parse the
// source code and then pass the call through to the real expression that
// is produced.
type Expression struct {
// Source is the raw source code of the expression, which should be parsed
// as the syntax specified by SourceType.
Source []byte
SourceType ExprSourceType
// Range_ and StartRange_ describe the physical extents of the expression
// in the original source code. SourceRange_ is its entire range while
// StartRange is just the tokens that introduce the expression type. For
// simple expression types, SourceRange and StartRange are identical.
Range_, StartRange_ hcl.Range
}
var _ hcl.Expression = (*Expression)(nil)
// Value implements the Value method of hcl.Expression but with the additional
// step of first parsing the expression source code. This implementation is
// unusual in that it can potentially return syntax errors, whereas other
// Value implementations usually work with already-parsed expressions.
func (e *Expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
expr, diags := e.Parse()
if diags.HasErrors() {
return cty.DynamicVal, diags
}
val, moreDiags := expr.Value(ctx)
diags = append(diags, moreDiags...)
return val, diags
}
// Variables implements the Variables method of hcl.Expression but with the
// additional step of first parsing the expression source code.
//
// Since this method cannot return errors, it will return a nil slice if
// parsing fails, indicating that no variables are present. This is okay in
// practice because a subsequent call to Value would fail with syntax errors
// regardless of what variables are in the context.
func (e *Expression) Variables() []hcl.Traversal {
expr, diags := e.Parse()
if diags.HasErrors() {
return nil
}
return expr.Variables()
}
// UnwrapExpression parses and returns the underlying expression, if possible.
//
// This is essentially the same as Parse but without the ability to return an
// error; it is here only to support the static analysis facilities in the
// main HCL package (ExprList, ExprMap, etc). If any error is encountered
// during parsing, the result is a static expression that always returns
// cty.DynamicVal.
//
// This function does not impose any further conversions on the underlying
// expression, so the result may still not be suitable for the static analysis
// functions, depending on the source type of the expression and thus what
// type of physical expression it becomes after decoding.
func (e *Expression) UnwrapExpression() hcl.Expression {
expr, diags := e.Parse()
if diags.HasErrors() {
return hcl.StaticExpr(cty.DynamicVal, e.Range_)
}
return expr
}
func (e *Expression) Range() hcl.Range {
return e.Range_
}
func (e *Expression) StartRange() hcl.Range {
return e.StartRange_
}
// Parse attempts to parse the source code of the receiving expression using
// its indicated source type, returning the expression if possible and any
// diagnostics produced during parsing.
func (e *Expression) Parse() (hcl.Expression, hcl.Diagnostics) {
switch e.SourceType {
case ExprNative:
return hclsyntax.ParseExpression(e.Source, e.Range_.Filename, e.Range_.Start)
case ExprTemplate:
return hclsyntax.ParseTemplate(e.Source, e.Range_.Filename, e.Range_.Start)
case ExprLiteralJSON:
ty, err := ctyjson.ImpliedType(e.Source)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid JSON value",
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
Subject: &e.Range_,
},
}
}
val, err := ctyjson.Unmarshal(e.Source, ty)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid JSON value",
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
Subject: &e.Range_,
},
}
}
return hcl.StaticExpr(val, e.Range_), nil
default:
// This should never happen for a valid Expression.
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid expression source type",
Detail: fmt.Sprintf("Packed version of this expression has an invalid source type %s. This is always a bug.", e.SourceType),
Subject: &e.Range_,
},
}
}
}
func (e *Expression) addRanges(rngs map[hcl.Range]struct{}) {
rngs[e.Range_] = struct{}{}
rngs[e.StartRange_] = struct{}{}
}
// ExprSourceType defines the syntax type used for an expression's source code,
// which is then used to select a suitable parser for it when evaluating.
type ExprSourceType rune
//go:generate stringer -type ExprSourceType
const (
// ExprNative indicates that an expression must be parsed as native
// expression syntax, with hclsyntax.ParseExpression.
ExprNative ExprSourceType = 'N'
// ExprTemplate indicates that an expression must be parsed as native
// template syntax, with hclsyntax.ParseTemplate.
ExprTemplate ExprSourceType = 'T'
// ExprLiteralJSON indicates that an expression must be parsed as JSON and
// treated literally, using cty/json. This can be used when populating
// literal attribute values from a non-HCL source.
ExprLiteralJSON ExprSourceType = 'L'
)