Skip to content

Fix CRLF (\r\n) line endings failing to parse - #317

Merged
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
agu2347:fix-crlf-line-endings-issue-315
Aug 24, 2026
Merged

Fix CRLF (\r\n) line endings failing to parse#317
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
agu2347:fix-crlf-line-endings-issue-315

Conversation

@agu2347

@agu2347 agu2347 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Any HCL2 source with CRLF (\r\n) line endings fails to parse, regardless of the surrounding construct:

>>> hcl2.loads("a = 1\r\n")
lark.exceptions.UnexpectedToken: Unexpected token Token('STRING_CHARS', '\r\n...

A single CRLF line anywhere in an otherwise-LF file is enough to fail the whole file. Windows-authored .tf files, or files checked out with core.autocrlf=true, are unparseable.

Fixes #315.

Root cause

hcl2/hcl2.lark's %ignore rule only skips spaces and tabs:

%ignore /[ \t]+/

NL_OR_COMMENT only matches starting from \n. So when a \r immediately precedes a significant \n, no terminal can consume it, and it falls through to STRING_CHARS, breaking the parse.

Fix

Add \r to the %ignore whitespace pattern: %ignore /[ \t\r]+/.

This only affects \r that is insignificant whitespace between tokens. Terminals that need to preserve \r verbatim as part of their own content (STRING_CHARS, HEREDOC_TEMPLATE*, comment alternatives) still win via lark's longest-match rule, since they match longer contiguous spans that include the \r. Verified this holds for a quoted string containing an embedded \r (round-trip fidelity is preserved).

Testing

Added test/unit/test_crlf.py covering every construct from the issue: a bare attribute, a block, a quoted string, a CRLF line embedded among LF lines, and confirmation that an embedded \r inside a string literal survives untouched. Confirmed each new test fails with the exact reported UnexpectedToken error against the pre-fix grammar, and passes after the fix.

Ran the full existing suite (1391 tests) against the fix: all pass, zero regressions.

hcl2.lark's %ignore rule only skipped spaces and tabs, so a bare '\r'
preceding the newline in a CRLF-terminated line had no terminal that
could consume it: NL_OR_COMMENT only matches starting from '\n', and
the ignore rule didn't cover '\r'. The stray '\r' fell through to
STRING_CHARS and the parse failed with UnexpectedToken for every
construct (bare attributes, blocks, quoted strings) as soon as a
single CRLF line appeared anywhere in the source.

Add '\r' to the %ignore whitespace pattern. Terminals that need to
preserve '\r' verbatim as part of their own content (STRING_CHARS,
HEREDOC_TEMPLATE*, comments) still win via lark's longest-match rule,
so this doesn't affect round-trip fidelity of quoted strings or
heredocs -- verified with a dedicated test plus the full existing
suite (1391 tests, unchanged, all still passing).

Fixes amplify-education#315
@agu2347
agu2347 requested a review from a team as a code owner August 24, 2026 15:01
%ignore handles \r only between tokens, so heredocs -- whose terminals
match \n inside their own pattern -- still failed under CRLF. HCL's
reference scanner accepts both \n and \r\n around heredoc markers, so
these are valid inputs. Three places needed it:

  - hcl2.lark: \r? around the markers in both heredoc terminals
  - utils.py: the same in HEREDOC_PATTERN / HEREDOC_TRIM_PATTERN, which
    run on an already-parsed token and otherwise raise RuntimeError when
    flattening a heredoc the grammar just accepted
  - strings.py: \r in _trim_chars, so the preserved form does not keep a
    stray carriage return after the closing marker

Body text keeps its carriage returns, matching HCL: \r around markers is
structure, \r inside the body is content.

Corrected the %ignore comment. It claimed the heredoc terminals "win via
Lark's longest-match rule" -- they never matched at all, so the comment
would have sent the next reader looking in the wrong place.

test_embedded_cr_inside_string_is_preserved asserted on \r escape *text*
(two ASCII characters, never at risk) and was the only test that passed
without the grammar change. Replaced with a real CR byte, and added the
cases the fix newly enables: heredocs, comments, tuples/objects, and the
CRLF-to-LF reconstruction that this approach implies.

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
Heredocs parse in CRLF files once amplify-education#317 lands, so the newline before the
closing marker may be \r\n. Stripping only \n leaves the \r stranded on
the end of the value.

Inert without amplify-education#317 -- no CRLF heredoc parses today -- but it belongs with
this helper rather than with the grammar change that exposes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
kkozik-amplify merged commit 8973c4c into amplify-education:main Aug 24, 2026
8 checks passed
kkozik-amplify added a commit to agu2347/python-hcl2 that referenced this pull request Aug 24, 2026
amplify-education#317 has landed since the last update. Only CHANGELOG.md conflicted,
resolved with the landed entries first and this branch's appended.

hcl2/rules/strings.py auto-merged: amplify-education#317's `_trim_chars = "\r\n\t "` for
the preserve path now sits alongside this branch's CRLF-aware
`_strip_closing_marker_line` for the flatten paths. Verified rather than
assumed -- CRLF heredocs flatten and preserve correctly, empty CRLF
heredocs parse, and this branch's own cases (trailing blank line,
trailing spaces, indented closing marker, blank line not cancelling the
`<<-` dedent) all still hold. Full suite: 1504 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify added a commit that referenced this pull request Aug 24, 2026
…rip (#318)

* Fix heredoc bodies losing trailing blank lines/spaces to a greedy rstrip

HeredocTemplateRule.serialize's preserve_heredocs=False path did
match.group(2).rstrip("\n\t "), which removes every trailing newline,
tab, and space character -- not just the single newline separating the
last body line from the closing marker. A trailing blank line, multiple
trailing blank lines, and trailing spaces on the last line all collapsed
to the same flattened value as a plain one-line body.

Replace the blanket rstrip with a helper that removes exactly the one
trailing newline that separates content from the marker line, per the
issue's proposed scope. This is limited to the plain <<MARKER heredoc;
the indented <<-MARKER variant has separate, pre-existing
leading-whitespace-trim semantics with its own golden tests, which are
unaffected by this change (verified against the full suite).

Fixes #316

* fix: apply the trailing-whitespace fix to <<- too, and keep the marker line out

The blanket rstrip was doing two jobs: dropping the newline that separates
the last body line from the closing marker, and dropping that marker
line's own indentation. Removing only the newline leaves the indentation
behind, so an indented closing marker leaked into the body:

    <<MARKER\nx\n   MARKER   ->  "x\n   "   (was "x")

No existing test flattens a heredoc with an indented closing marker, so
the suite stayed green. The spec is explicit that the marker "may also
have an arbitrary number of spaces preceding it on its line", so that
whitespace is not content.

Splitting the two concerns in _strip_closing_marker_line also removes the
reason to skip <<-MARKER, which still collapsed trailing blank lines and
trailing spaces exactly as #316 describes. Its rstrip was the same
double-duty call, which is why swapping in a newline-only strip broke the
dedent; handling the marker line separately does not.

Fixing <<- exposed a second bug that the old rstrip had been masking: a
blank line has zero leading spaces, so once blank lines survive they drag
min_spaces to zero and cancel the dedent for every other line. The spec
measures "any literal string at the start of each line" -- a blank line
has none -- so blank lines are now skipped when measuring.

Tests cover the indented closing marker (spaces and tab, with and without
a trailing blank line) and the <<- variant throughout. The module
docstring is now raw; its `rstrip("\n\t ")` reference was being read as
real control characters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: make _strip_closing_marker_line CRLF-aware

Heredocs parse in CRLF files once #317 lands, so the newline before the
closing marker may be \r\n. Stripping only \n leaves the \r stranded on
the end of the value.

Inert without #317 -- no CRLF heredoc parses today -- but it belongs with
this helper rather than with the grammar change that exposes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kamil Kozik <kkozik@amplify.com>
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

Development

Successfully merging this pull request may close these issues.

CRLF line endings fail to parse (no terminal matches \r)

2 participants