Skip to content

Commit 238d4e5

Browse files
test: cover the reachable paths in negative-literal serialization
The bool-operand test asserted "${-True}", which no HCL input can produce: UnaryOpRule serializes its operand with inside_dollar_string set, and LiteralValueRule yields the string "true" in that context. Assert the helper contract directly instead, and add TestNegatedKeywords for the path a parse actually takes. -1e10 lexes as a single FLOAT_LITERAL and never reaches the unary path. Add the spaced form, which does, and whose operand is a string under preserve_scientific_notation. Pin -0 normalization, and add -0 and -true to the integers round-trip suite. The spaced forms stay out of that fixture because the direct pipeline normalizes the space away, which would break the byte-exact direct-reconstruct assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f06a2e1 commit 238d4e5

6 files changed

Lines changed: 68 additions & 2 deletions

File tree

test/integration/hcl2_original/integers.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ locals {
55
negative_int = -42
66
negative_one = -1
77
negative_large = -9876543210
8+
negative_zero = -0
9+
negated_keyword = -true
810
int_calculation = 105 * 3 / 2
911
int_subtraction = 10 - 3
1012
int_negated_reference = -var.count

test/integration/hcl2_reconstructed/integers.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ locals {
55
negative_int = -42
66
negative_one = -1
77
negative_large = -9876543210
8+
negative_zero = 0
9+
negated_keyword = -true
810
int_calculation = 105 * 3 / 2
911
int_subtraction = 10 - 3
1012
int_negated_reference = -var.count

test/integration/json_reserialized/integers.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"negative_int": -42,
88
"negative_one": -1,
99
"negative_large": -9876543210,
10+
"negative_zero": 0,
11+
"negated_keyword": "${-true}",
1012
"int_calculation": "${105 * 3 / 2}",
1113
"int_subtraction": "${10 - 3}",
1214
"int_negated_reference": "${-var.count}",

test/integration/json_serialized/integers.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"negative_int": -42,
88
"negative_one": -1,
99
"negative_large": -9876543210,
10+
"negative_zero": 0,
11+
"negated_keyword": "${-true}",
1012
"int_calculation": "${105 * 3 / 2}",
1113
"int_subtraction": "${10 - 3}",
1214
"int_negated_reference": "${-var.count}",

test/unit/rules/test_expressions.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,22 @@ def test_force_parens_keeps_expression_form(self):
469469
self.assertEqual(rule.serialize(options=opts), "${-3}")
470470

471471
def test_boolean_operand_is_not_treated_as_a_number(self):
472-
rule = self._make_unary("-", True)
473-
self.assertEqual(rule.serialize(), "${-True}")
472+
"""`bool` subclasses `int`, so without the guard `-true` would serialize to -1.
473+
474+
Asserted against the helper rather than `serialize()`: no HCL input can
475+
route a Python bool here, because `UnaryOpRule` serializes its operand
476+
with `inside_dollar_string` set, and `LiteralValueRule` yields the string
477+
"true" in that context. See `TestNegatedKeywords` for the parsed path.
478+
"""
479+
options = SerializationOptions()
480+
self.assertIsNone(UnaryOpRule._negate_numeric_literal("-", True, options))
481+
self.assertIsNone(UnaryOpRule._negate_numeric_literal("-", False, options))
482+
483+
def test_zero_operand_returns_zero_not_none(self):
484+
"""The caller must test `is not None`; plain truthiness would drop `-0`."""
485+
result = UnaryOpRule._negate_numeric_literal("-", 0, SerializationOptions())
486+
self.assertEqual(result, 0)
487+
self.assertIsNotNone(result)
474488

475489

476490
# --- ExpressionRule._wrap_into_parentheses tests ---

test/unit/test_api.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,51 @@ def test_parenthesised_negation_is_still_an_expression(self):
327327
self.assertEqual(loads("x = -(3)\n"), {"x": "${-(3)}"})
328328

329329
def test_scientific_notation_is_unaffected(self):
330+
"""`-1e10` never reaches the unary path: it lexes as a single FLOAT_LITERAL."""
330331
self.assertEqual(loads("x = -1e10\n"), {"x": "${-1e10}"})
331332

333+
def test_spaced_negation_of_scientific_notation_stays_an_expression(self):
334+
"""The spaced form *is* a unary op, and its operand is a string.
335+
336+
`preserve_scientific_notation` (on by default) keeps `1e10` as source
337+
text, so there is no number to negate and the expression form stands.
338+
"""
339+
self.assertEqual(loads("x = - 1e10\n"), {"x": "${-1e10}"})
340+
341+
def test_spaced_negation_of_integer_is_still_a_number(self):
342+
self.assertEqual(loads("x = - 3\n"), {"x": -3})
343+
344+
def test_negative_zero_normalises_to_zero(self):
345+
"""`-0` is 0. The dict path drops the sign; the direct path keeps the source."""
346+
self.assertEqual(loads("x = -0\n"), {"x": 0})
347+
self.assertEqual(dumps(loads("x = -0\n")), "x = 0\n")
348+
332349
def test_round_trip_through_dumps(self):
333350
self.assertEqual(loads(dumps(loads("x = -3\n"))), {"x": -3})
351+
352+
353+
class TestNegatedKeywords(TestCase):
354+
"""`-true` is not arithmetic, so it stays an expression.
355+
356+
This is the parsed counterpart to the `bool` guard in
357+
`UnaryOpRule._negate_numeric_literal`: a keyword operand is serialized with
358+
`inside_dollar_string` set and so arrives as the string "true", never as a
359+
Python bool. Without that distinction `bool` subclassing `int` would turn
360+
`-true` into -1.
361+
"""
362+
363+
def test_negated_true(self):
364+
self.assertEqual(loads("x = -true\n"), {"x": "${-true}"})
365+
366+
def test_negated_false(self):
367+
self.assertEqual(loads("x = -false\n"), {"x": "${-false}"})
368+
369+
def test_negated_null(self):
370+
self.assertEqual(loads("x = -null\n"), {"x": "${-null}"})
371+
372+
def test_not_operator_on_keyword(self):
373+
self.assertEqual(loads("x = !true\n"), {"x": "${!true}"})
374+
375+
def test_bare_keywords_are_still_python_values(self):
376+
"""Outside an expression the keywords keep their Python mappings."""
377+
self.assertEqual(loads("x = true\ny = false\nz = null\n"), {"x": True, "y": False, "z": None})

0 commit comments

Comments
 (0)