From 9411bec7b18693cc02097f805b3b4ae2d14a334c Mon Sep 17 00:00:00 2001 From: Kamil Kozik Date: Mon, 9 Mar 2026 13:15:57 +0100 Subject: [PATCH] `BaseFormatter` - fixes to complex function args formatting; `FormatterOptions` - dont open empty objects by default --- hcl2/formatter.py | 17 ++++++++++++- .../hcl2_original/function_objects.tf | 22 +++++++++++++++++ .../hcl2_reconstructed/function_objects.tf | 24 +++++++++++++++++++ .../json_reserialized/function_objects.json | 22 +++++++++++++++++ .../json_serialized/function_objects.json | 22 +++++++++++++++++ test/unit/test_formatter.py | 2 +- 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 test/integration/hcl2_original/function_objects.tf create mode 100644 test/integration/hcl2_reconstructed/function_objects.tf create mode 100644 test/integration/json_reserialized/function_objects.json create mode 100644 test/integration/json_serialized/function_objects.json diff --git a/hcl2/formatter.py b/hcl2/formatter.py index c1bac9df..20c3dced 100644 --- a/hcl2/formatter.py +++ b/hcl2/formatter.py @@ -17,6 +17,7 @@ TupleRule, ) from hcl2.rules.expressions import ExprTermRule +from hcl2.rules.functions import FunctionCallRule from hcl2.rules.for_expressions import ( ForTupleExprRule, ForObjectExprRule, @@ -33,7 +34,7 @@ class FormatterOptions: indent_length: int = 2 open_empty_blocks: bool = True - open_empty_objects: bool = True + open_empty_objects: bool = False open_empty_tuples: bool = False vertically_align_attributes: bool = True @@ -125,6 +126,10 @@ def format_tuple_rule(self, rule: TupleRule, indent_level: int = 0): if isinstance(child, (COMMA, LSQB)): # type: ignore[misc] new_children.append(self._build_newline(indent_level)) + # If no trailing comma, add newline before closing bracket + if not isinstance(new_children[-2], NewLineOrCommentRule): + new_children.insert(-1, self._build_newline(indent_level)) + self._deindent_last_line() self._set_children(rule, new_children) @@ -175,9 +180,19 @@ def format_expression(self, rule: ExprTermRule, indent_level: int = 0): elif isinstance(rule.expression, ForObjectExprRule): self.format_forobjectexpr(rule.expression, indent_level) + elif isinstance(rule.expression, FunctionCallRule): + self.format_function_call(rule.expression, indent_level) + elif isinstance(rule.expression, ExprTermRule): self.format_expression(rule.expression, indent_level) + def format_function_call(self, rule: FunctionCallRule, indent_level: int = 0): + """Format a function call by recursively formatting its arguments.""" + if rule.arguments is not None: + for arg in rule.arguments.arguments: + if isinstance(arg, ExprTermRule): + self.format_expression(arg, indent_level) + def format_fortupleexpr(self, expression: ForTupleExprRule, indent_level: int = 0): """Format a for-tuple expression with newlines around clauses.""" for child in expression.children: diff --git a/test/integration/hcl2_original/function_objects.tf b/test/integration/hcl2_original/function_objects.tf new file mode 100644 index 00000000..d9e733b7 --- /dev/null +++ b/test/integration/hcl2_original/function_objects.tf @@ -0,0 +1,22 @@ +variable "object" { + type = object({ + key = string + value = string + }) +} + +variable "nested" { + type = map(object({ + name = string + enabled = bool + })) +} + +variable "multi_arg" { + default = merge({ + a = 1 + b = 2 + }, { + c = 3 + }) +} diff --git a/test/integration/hcl2_reconstructed/function_objects.tf b/test/integration/hcl2_reconstructed/function_objects.tf new file mode 100644 index 00000000..69dac286 --- /dev/null +++ b/test/integration/hcl2_reconstructed/function_objects.tf @@ -0,0 +1,24 @@ +variable "object" { + type = object({ + key = string, + value = string + }) +} + + +variable "nested" { + type = map(object({ + name = string, + enabled = bool + })) +} + + +variable "multi_arg" { + default = merge({ + a = 1, + b = 2 + }, { + c = 3 + }) +} diff --git a/test/integration/json_reserialized/function_objects.json b/test/integration/json_reserialized/function_objects.json new file mode 100644 index 00000000..86b8b131 --- /dev/null +++ b/test/integration/json_reserialized/function_objects.json @@ -0,0 +1,22 @@ +{ + "variable": [ + { + "\"object\"": { + "type": "${object({key = string, value = string})}", + "__is_block__": true + } + }, + { + "\"nested\"": { + "type": "${map(object({name = string, enabled = bool}))}", + "__is_block__": true + } + }, + { + "\"multi_arg\"": { + "default": "${merge({a = 1, b = 2}, {c = 3})}", + "__is_block__": true + } + } + ] +} diff --git a/test/integration/json_serialized/function_objects.json b/test/integration/json_serialized/function_objects.json new file mode 100644 index 00000000..86b8b131 --- /dev/null +++ b/test/integration/json_serialized/function_objects.json @@ -0,0 +1,22 @@ +{ + "variable": [ + { + "\"object\"": { + "type": "${object({key = string, value = string})}", + "__is_block__": true + } + }, + { + "\"nested\"": { + "type": "${map(object({name = string, enabled = bool}))}", + "__is_block__": true + } + }, + { + "\"multi_arg\"": { + "default": "${merge({a = 1, b = 2}, {c = 3})}", + "__is_block__": true + } + } + ] +} diff --git a/test/unit/test_formatter.py b/test/unit/test_formatter.py index eceb1f65..34fadc71 100644 --- a/test/unit/test_formatter.py +++ b/test/unit/test_formatter.py @@ -113,7 +113,7 @@ def test_defaults(self): opts = FormatterOptions() self.assertEqual(opts.indent_length, 2) self.assertTrue(opts.open_empty_blocks) - self.assertTrue(opts.open_empty_objects) + self.assertFalse(opts.open_empty_objects) self.assertFalse(opts.open_empty_tuples) self.assertTrue(opts.vertically_align_attributes) self.assertTrue(opts.vertically_align_object_elements)