Skip to content

Fix properties line continuation handling - #8431

Merged
timtebeek merged 2 commits into
mainfrom
tim/properties-line-continuations
Aug 8, 2026
Merged

Fix properties line continuation handling#8431
timtebeek merged 2 commits into
mainfrom
tim/properties-line-continuations

Conversation

@timtebeek

@timtebeek timtebeek commented Aug 8, 2026

Copy link
Copy Markdown
Member

What's changed?

PropertiesParser decided whether a line continued from a single character of lookbehind (prev == '\\'). This replaces that with a check of the buffered line itself — an odd number of trailing backslashes, ignoring the CR of a CRLF pair, and never on a comment line — and fixes the neighbouring defects of the same kind that surfaced while testing it.

Note on the reported symptom

The round-trip in the issue's repro does not reproduce on main — the LST already keeps the raw, continued text in Properties.Value.text (getSource() returns it with continuations, getText() returns it flattened), and PropertiesPrinter prints getSource(). I verified this end-to-end:

# Gradle JVM args with backslash continuations
org.gradle.jvmargs=-Xms1g -Xmx4g \
  --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
  --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED

parses and prints back byte-for-byte, and ChangePropertyValue/ChangePropertyKey/AddProperty/DeleteProperty all leave the continued entry's formatting intact when they touch a different property. (Changing the continued value itself does replace the whole value, which is expected — the new value has no continuation layout to keep.) That case is now covered by lineContinuationWithWhitespaceBeforeBackslash as a characterization test.

What digging around it did turn up is a cluster of continuation bugs that are invisible to a round-trip — the raw text is always printed back verbatim — but corrupt the LST, so recipes either silently skip a property or delete it. All of these drop an Entry from Properties.File.getContent():

1. CRLF line endings broke continuation detection. With \r\n the character before the newline is \r, not \\, so org.gradle.jvmargs=-Xms1g \<CRLF> -Xmx4g<CRLF> parsed the value as the truncated -Xms1g \ and absorbed the continuation line as whitespace prefix. getText() returned -Xms1g \ instead of -Xms1g -Xmx4g, so any recipe reading or rewriting that value operated on a wrong value — the kind of thing that forces the text-splicing workaround described in the issue.

2. An escaped backslash at end of line was treated as a continuation. \\ is a literal backslash, so path=C:\\dir\\ followed by other=value is two entries; the parser merged other=value into path's value and dropped the other entry.

3. Comments were continued. java.util.Properties never continues a comment, so # install dir: C:\App\ followed by key=value swallowed the entry into the comment message. Worse than a missed match: a recipe that edits that comment deletes the real property.

4. A blank line after a continuation did not terminate the logical line. key=a \ / blank / next=value absorbed next=value into key's value. Since ChangePropertyValue does value.withText(newValue) over that whole multi-line text, running it on key emits key=b and silently deletes the next=value line from the user's file.

5. entryFromLine had the same single-character escape lookback for =/: in keys. a\\=b parsed as one escaped key with Delimiter.NONE and an empty value, instead of key a\ and value b.

6. isDelimitedByWhitespace passed an index where a char was wantedCharacter.isWhitespace(line.length() - 1) instead of charAt(...). A whitespace-delimited line whose length happened to land in 10-14 or 29-33 (those minus one being the whitespace code points) was dropped from the LST entirely.

3-6 are pre-existing rather than new, but they're all the same defect — deciding a lexical property of a line from one character — and 3 and 4 become much easier to hit once CRLF files reach the continuation path, so fixing them together seemed better than leaving landmines behind the fix.

Anything in particular you'd like reviewers to focus on?

  • Whether Relates to vs Fixes is right for Properties parser/printer flattens backslash line continuations #8417: I couldn't reproduce the exact reported flattening on main, so I'd rather leave the issue open for the reporter to confirm against a real file (CRLF endings would be my first guess at the difference) than auto-close it.
  • One confirmed defect deliberately not fixed here: a file ending in a dangling a=b\ leaves getText() as b\ where java.util.Properties reports b. Fixing it properly means teaching Properties.Continuation to do real escape processing (getText() today doesn't unescape \n, \t or \: either), which changes the semantics of a public API for every recipe that matches on values — too big to smuggle into this PR.

Checklist

  • Unit tests for the reported case and for each of the six fixed cases
  • ./gradlew :rewrite-properties:test — 135 tests, 0 failures; every new test verified to fail without its fix
  • Downstream consumers exercised: :rewrite-gradle:test --tests "*Propert*" --tests "*Wrapper*", :rewrite-maven:test --tests "*Propert*", :rewrite-android:test — all green

…ashes

The parser decided a line continued by looking only at whether the
previous character was a backslash. That misread two cases:

- With CRLF line endings the character before the newline is the
  carriage return, so `key=a \<CRLF>  b` was not treated as a
  continuation: the value was truncated to `a \` and the continuation
  line was absorbed as whitespace prefix of the next content.
- An escaped backslash at the end of a line (`path=C:\\dir\\`) was
  treated as a continuation, swallowing the following entry into the
  value.

Track the number of consecutive trailing backslashes instead, ignoring
an intervening carriage return, so a line continues only on an odd
count.

Relates to #8417
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 8, 2026
@timtebeek
timtebeek marked this pull request as ready for review August 8, 2026 20:32
…hind

Replaces the trailing-backslash counter with a check of the buffered line
itself, which drops the unbounded carriage-return exemption (a run of stray
CRs kept the count alive and paired a backslash with a newline it did not
precede) and lets the same rule cover comments.

Also fixes four adjacent defects of the same kind:

- Comments were continued. java.util.Properties never continues a comment,
  so `# install dir: C:\App\` swallowed the entry on the following line,
  which then vanished from the LST entirely.
- A blank line after a continuation did not terminate the logical line: the
  next property was absorbed into the previous value, and rewriting that
  value through withText() erased the absorbed line from the file.
- entryFromLine used the same single-character escape lookback for `=` and
  `:` in keys, so `a\\=b` parsed as one escaped key with no delimiter and an
  empty value instead of key `a\` and value `b`.
- isDelimitedByWhitespace passed the index `line.length() - 1` to
  Character.isWhitespace rather than the character at it, so a
  whitespace-delimited line whose length happened to fall in 10-14 or 29-33
  was dropped from the LST.
@timtebeek timtebeek changed the title Fix properties line continuation handling for CRLF and escaped backslashes Fix properties line continuation handling Aug 8, 2026
@timtebeek
timtebeek merged commit cd650d8 into main Aug 8, 2026
1 check passed
@timtebeek
timtebeek deleted the tim/properties-line-continuations branch August 8, 2026 21:47
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant