hclwrite: Allow constructing expressions from raw tokens

We currently have functions for constructing new expressions from either
constant values or from traversals, but some surgical updates require
producing a more complex expression.

In the long run perhaps we'll have some mechanism for constructing valid
expressions via a high-level AST-like API, similar to what we already have
for structural constructs, but as a simpler first step here we add a
mechanism to just write raw tokens directly into an expression, with the
caller being responsible for making sure those tokens represent valid
HCL expression syntax.

Since this new API treats the given tokens as unstructured, the resulting
expression can't fully support the whole of the expression API, but it's
good enough for writing in complex expressions without disturbing existing
content elsewhere in the input file.
This commit is contained in:
Martin Atkins 2019-11-19 14:32:07 -08:00
parent 56bc00a23c
commit 06985781df
3 changed files with 207 additions and 0 deletions

View File

@ -134,6 +134,26 @@ func (b *Body) RemoveBlock(block *Block) bool {
return false
}
// SetAttributeRaw either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block,
// using the given tokens verbatim as the expression.
//
// The same caveats apply to this function as for NewExpressionRaw on which
// it is based. If possible, prefer to use SetAttributeValue or
// SetAttributeTraversal.
func (b *Body) SetAttributeRaw(name string, tokens Tokens) *Attribute {
attr := b.GetAttribute(name)
expr := NewExpressionRaw(tokens)
if attr != nil {
attr.expr = attr.expr.ReplaceWith(expr)
} else {
attr := newAttribute()
attr.init(name, expr)
b.appendItem(attr)
}
return attr
}
// SetAttributeValue either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block.
//

View File

@ -766,6 +766,170 @@ func TestBodySetAttributeTraversal(t *testing.T) {
}
}
func TestBodySetAttributeRaw(t *testing.T) {
tests := []struct {
src string
name string
tokens Tokens
want Tokens
}{
{
"",
"a",
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`true`),
SpacesBefore: 0,
},
},
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
"a = 23\n",
"a",
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`true`),
SpacesBefore: 0,
},
},
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
"b = 23\n",
"a",
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`true`),
SpacesBefore: 0,
},
},
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'b'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNumberLit,
Bytes: []byte("23"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s = %s in %s", test.name, test.tokens.Bytes(), test.src), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
f.Body().SetAttributeRaw(test.name, test.tokens)
got := f.BuildTokens(nil)
format(got)
if !reflect.DeepEqual(got, test.want) {
diff := cmp.Diff(test.want, got)
t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff)
}
})
}
}
func TestBodySetAttributeValueInBlock(t *testing.T) {
src := `service "label1" {
attr1 = "val1"

View File

@ -21,6 +21,29 @@ func newExpression() *Expression {
}
}
// NewExpressionRaw constructs an expression containing the given raw tokens.
//
// There is no automatic validation that the given tokens produce a valid
// expression. Callers of thus function must take care to produce invalid
// expression tokens. Where possible, use the higher-level functions
// NewExpressionLiteral or NewExpressionAbsTraversal instead.
//
// Because NewExpressionRaw does not interpret the given tokens in any way,
// an expression created by NewExpressionRaw will produce an empty result
// for calls to its method Variables, even if the given token sequence
// contains a subslice that would normally be interpreted as a traversal under
// parsing.
func NewExpressionRaw(tokens Tokens) *Expression {
expr := newExpression()
// We copy the tokens here in order to make sure that later mutations
// by the caller don't inadvertently cause our expression to become
// invalid.
copyTokens := make(Tokens, len(tokens))
copy(copyTokens, tokens)
expr.children.AppendUnstructuredTokens(copyTokens)
return expr
}
// NewExpressionLiteral constructs an an expression that represents the given
// literal value.
//