Skip to content

fix: make strip_string_quotes yield values, not broken source (#308, #310) - #313

Merged
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
livingstaccato:fix/strip-string-quotes
Aug 24, 2026
Merged

fix: make strip_string_quotes yield values, not broken source (#308, #310)#313
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
livingstaccato:fix/strip-string-quotes

Conversation

@livingstaccato

Copy link
Copy Markdown
Contributor

Fixes #310. Fixes #308.

strip_string_quotes is documented as returning a plain string instead of '"hello"', and docs/06_migrating_to_v8.md presents it as the v7 compatibility path. It did neither job completely, and both defects live in the same method.

It unquoted strings inside expressions (#310)

The option stripped every string literal, including those nested in an expression, where the surrounding text is HCL source rather than a value:

source before after
upper("x") ${upper(x)} ${upper("x")}
var.x ? "yes" : "no" ${var.x ? yes : no} ${var.x ? "yes" : "no"}
[for s in l : s if s != ""] ${[for s in l : s if s != ]} ${[for s in l : s if s != ""]}

The first two silently change meaning, referring to identifiers that do not exist; the third is not parseable at all. The fix restricts stripping to strings that are values, using the inside_dollar_string flag the serializer already threads through expression rules.

It left escapes unresolved (#308)

Asking for the value of "line1\nline2" returned a literal backslash and an n. v7 stripped quotes and resolved escapes in the same call; v8 did only the first half, so the option delivered neither the source form nor the value. Since it is already documented as one-way and not round-trippable, resolving escapes there costs nothing in reconstruction fidelity.

The pass is single, so an escaped backslash cannot combine with the character after it: \\n is a backslash followed by "n". v7 replaced sequentially and produced a backslash followed by a newline; the single pass matches what OpenTofu evaluates the same source to, so this is deliberately not bug-compatible with v7.

Only literal STRING_CHARS parts are processed. Interpolations and escaped interpolation markers carry expression text, whose escapes are not the string's to resolve.

Scope

Default output is untouched — it stays source-shaped so dumps() can reconstruct it.

Existing coverage exercised the option only on simple values, which is why neither defect showed up. Without the source change, 10 of the new tests fail. nose2 --config tox.ini: 1419 tests, OK. ruff clean.


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.

…y-education#308, amplify-education#310)

`strip_string_quotes` is documented as the option that returns a plain
string instead of `'"hello"'`, and the v8 migration guide presents it as
the v7 compatibility path. It did neither job completely.

It unquoted every string literal, including those nested inside an
expression, where the surrounding text is HCL source rather than a value:

    upper("x")                  ->  ${upper(x)}
    var.x ? "yes" : "no"        ->  ${var.x ? yes : no}
    [for s in l : s if s != ""] ->  ${[for s in l : s if s != ]}

The first two silently change meaning, referring to identifiers that do
not exist, and the third is not parseable at all. Restrict the stripping
to strings that are values, using the `inside_dollar_string` context flag
the serializer already threads through expression rules.

It also left escape sequences unresolved, so a caller asking for the
value of `"line1\nline2"` got a literal backslash and an `n`. Resolve
them when stripping, as v7 did. The pass is single, so an escaped
backslash cannot combine with the character after it -- v7 replaced
sequentially and turned `\\n` into a backslash followed by a newline,
where HCL specifies a backslash followed by "n". Only literal
STRING_CHARS parts are processed; interpolations and escaped
interpolation markers carry expression text, whose escapes are not this
string's to resolve.

Default output is untouched: it stays source-shaped so that dumps() can
reconstruct it, and this option is already documented as one-way.

Existing coverage exercised the option only on simple values, which is
why neither defect showed up. Without the fix, 10 of the new tests fail.
@livingstaccato
livingstaccato requested a review from a team as a code owner August 18, 2026 18:29
Three loose ends in the strip_string_quotes fix.

_decode_unicode_escape called chr() on any well-formed hex run, so an
out-of-range codepoint escaped as an exception rather than as text:
\U00110000 raised ValueError and \UFFFFFFFF raised OverflowError, both
straight out of loads(). A lone surrogate was worse than a raise — it
decoded fine and then failed later on .encode("utf-8"). All three now
return None, which the caller already handles by preserving the escape
verbatim, matching the documented policy that a serializer should not
raise an error the parser did not.

isinstance(part.content, STRING_CHARS) failed mypy, since STRING_CHARS
is a subscripted factory it reads as a parameterized generic. Compare
lark_name() instead, which is how the rest of the codebase identifies
nodes.

Document what the option now does: docs/01 described only quote removal,
and docs/06 warned only that it is one-way. Neither mentioned escape
resolution or that expression interiors keep their quotes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
kkozik-amplify merged commit 0015121 into amplify-education:main Aug 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants