From c0f39a3f6f7b8bf44e98c71d1f7dbda4d30b0892 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Sat, 7 Feb 2026 00:47:41 -0700 Subject: [PATCH] fix: Support writing keys with invalid chars This actually fixes a few of things related to expressions for object keys: 1. It allows they keys contain characters that aren't valid as identifiers, if the key is a plain string, for example: ":" 2. It removes the need for superflouous parenthesis around expresions in the keys 3. It no longer puts double quotes around an interpolated string in the key. The second two were actually kind of side-affects of my fix for 1. If we want to preserve the previous behavior for 2 and 3, I think it wouldn't be too hard to do though. --- hcl2/reconstructor.py | 26 ++++++++++++++++--- hcl2/transformer.py | 8 +++--- .../terraform-config-json/variables.json | 10 ++++--- test/helpers/terraform-config/variables.tf | 10 ++++--- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/hcl2/reconstructor.py b/hcl2/reconstructor.py index 7f957d7b..ea5ce744 100644 --- a/hcl2/reconstructor.py +++ b/hcl2/reconstructor.py @@ -261,7 +261,6 @@ def _should_add_space(self, rule, current_terminal, is_block_label: bool = False if isinstance(self._last_rule, str) and re.match( r"^__(tuple|arguments)_(star|plus)_.*", self._last_rule ): - # string literals, decimals, and identifiers should always be # preceded by a space if they're following a comma in a tuple or # function arg @@ -362,6 +361,10 @@ def _name_to_identifier(name: str) -> Tree: """Converts a string to a NAME token within an identifier rule.""" return Tree(Token("RULE", "identifier"), [Token("NAME", name)]) + @staticmethod + def _is_valid_identifier(name: str) -> bool: + return re.match(r"^\w[\w\d]*$", name) is not None + @staticmethod def _escape_interpolated_str(interp_s: str) -> str: if interp_s.strip().startswith("<<-") or interp_s.strip().startswith("<<"): @@ -620,14 +623,14 @@ def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]: continue value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1) - k = self._unwrap_interpolation(k) + k = self._transform_value_to_key(k, level + 1) elements.append( Tree( Token("RULE", "object_elem"), [ Tree( Token("RULE", "object_elem_key"), - [Tree(Token("RULE", "identifier"), [Token("NAME", k)])], + [k], ), Token("EQ", " ="), value_expr_term, @@ -732,3 +735,20 @@ def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]: # otherwise, we don't know the type raise RuntimeError(f"Unknown type to transform {type(value)}") + + def _transform_value_to_key(self, value, level) -> Tree: + """ + Convert any value to a suitable key for an object + """ + if self._is_valid_identifier(value): + return self._name_to_identifier(value) + expr = self._transform_value_to_expr_term(value, level) + # If the expression is a string, then return an object_elem_key of the string + if expr.data == "expr_term" and expr.children[0].data == "string": + return expr.children[0] + else: + # Otherwise return an expression + return Tree( + Token("RULE", "object_elem_key_expression"), + [Token("LPAR", "("), expr, Token("RPAR", ")")], + ) diff --git a/hcl2/transformer.py b/hcl2/transformer.py index 382092d6..6f5bd4fe 100644 --- a/hcl2/transformer.py +++ b/hcl2/transformer.py @@ -1,4 +1,5 @@ """A Lark Transformer for transforming a Lark parse tree into a Python dict""" + import json import re import sys @@ -103,9 +104,7 @@ def object_elem(self, args: List) -> Dict: # into a bigger dict that is returned by the "object" function key = str(args[0].children[0]) - if not re.match(r".*?(\${).*}.*", key): - # do not strip quotes of a interpolation string - key = self.strip_quotes(key) + key = self.strip_quotes(key) value = self.to_string_dollar(args[2]) return {key: value} @@ -114,7 +113,8 @@ def object_elem_key_dot_accessor(self, args: List) -> str: return "".join(args) def object_elem_key_expression(self, args: List) -> str: - return self.to_string_dollar("".join(args)) + # the first and third arguments are parentheses + return self.to_string_dollar(args[1]) def object(self, args: List) -> Dict: args = self.strip_new_line_tokens(args) diff --git a/test/helpers/terraform-config-json/variables.json b/test/helpers/terraform-config-json/variables.json index d344902c..2dba548e 100644 --- a/test/helpers/terraform-config-json/variables.json +++ b/test/helpers/terraform-config-json/variables.json @@ -47,9 +47,13 @@ "foo": "${var.account}_bar", "bar": { "baz": 1, - "${(var.account)}": 2, - "${(format(\"key_prefix_%s\", local.foo))}": 3, - "\"prefix_${var.account}:${var.user}_suffix\"": "interpolation" + "${var.account}": 2, + "${format(\"key_prefix_%s\", local.foo)}": 3, + "prefix_${var.account}:${var.user}_suffix": "interpolation", + "${var.start}-mid-${var.end}": 4, + "a:b": 5, + "123": 6, + "${var.x + 1}": 7 }, "tuple": ["${local.foo}"], "empty_tuple": [] diff --git a/test/helpers/terraform-config/variables.tf b/test/helpers/terraform-config/variables.tf index 9f0d6c6b..d6448b94 100644 --- a/test/helpers/terraform-config/variables.tf +++ b/test/helpers/terraform-config/variables.tf @@ -10,9 +10,13 @@ locals { baz : 1 (var.account) : 2 (format("key_prefix_%s", local.foo)) : 3 - "prefix_${var.account}:${var.user}_suffix":"interpolation", + "prefix_${var.account}:${var.user}_suffix" : "interpolation", + "${var.start}-mid-${var.end}" : 4 + "a:b" : 5 + 123 : 6 + (var.x + 1) : 7 } - tuple = [local.foo] + tuple = [local.foo] empty_tuple = [] } @@ -76,7 +80,7 @@ locals { for_whitespace = { for i in [1, 2, 3] : i => - i ... + i... } }