fix: return heredoc bodies as real multi-line values when unquoting (#303) - #324
Merged
Merged
Conversation
…303) The flatten path escapes the body to build a quoted-string source form (`'"a\nb"'`). That escaping ran before the strip_string_quotes early return, so a caller asking for the *value* got escaped *source* back: every line break arrived as a literal backslash-n. Reported from production, where a heredoc-defined PGP private key came out as a single line and was silently unusable. Before this fix no combination of SerializationOptions reproduced v7's plain multi-line string -- confirmed by brute-forcing all sixteen combinations of strip_string_quotes, preserve_heredocs, wrap_objects and explicit_blocks. This is the same defect #313 fixed for quoted strings -- strip_string_quotes should yield values, not source -- which touched StringRule and left both heredoc rules behind. Moving the escaping after the early return in each is the whole change; the quoted form is unaffected. The only two tests that failed were the ones asserting the reported behaviour (`"line1\\nline2"`). Both now assert real newlines, and each gains a sibling pinning that the quoted form still escapes, so the two paths cannot drift again. Docs: the option table described preserve_heredocs only as "keep heredocs in their original form", and the migration guide's V7_COMPAT recipe omitted it entirely -- which is how the reporter ended up without a working combination. Both now cover it, and the guide's example output is verified against the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review turned up an asymmetry the guide did not mention: with strip_string_quotes set, a quoted string's `\n` resolves to a newline while a heredoc body's stays two literal characters. Both are correct -- HCL processes escapes in quoted templates only -- but a reader coming from the V7_COMPAT recipe has no way to predict it. Same for line endings: a heredoc in a CRLF file yields a body containing `\r\n`, since a carriage return inside the body is content rather than structure. Example output verified against the code rather than written from memory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rddimon
approved these changes
Aug 26, 2026
#323 landed the single-character delimiter fix. Only test_api.py conflicted -- both branches append a test class -- resolved by keeping both, main's first. CHANGELOG.md auto-merged this time thanks to the union driver from #319, so the usual two-file conflict was one. Verified the two heredoc changes compose: a single-character delimiter flattened to a value yields real newlines for the plain, trimmed and empty forms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify
added a commit
that referenced
this pull request
Aug 26, 2026
#323 and #324 landed after this branch was opened. Added in the same style as the rest of the section -- PR link, no credit line, since both were fixed in-house. CHANGELOG.md conflicted because main carried the ungroomed form of every entry this branch had already rewritten; kept the groomed text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 26, 2026
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.
caused by #303
Problem
preserve_heredocs=Falsestrips the<<-EOTmarkers as documented, but converts every real newline in the body into a literal\n(backslash +n). Reported from production: a heredoc-defined PGP private key came out as a single line and was silently unusable.I verified the reporter's strongest claim rather than taking it on trust — brute-forcing all sixteen combinations of
strip_string_quotes,preserve_heredocs,wrap_objectsandexplicit_blocks:There was genuinely no way to get a plain multi-line string out of 8.x.
Root cause
hcl2/rules/strings.py. The flatten path escapes the body to build a quoted-string source form, and that escaping ran before thestrip_string_quotesearly return:HeredocTrimTemplateRulehas the same shape via itssep = "\\n".This is the same defect #313 fixed for quoted strings —
strip_string_quotesshould yield values, not source. That change touchedStringRuleand left both heredoc rules behind.Fix
Move the escaping after the early return, in both rules. The quoted form is unchanged.
strip_string_quotes+preserve_heredocs=False'line1\\nline2''line1\nline2'preserve_heredocs=Falsealone'"line1\\nline2"'What the tests said
The only two failures across the whole suite were the tests asserting the reported behaviour:
No golden or integration file moved. Both now assert real newlines, and each gained a sibling pinning that the quoted form still escapes — so the value path and the source path cannot drift apart again.
Added
TestHeredocFlattenedToValuecovering the plain and trimmed forms, the reported multi-line-secret shape, and that embedded quotes and backslashes are escaped in the source form but not the value form.Docs
The issue notes the migration guide never mentions this, which is how the reporter ended up with no working combination. Fixed both:
docs/01_getting_started.md— the option table said only "Keep heredocs in their original form"; it now describes whatFalseactually produces and how to get the value.docs/06_migrating_to_v8.md—preserve_heredocs=Falseadded to theV7_COMPATrecipe, with a worked example and a note that it needsstrip_string_quotesalongside it.Both documented outputs were executed against the code, not written from memory.
Related
strip_string_quotesshould be enabled by default #289 — proposesstrip_string_quotesas the default; this should land first, or that change ships the corruption to everyoneTest plan
pre-commit run: clean (ruff check, ruff format, mypy)real newlines: 3 | literal backslash-n: 0