Functions which accept multiple parameters can be called with the
expansion operator, `...`. When doing so, we must unmark the expanded
argument value before transforming it into a collection of function
arguments. To ensure that any marks applied to the collection are
preserved, we transfer the collection marks to the individual elements
as we build the argument list.
So far the expression parentheses syntax has been handled entirely in the
parser and has been totally invisible in the AST. That's fine for typical
expression evaluation, but over the years it's led to a few quirky
behaviors in less common situations where we've assumed that all
expressions are covered by the AST itself or by the source ranges that the
AST captures.
In particular, hclwrite assumes that all expressions will have source
ranges that cover their tokens, and it generates an incorrect physical
syntax tree when the AST doesn't uphold that.
After resisting through a few other similar bugs, this commit finally
introduces an explicit AST node for parentheses, which makes the
parentheses explicit in the AST and captures the larger source range that
includes the TokenOParen and the TokenCParen.
This means that parentheses will now be visible as a distinct node when
walking the AST, as reflected in the updated tests here. That may cause
downstream applications that traverse the tree to exhibit different
behaviors but we're not considering that as a "breaking change" because
the Walk function doesn't make any guarantees about the specific AST
shape.
The rules for the splat operator call for it to return an empty tuple
when its operand is null, but this rule was previously being
overridden by another rule that a value whose type is unknown
causes the operator to return an unknown value of unknown
type.
This was initially reported and discussed in Terraform, under
hashicorp/terraform#26746.
The hclsyntax package permits block labels to be blank quoted strings,
and defers to the application to determine if this is valid or not. This
commit updates hclwrite to allow the same behaviour.
This is in response to an upstream bug report on Terraform, which uses
hclwrite to implement its fmt subcommand. Given a block with a blank
quoted string label, it currently deletes the label when formatting.
This is inconsistent with Terraform's behaviour when parsing HCL, which
treats empty labels differently from missing labels.
Structs can specify the "body" struct tag to capture the entire block
body. This does NOT act as "remain" where leftover fields are
non-erroneous. If you want leftover fields to be allowed, a "remain"
field must ALSO be present.
This is used to capture the full block body that was decoded for the
block. This is useful if you want to ever redecode something differently
(maybe with a different EvalContext) or partially decode something but
redecode the entire value.
If a template expression interpolates values which have marks, we should
apply all of those marks to the output value. This allows template
expressions to function like native cty functions with respect to marks.
It seems to be somewhat common for someone to share HCL code via a forum
or a document and have the well-meaning word processor or CMS replace the
straight quotes with curly quotes, which then lead to confusing errors
when someone copies the result and tries to use it as valid HCL
configuration.
Here we add a special hint for that, giving a tailored error message
instead of the generic "This character is not used within the language"
error message.
HCL has always had some of these special hints implemented here, and they
were originally implemented with special token types to allow the parser
handle them. However, we later refactored to do the check all at once
inside the Lex* family of functions, prior to parsing, so it's now
relatively straightforward to handle it as a special case of TokenInvalid
rather than an entirely new token type. Perhaps later we'll rework the
existing ones to also just use TokenInvalid, but that's a decision for
another day.
All of the other subdivisions of a block were already nodes, but we'd
represented the labels as an undifferentiated set of nodes belonging
directly to the block's child node list.
Now that we support replacing the labels in the public API, that's a good
excuse to refactor this slightly to make the labels their own node. As
well as being consistent with everything else in Block, this also makes
it easier to implement the Block.SetLabels operation because we can
just change the children of the labels node, rather than having to
carefully identify and extract the individual child nodes of the block
that happen to represent labels.
Internally this models the labels in a similar sort of way as the content
of a body, although we've kept the public API directly on the Block type
here because that's a more straightforward model for the use-cases we
currently know and matches better with the API of hcl.Block. This is just
an internal change for consistency.
I also added a few tests for having comments interspersed with labels
while I was here, because that helped to better exercise the new
parseBlockLabels function.
While implementing Block.SetLabels(), I found a new hclwrite parser bug.
The NewBlock() method records positions of TokenOBrace / TokenCBrace.
Nevertheless when generating blocks via hclwrite.ParseConfig(),
they were not recorded.
The position of TokenOBrace is needed for Block.SetLabels(),
so I also fixed this existing bug.
Fixes#338
Add methods to update block type and labels to enable us to refactor HCL
configurations such as renaming Terraform resources.
- `*Block.SetType(typeName string)`
- `*Block.SetLabels(labels []string)`
Some additional notes about SetLabels:
Since we cannot assume that old and new labels are equal in length,
remove old labels and insert new ones before TokenOBrace.
To implement this, I also added the following methods.
- `*nodes.Insert(pos *node, c nodeContent) *node`
- `*nodes.InsertNode(pos *node, n *node) *node`
They are similar to the existing Append / AppendNode,
but insert a node before a given position.