fix: two rules that ignored they were writing expression source (#340, #341) - #350
livingstaccato wants to merge 5 commits into
Conversation
…ify-education#340, amplify-education#341) `inside_dollar_string` says the text being produced is part of an expression rather than a value for the caller. `StringRule` checks it and keeps its quotes, because `upper("x")` becoming `upper(x)` asks for a variable nobody declared. Two rules did not. A heredoc argument was spliced in bare: `upper(<<E\nx\nE\n)` came back as `${upper(x)}`, and a multi-line body put raw newlines into source that no longer parsed. Both heredoc rules check the flag now, in the flattened and the trimmed paths. With `preserve_heredocs` on, the heredoc is left as itself rather than wrapped in quotes. That was quoting raw newlines, which OpenTofu rejects with "Invalid multi-line string"; as a heredoc it is a legal argument and `trimspace(<<EOF\n hi \nEOF\n)` evaluates to "hi". One existing test pinned the quoted form and now states this. `TemplateStringRule` dropped its delimiters in the value form, turning `%{ if x == "y" }` into `%{ if x == y }` -- a comparison against a variable rather than a string. It only ever appears inside a directive, which is always expression source.
|
Please hold off on merging this one for now — I want to do another review pass over it before it goes in. Opened as a draft for that reason; I will mark it ready and say so here once I am done. |
|
Review pass done, so the hold above no longer applies — this is ready for review now. Rebased on current 🤖 Drafted with Claude Code. |
…-education#340, amplify-education#341) The branch also stopped quoting a heredoc that sits inside an expression on the default path, which neither amplify-education#340 nor amplify-education#341 asks for -- both are scoped to `strip_string_quotes`. It regressed the round trip: `dumps(loads(...))` of a heredoc argument raised `UnexpectedToken`, because the emitted `trimspace(<<EOF\nhi\nEOF)` puts the closing marker on the same line as the `)`, which this grammar does not accept. On main that round trip works. The change is right in the end -- Terraform v1.11.4 rejects the quoted form with "Invalid multi-line string" and evaluates the heredoc argument to "hi" -- but it needs the writer to give a heredoc its own line first, which is amplify-education#338. Landing it here breaks `dumps` for anyone whose document has a heredoc in a call. Reverted, with `test_api.py` back to the expectation it had and a note saying which fix supersedes it, so amplify-education#338 makes the change deliberately. The two fixes the issues do ask for are untouched, and are now covered per rule: reverting `HeredocTemplateRule`'s check fails twelve assertions, `HeredocTrimTemplateRule`'s one, `TemplateStringRule`'s two. Also widens amplify-education#340's coverage past the reported function argument to every rule that marks its children as expression source -- nested call, later argument, binary operand, conditional branch, indexed tuple, interpolation -- and adds round-trip assertions for both the default and value dicts, which is the half that a change here can break without any of the value assertions noticing. Verified against Terraform v1.11.4: `upper(<<EOT\nx\nEOT\n)` is "X\n", and `"%{ if local.x == "y" }t%{ endif }"` is "t". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # hcl2/rules/strings.py
Combines the expression-source guard with the heredoc body values amplify-education#335 returns: a body carries the newline ending its last content line, so the expectations for a heredoc used as expression source carry it too.
|
Brought up to date with current Merge order. Eight of the twelve open PRs apply to (1552 → 1563 → 1591 → 1627 → 1671 → 1696 → 1703 → 1711 passing, from a 1534 #335 changes a value two of the others assert on. It makes a heredoc body #350 and #351 were written before that. Once #335 is in, their expectations need A resolved integration of all twelve is on the fork if it is useful: Drafted with the help of an LLM, working on behalf of the author. |
`(...)` is written as `${(...)}`, but the term serialized its inside as a
value, so literals came back in Python's spelling: `(true)` as `${(True)}`
and `(null)` as `${(None)}` on the default options, which dumps wrote back
as references OpenTofu rejects, and a tuple or object inside as a repr that
did not parse. With strip_string_quotes, `("s")` lost its quotes. Every
changed output was one no reader could evaluate; each round trip now
evaluates, in OpenTofu v1.12.6, to the value the source does.
The two options come from different PRs (amplify-education#333 writes the span, amplify-education#345 adds the sidecar), so the combination only exists here. with_meta put __start_line__/__end_line__ into the body, and a sidecar body reserves no key, so dumps wrote them out as attributes. The span now goes to HclMeta.start_line/end_line. Also flips amplify-education#350's pin of the quoted heredoc argument, which amplify-education#338 (amplify-education#352) changes on purpose.
Fixes #340.
Fixes #341.
What
SerializationContext.inside_dollar_stringtells a rule that the text it is producing is part of an expression rather than a value handed to the caller.StringRulechecks it and keeps its quotes, for the obvious reason:upper("x")becomingupper(x)asks for a variable nobody declared. Two rules did not check it.#340 — a heredoc argument was spliced in bare.
The quoted equivalent one line away was already correct (
${upper("x")}). With a multi-line body it was worse: raw newlines went into expression source that then did not parse.#341 — a string literal inside a directive lost its delimiters.
TemplateStringRuleappears inside%{ ... }, where the text is expression source and the quotes belong to a literal written in it.A parenthesised term had the same gap.
(true)loaded as${(True)}and(null)as${(None)}-- Python's spelling, which OpenTofu reads as references -- and([1, "a"])as a Python reprdumpscould not parse. What the parentheses wrap is now written as expression source.Ground truth
OpenTofu v1.12.5:
upper(<<EOT\nx\nEOT\n)evaluates to"X\n", so the argument is a string; a directive literal is written with plain quotes,"%{ if local.x == "y" }t%{ endif }", and evaluates to"t". That plain spelling round-trips here unchanged, and is asserted so it stays that way.One divergence this does not touch: the grammar accepts
\"y\"inside a directive, which OpenTofu rejects with "Invalid character". That is a separate defect, filed as #353. This PR only stops the value form from mangling that spelling into a reference; it neither blesses nor removes it.Merging
Up to date with
mainas of 2f6d718, by merging rather than rebasing (themerge=unionCHANGELOG duplicates entries under a rebase).It merges cleanly on its own, and all twelve open PRs are merged together, with the full suite passing on 3.8-3.14, at
livingstaccato/python-hcl2@int/pyvider-hcl-9. Landing alongside the others needs one small follow-up from whichever lands second:test_default_options_keep_the_heredoc_as_quoted_sourceflips to the heredoc form, which fix: a heredoc ends its own line wherever it is written (#338) #352 makes possible.serializemethods into one, so their edits land in that shared method.This pull request, and the investigation behind it, were produced by an AI assistant (Claude) working on behalf of the author. Please review with that provenance in mind.