From 2280fb36153d30189c5de8c86af3d8b7ee835bbf Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 1 Sep 2026 22:44:56 -0700 Subject: [PATCH 1/2] fix: a closing marker may carry trailing whitespace (#343) The spec puts the delimiter "alone on its own line", and Terraform's scanner ends the heredoc at a line holding the word and nothing else that matters -- trailing spaces and tabs included. Both terminals required the newline to follow the word immediately, so `EOF ` was not a marker: the heredoc ran on, swallowed the rest of the file, and the parse failed with an error pointing somewhere else entirely. Trailing whitespace is invisible, survives copy-paste, and is left by editors that do not trim it, so a file someone has been running through Terraform for months could fail here for a reason nothing in the message suggests. Only whitespace: `EOFX` and `EOF x` are still body text, which OpenTofu agrees with -- it reads `<[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc)\r?\n/ -HEREDOC_TEMPLATE_TRIM : /<<-(?P[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc_trim)\r?\n/ +HEREDOC_TEMPLATE : /<<(?P[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc)[ \t]*\r?\n/ +HEREDOC_TEMPLATE_TRIM : /<<-(?P[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc_trim)[ \t]*\r?\n/ // Ignore whitespace (but not newlines, as they're significant in HCL). // \r is ignored too so CRLF line endings (\r\n) parse the same as LF: the diff --git a/test/unit/test_heredoc_marker_whitespace.py b/test/unit/test_heredoc_marker_whitespace.py new file mode 100644 index 00000000..94029734 --- /dev/null +++ b/test/unit/test_heredoc_marker_whitespace.py @@ -0,0 +1,67 @@ +# pylint: disable=C0103,C0114,C0115,C0116 +r"""A closing marker may carry trailing whitespace (GH #343). + +The spec puts the delimiter "alone on its own line", and Terraform's scanner +ends the heredoc at a line holding the word and nothing else that matters -- +trailing spaces and tabs included. `HEREDOC_TEMPLATE` required the newline to +follow the word immediately, so `EOF ` was not a marker: the heredoc ran on, +swallowed the rest of the file, and the parse failed with an error pointing +somewhere else entirely. + +Trailing whitespace is invisible, survives copy-paste, and is left behind by +editors that do not trim it, so a file someone has been running through +Terraform for months could fail here. + +Checked against OpenTofu v1.12.5: `< Date: Mon, 7 Sep 2026 14:12:33 +0200 Subject: [PATCH 2/2] test: pin the marker-whitespace behaviour against Terraform (#343) The suite asserted that the file parsed -- every case checked `b == 1` -- so a regression leaking the marker's trailing whitespace into the heredoc body would have passed all of it. Assert the value instead, as an equality against the same heredoc without the trailing whitespace, so the tests stay honest across the separate fixes to the body-value rules (#326). Adds the case that changes meaning: a body line consisting of the delimiter plus trailing whitespace now closes the heredoc rather than being content. Terraform v1.11.4 reads `a = < --- CHANGELOG.md | 2 +- hcl2/hcl2.lark | 5 + test/unit/test_heredoc_marker_whitespace.py | 128 ++++++++++++++++++-- 3 files changed, 123 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798710f7..d2794db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed -- A heredoc whose closing marker carries trailing whitespace parses. The spec puts the delimiter "alone on its own line" and Terraform's scanner ends the heredoc at a line holding the word and nothing else that matters, trailing spaces and tabs included; `HEREDOC_TEMPLATE` required the newline to follow the word immediately, so `EOF ` was not a marker at all -- the heredoc ran on, swallowed the rest of the file, and the parse failed with an error pointing somewhere else. Trailing whitespace is invisible and survives copy-paste, so a file that has been running through Terraform for months could fail here. OpenTofu evaluates `<[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc)[ \t]*\r?\n/ HEREDOC_TEMPLATE_TRIM : /<<-(?P[a-zA-Z][a-zA-Z0-9._-]*)\r?\n(?:(?:.|\n)*?\r?\n)??\s*(?P=heredoc_trim)[ \t]*\r?\n/ diff --git a/test/unit/test_heredoc_marker_whitespace.py b/test/unit/test_heredoc_marker_whitespace.py index 94029734..52a4b85c 100644 --- a/test/unit/test_heredoc_marker_whitespace.py +++ b/test/unit/test_heredoc_marker_whitespace.py @@ -12,16 +12,34 @@ editors that do not trim it, so a file someone has been running through Terraform for months could fail here. -Checked against OpenTofu v1.12.5: `< "body\n", and the next attribute reads + < "", and b reads as 1 + < "EOFX\nEOF x\nbody\n" + +The value assertions compare a marker carrying trailing whitespace against the +same heredoc without it, rather than pinning an absolute string. The two must +agree whatever the body-value rules are, so stating it as an equality keeps +these tests honest across the separate fixes to those rules (GH #326). """ from unittest import TestCase -from hcl2.api import loads +from hcl2.api import loads, parses_to_tree, reconstruct, transform +from hcl2.utils import SerializationOptions + +VALUE = SerializationOptions(preserve_heredocs=False, strip_string_quotes=True) + + +def value_of(src: str) -> str: + """Return the body that the heredoc assigned to `a` evaluates to.""" + return loads(src, serialization_options=VALUE)["a"] class TestATrailingSpaceClosesTheHeredoc(TestCase): + """The attribute after the heredoc is reached, so the marker closed it.""" + def test_spaces(self): self.assertEqual(loads("a = <