Skip to content

A long string containing \\t fails to format: spurious "unclosed string literal" #32

Description

@abashev

What

Reported upstream as palantir/palantir-java-format#1680 (open since 2026-05-29, no comments). It reproduces here on 2.98.0.2. Minimal case:

class C {
    void m(java.sql.Connection con) throws Exception {
        con.createStatement()
                .execute("ALTER DATABASE tempdb MODIFY FILE (NAME = 'tempdev', FILENAME = 'D:\\tempDb\\DATA\\tempdb.mdf')");
    }
}

open-java-format C.java exits 2 with C.java:5:26: error: unclosed string literal, while javac compiles the same file. Replace the escaped backslashes with forward slashes and the file formats.

In the upstream report the errors point at methods further down the file, which makes the cause hard to see: the line and column come from the rewritten source the formatter parses to check itself, not from the file on disk.

Why it happens

StringWrapper.stringComponents splits the literal's source text at whitespace and at escaped whitespace, and hasEscapedWhitespaceAt/hasEscapedNewlineAt look for \t, \n and \r without asking whether that backslash is itself escaped. In DATA\\tempDb the second backslash and the t look like an escaped tab, so the split lands inside the escaped backslash. StringWrapper.getReflowReplacements on the file above returns:

"ALTER DATABASE tempdb MODIFY FILE (NAME = 'tempdev', FILENAME = 'D:\\tempDb\\DATA\"
        + "\tempdb.mdf')"

The first piece ends in a lone backslash, which escapes the closing quote — that is the error. The second piece starts with \t, a tab, so even if it parsed the string would no longer be the one the author wrote.

\n and \r are mis-read the same way, but that branch advances past the escape, so the split is merely in an odd place and the result still parses and still means the same: the same file with D:\\nempDb formats fine. Only \\t produces code that does not parse.

Fix

Walk the literal left to right consuming escape sequences, so that \\ takes two characters and the t after it is an ordinary letter.

Mind the output promise: files whose long strings contain \\n or \\r format today, and the fix moves where they are split, so their output changes. Measure that over a corpus and decide which release it belongs to, as in #22 and #31.

Done when

  • the file above formats, and formatting it twice is a no-op;
  • a golden covers a wrapped literal with \\t, \\n, \\r, \\\\ and \u escapes, and fails without the fix;
  • a test asserts the string's value is unchanged by wrapping, not only that the result parses;
  • a corpus run says how many files the \\n/\\r split moves.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions