fix: parse heredocs with an empty body (#309) - #312
Merged
kkozik-amplify merged 2 commits intoAug 24, 2026
Conversation
`a = <<EOF\nEOF` failed to parse. The v8 rewrite turned the optional
newline after the opening marker into a mandatory one, so a marker
immediately followed by its closing delimiter no longer matched the
HEREDOC_TEMPLATE terminal.
The failure is not confined to the empty heredoc. Having failed to match
at that position, the lexer scans on and matches a *later* delimiter, so
an empty heredoc followed by more attributes silently swallows them:
locals {
a = <<EOF
EOF
b = <<EOF
real body
EOF
}
serialized to a single attribute `a` whose value ran to the end of `b`,
with `b` absent from the output. Only a file whose empty heredoc had no
later delimiter to latch onto surfaced as a parse error.
Make the body and its trailing newline a single optional group rather
than restoring the bare `\n?` the rewrite dropped. The group has to be
lazy: a greedy one prefers to match a body, which reintroduces the
run-on above. The delimiter still has to begin its own line, so amplify-education#194's
rule holds and a word merely ending in the delimiter does not terminate
the body.
Cover empty, empty-trimmed and blank-line-only heredocs in the
specialized suite, along with an attribute after them that proves the
lexer stops where it should. `test/helpers/terraform-config/` carried an
empty-heredoc fixture for this before the v8 test reorganisation; nothing
in the current suite replaced it. Without the fix, 6 of the new tests
fail.
livingstaccato
added a commit
to provide-io/pyvider-hcl
that referenced
this pull request
Aug 18, 2026
`<<EOF` immediately followed by its closing marker leaked the raw markers as the value: `_unwrap_heredoc` split the remainder on its last newline, found none, and gave up, so the string fell through to escape processing untouched. The case is unreachable with python-hcl2 8.1.2, whose grammar cannot parse a body-less heredoc at all, so nothing exercised it. Running the suite against a build carrying the fix for that (submitted upstream as amplify-education/python-hcl2#312) surfaced it: with the parser repaired, `x = <<EOF\nEOF` reached the normalizer and came back as the two-line string `<<EOF\nEOF` rather than the empty string HCL specifies. Distinguish having no body lines, which holds "", from having one empty body line, which holds a newline. The new tests drive the normalizer directly rather than through the parser, so they run on the released python-hcl2 as well.
The grammar change fixes more than the existing tests pin down. Two consecutive empty heredocs are the tightest form of the run-on and were silent data loss before: main returned one attribute spanning both, with the second absent and no exception. The run-on also latched onto a non-matching later delimiter. Add container contexts too — tuple element, object value, function argument — which all raised before the fix, showing the repair is not limited to top-level attributes. Tab-indented closing delimiter covers the whitespace class in `\s*`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 24, 2026
kkozik-amplify
approved these changes
Aug 24, 2026
kkozik-amplify
added a commit
to livingstaccato/python-hcl2
that referenced
this pull request
Aug 24, 2026
amplify-education#312 landed the empty-heredoc fix, which touched the same two append-only spots as this branch. Both conflicts keep both sides: CHANGELOG.md - both entries, amplify-education#309's landed one first so this branch's diff is a pure append test_api.py - TestEmptyHeredocs from main, then this branch's TestNegativeIntegerLiterals and TestNegatedKeywords No source overlap: this branch only touches hcl2/rules/expressions.py, amplify-education#312 only touched hcl2/hcl2.lark. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify
added a commit
to livingstaccato/python-hcl2
that referenced
this pull request
Aug 24, 2026
amplify-education#312 and amplify-education#311 have landed since this branch was opened. Both conflicts are append-only and keep both sides: CHANGELOG.md - landed entries first, so this branch's two lines are a pure append test_api.py - TestEmptyHeredocs, TestNegativeIntegerLiterals and TestNegatedKeywords from main, then this branch's TestStripStringQuotes No source overlap: this branch touches hcl2/rules/strings.py and hcl2/utils.py, which neither amplify-education#311 (expressions.py) nor amplify-education#312 (hcl2.lark) went near. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify
added a commit
to agu2347/python-hcl2
that referenced
this pull request
Aug 24, 2026
amplify-education#312, amplify-education#311 and amplify-education#313 have landed. Two conflicts: hcl2/hcl2.lark - real overlap with amplify-education#312, both editing the heredoc terminals. Resolved as the union: amplify-education#312's lazy optional body group plus this branch's \r?, giving /<<MARKER\r?\n(?:(?:.|\n)*?\r?\n)??\s*MARKER\r?\n/. Verified an empty heredoc in a CRLF file parses, two consecutive empty CRLF heredocs stay separate, and a line merely ending in the marker still does not terminate the body. CHANGELOG.md - landed entries first, this branch's appended. hcl2/rules/strings.py and hcl2/utils.py auto-merged against amplify-education#313, which touched both. Checked rather than assumed: strip_string_quotes on CRLF source, escapes resolved through CRLF line endings, a literal CR inside a string surviving, and amplify-education#313's out-of-range and lone-surrogate escape guards all still behave. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify
added a commit
to agu2347/python-hcl2
that referenced
this pull request
Aug 24, 2026
amplify-education#312, amplify-education#311 and amplify-education#313 have landed. Only CHANGELOG.md conflicted, resolved with the landed entries first and this branch's appended. hcl2/rules/strings.py auto-merged against amplify-education#313, which rewrote the same file's string-serialization path. Verified the two coexist: amplify-education#313's process_escape_sequences and lark_name() dispatch sit alongside this branch's _strip_closing_marker_line, and the full suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #309.
A heredoc with an empty body failed to parse. The v8 rewrite turned the optional newline after the opening marker into a mandatory one, so a marker immediately followed by its closing delimiter no longer matched
HEREDOC_TEMPLATE.The louder half of the bug
The parse error is the rare case. Having failed to match at that position, the lexer scans on and matches a later delimiter, silently absorbing everything in between:
8.1.2 returns a single attribute whose value runs to the end of
b, withbabsent from the output. No exception. Only a file whose empty heredoc has no later delimiter to latch onto surfaces asUnexpectedToken.The fix
Make the body and its trailing newline one lazy optional group:
Two simpler-looking patches are both wrong, which seems worth recording:
\n?that the rewrite dropped fixes the empty case but keeps the run-on, because the body can still stretch to a later delimiter.\nbefore\s*fixes both of those, but then\s*can match nothing and a word merely ending in the delimiter terminates the body:<<EOF\nsayEOF\nEOFtruncates atsayEOF.The laziness is load-bearing for the same reason: a greedy
?prefers to match a body. The delimiter still has to begin its own line, so the rule from #194 is preserved.Tests
test/helpers/terraform-config/carried an empty-heredoc fixture, added with #117 when this was fixed the first time. The v8 test reorganisation replaced that layout, and no zero-line heredoc appears anywhere in the current suite — which is why nothing caught this.The specialized heredoc fixture now covers empty, empty-trimmed and blank-line-only heredocs, plus an attribute after them that proves the lexer stops where it should. Without the grammar change, 6 of the new tests fail.
nose2 --config tox.ini: 1400 tests, OK.ruffclean.This pull request, and the investigation behind it, were produced by an AI assistant (Claude) working on behalf of the author. Every reproduction, test run and benchmark cited was executed rather than inferred, but please review with that provenance in mind.