diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java index 787c98361..8b7393846 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -230,23 +230,41 @@ private static ImmutableList stringComponents( // adjust for leading and trailing double quotes String text = input.substring(getStartPosition(tree) + 1, getEndPosition(tree, unit) - 1); int start = 0; - for (int idx = 0; idx < text.length(); idx++) { + int next; + for (int idx = 0; idx < text.length(); idx = next) { + int splitAt; + int length; if (CharMatcher.whitespace().matches(text.charAt(idx))) { - // continue below - } else if (hasEscapedWhitespaceAt(text, idx) != -1) { - // continue below + // split before whitespace + splitAt = idx; + next = idx + 1; + } else if ((length = hasEscapedWhitespaceAt(text, idx)) != -1) { + // split before escaped whitespace + splitAt = idx; + next = idx + length; } else if (hasEscapedNewlineAt(text, idx) != -1) { - int length; - while ((length = hasEscapedNewlineAt(text, idx)) != -1) { - idx += length; + // Split after a run of escaped newlines, rather than before it, so the newlines end the + // preceding piece. `reflow` ends a line after a piece ending in an escaped newline, so + // where possible each line of the wrapped literal corresponds to a line of its value. + next = idx; + while ((length = hasEscapedNewlineAt(text, next)) != -1) { + next += length; } + splitAt = next; } else { + // Skip over the entire escape sequence, so the second character of an escape like `\\` is + // not mistaken for the start of another escape. Octal and Unicode escapes don't need + // special handling: after the backslash they only contain letters and digits, which are + // never split points. + next = idx + (text.charAt(idx) == '\\' ? 2 : 1); continue; } - piece.append(text, start, idx); - result.add(piece.toString()); - piece = new StringBuilder(); - start = idx; + if (splitAt > start || piece.length() > 0) { + piece.append(text, start, splitAt); + result.add(piece.toString()); + piece = new StringBuilder(); + start = splitAt; + } } if (piece.length() > 0) { result.add(piece.toString()); diff --git a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java index ab3599fed..ccc4f0552 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java @@ -118,34 +118,6 @@ public void formatDiagnostics_positionOutOfRange() { .isEqualTo("T.java:1:0: error: oops" + System.lineSeparator()); } - @Test - public void stringWrappingProducesInvalidCode() throws Exception { - // Reflowing this string splits the escaped backslash in `\\t`, which produces an unterminated - // string literal several lines past the end of the original input. - String input = - """ - class T { - String s = "%s%s\\\\tend"; - } - """ - .formatted("word ".repeat(100), "x".repeat(95)); - - StringWriter stdout = new StringWriter(); - StringWriter stderr = new StringWriter(); - Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in); - - Path tmpdir = testFolder.newFolder().toPath(); - Path path = tmpdir.resolve("T.java"); - Files.writeString(path, input); - - int result = main.format(path.toString()); - assertThat(stdout.toString()).isEmpty(); - assertThat(stderr.toString()) - .contains( - "it would produce code that does not parse (9:13: error: unclosed string literal)"); - assertThat(result).isEqualTo(1); - } - @Test public void oneFileParseError() throws Exception { String one = "class One {\n"; diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java index 0c69ea61c..9a5875dec 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java @@ -18,6 +18,7 @@ import static org.junit.Assume.assumeTrue; import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -106,4 +107,85 @@ class T { } """); } + + // https://github.com/google/google-java-format/issues/1186 + @Test + public void escapedBackslashBeforeT() throws Exception { + String input = + """ + class T { + String t = + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\\\\t"; + } + """; + String output = + """ + class T { + String t = + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\\\\t"; + } + """; + assertThat(new Formatter().formatSourceAndFixImports(input)).isEqualTo(output); + } + + // https://github.com/google/google-java-format/issues/1439 + @Test + public void escapedBackslashes() throws Exception { + String input = + """ + class T { + String s = "%s xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\\\\text{C} \\\\n \\\\\\\\"; + } + """ + .formatted("word ".repeat(20)); + String output = + """ + class T { + String s = + "word word word word word word word word word word word word word word word word word word" + + " word word " + + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\\\\text{C} \\\\n" + + " \\\\\\\\"; + } + """; + assertThat(new Formatter().formatSourceAndFixImports(input)).isEqualTo(output); + } + + @Test + public void escapesRandomized() throws Exception { + // The source text of string literal characters and escape sequences, including escaped + // backslashes followed by characters that would otherwise form escapes, and octal and Unicode + // escapes. + String[] atoms = { + "a", + "bb", + " ", + "\\\\", + "\\t", + "\\n", + "\\r\\n", + "\\\"", + "\\\\t", + "\\\\n", + "\\\\\\t", + "\\0", + "\\12", + "\\377", + "\\s", + "\\u0041", + "\\\\u0041" + }; + Random random = new Random(42); + Formatter formatter = new Formatter(); + for (int i = 0; i < 200; i++) { + StringBuilder literal = new StringBuilder(); + while (literal.length() < 150) { + literal.append(atoms[random.nextInt(atoms.length)]); + } + String input = "class T {\n String s = \"" + literal + "\";\n}\n"; + // StringWrapper checks that its output parses and has the same AST as its input. (This uses + // `unused` rather than `_` since google-java-format still supports building with JDK 21.) + String unused = StringWrapper.wrap(40, formatter.formatSource(input), formatter); + } + } }