Skip to content

fix: two rules that ignored they were writing expression source (#340, #341) - #350

Draft
livingstaccato wants to merge 1 commit into
amplify-education:mainfrom
livingstaccato:fix/expression-source-rules
Draft

fix: two rules that ignored they were writing expression source (#340, #341)#350
livingstaccato wants to merge 1 commit into
amplify-education:mainfrom
livingstaccato:fix/expression-source-rules

Conversation

@livingstaccato

@livingstaccato livingstaccato commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #340.
Fixes #341.

What

SerializationContext.inside_dollar_string tells a rule that the text it is producing is part of an expression rather than a value handed to the caller. StringRule checks it and keeps its quotes, for the obvious reason: upper("x") becoming upper(x) asks for a variable nobody declared. Two rules did not check it.

#340 — a heredoc argument was spliced in bare.

loads("a = upper(<<E\nx\nE\n)\n", serialization_options=value)["a"]
# '${upper(x)}'   ← a reference, not a string

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.

loads('a = "%{ if x == \\"y\\" }t%{ endif }"\n', serialization_options=value)["a"]
# '%{ if x == y }t%{ endif }'   ← compares against a variable

TemplateStringRule only ever appears inside %{ ... }, where the text is expression source and the quotes belong to a literal written in it.

One thing that changes beyond the issues

With preserve_heredocs on, a heredoc inside an expression is now left as itself rather than wrapped in quotes. Quoting it put raw newlines inside a quoted string, 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, with the reason.

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

It touches the same code as #335 (hcl2/rules/strings.py), #346 (hcl2/rules/strings.py), #352 (hcl2/rules/strings.py), #354 (hcl2/rules/strings.py). Whichever of those lands first, this one needs a rebase rather than a merge — the overlaps are real edits to the same methods, not adjacent lines, so resolving them by hand risks losing one of the two fixes. Say the word and I will rebase and re-run the suite.


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.

…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.
@livingstaccato

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant