Reflow string literals that exceed the column limit
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=237903057
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
index dcb7dba..80f1579 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
@@ -41,6 +41,7 @@
private final boolean dryRun;
private final boolean setExitIfChanged;
private final Optional<String> assumeFilename;
+ private final boolean reflowLongStrings;
CommandLineOptions(
ImmutableList<String> files,
@@ -57,7 +58,8 @@
boolean removeUnusedImports,
boolean dryRun,
boolean setExitIfChanged,
- Optional<String> assumeFilename) {
+ Optional<String> assumeFilename,
+ boolean reflowLongStrings) {
this.files = files;
this.inPlace = inPlace;
this.lines = lines;
@@ -73,6 +75,7 @@
this.dryRun = dryRun;
this.setExitIfChanged = setExitIfChanged;
this.assumeFilename = assumeFilename;
+ this.reflowLongStrings = reflowLongStrings;
}
/** The files to format. */
@@ -152,6 +155,10 @@
return assumeFilename;
}
+ boolean reflowLongStrings() {
+ return reflowLongStrings;
+ }
+
/** Returns true if partial formatting was selected. */
boolean isSelection() {
return !lines().isEmpty() || !offsets().isEmpty() || !lengths().isEmpty();
@@ -178,6 +185,7 @@
private boolean dryRun = false;
private boolean setExitIfChanged = false;
private Optional<String> assumeFilename = Optional.empty();
+ private boolean reflowLongStrings = true;
ImmutableList.Builder<String> filesBuilder() {
return files;
@@ -252,6 +260,11 @@
return this;
}
+ Builder reflowLongStrings(boolean reflowLongStrings) {
+ this.reflowLongStrings = reflowLongStrings;
+ return this;
+ }
+
CommandLineOptions build() {
return new CommandLineOptions(
files.build(),
@@ -268,7 +281,8 @@
removeUnusedImports,
dryRun,
setExitIfChanged,
- assumeFilename);
+ assumeFilename,
+ reflowLongStrings);
}
}
}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
index 7b27807..5abc54a 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
@@ -105,6 +105,9 @@
case "--skip-removing-unused-imports":
optionsBuilder.removeUnusedImports(false);
break;
+ case "--skip-reflowing-long-strings":
+ optionsBuilder.reflowLongStrings(false);
+ break;
case "-":
optionsBuilder.stdin(true);
break;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
index 061ae4b..487da0c 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
@@ -44,6 +44,9 @@
String formatted =
new Formatter(options).formatSource(input, characterRanges(input).asRanges());
formatted = fixImports(formatted);
+ if (parameters.reflowLongStrings()) {
+ formatted = StringWrapper.wrap(options.maxLineLength(), formatted);
+ }
return formatted;
}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
index 2b76d8e..fb66108 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -217,7 +217,9 @@
public String formatSourceAndFixImports(String input) throws FormatterException {
input = ImportOrderer.reorderImports(input);
input = RemoveUnusedImports.removeUnusedImports(input);
- return formatSource(input);
+ String formatted = formatSource(input);
+ formatted = StringWrapper.wrap(formatted);
+ return formatted;
}
/**
diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
new file mode 100644
index 0000000..f985d22
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.Iterables.getLast;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Range;
+import com.google.common.collect.TreeRangeMap;
+import com.google.googlejavaformat.Newlines;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.openjdk.javax.tools.Diagnostic;
+import org.openjdk.javax.tools.DiagnosticCollector;
+import org.openjdk.javax.tools.DiagnosticListener;
+import org.openjdk.javax.tools.JavaFileObject;
+import org.openjdk.javax.tools.SimpleJavaFileObject;
+import org.openjdk.javax.tools.StandardLocation;
+import org.openjdk.source.tree.BinaryTree;
+import org.openjdk.source.tree.LiteralTree;
+import org.openjdk.source.tree.Tree;
+import org.openjdk.source.util.TreePath;
+import org.openjdk.source.util.TreePathScanner;
+import org.openjdk.tools.javac.file.JavacFileManager;
+import org.openjdk.tools.javac.parser.JavacParser;
+import org.openjdk.tools.javac.parser.ParserFactory;
+import org.openjdk.tools.javac.tree.JCTree;
+import org.openjdk.tools.javac.util.Context;
+import org.openjdk.tools.javac.util.Log;
+import org.openjdk.tools.javac.util.Options;
+import org.openjdk.tools.javac.util.Position;
+
+/** Wraps string literals that exceed the column limit. */
+public final class StringWrapper {
+ /** Reflows long string literals in the given Java source code. */
+ public static String wrap(String input) throws FormatterException {
+ return StringWrapper.wrap(JavaFormatterOptions.defaultOptions().maxLineLength(), input);
+ }
+
+ /**
+ * Reflows string literals in the given Java source code that extend past the given column limit.
+ */
+ static String wrap(final int columnLimit, final String input) throws FormatterException {
+ if (!longLines(columnLimit, input)) {
+ // fast path
+ return input;
+ }
+
+ JCTree.JCCompilationUnit unit = parse(input, /* allowStringFolding= */ false);
+
+ // Paths to string literals that extend past the column limit.
+ List<TreePath> toFix = new ArrayList<>();
+ final Position.LineMap lineMap = unit.getLineMap();
+ new TreePathScanner<Void, Void>() {
+ @Override
+ public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
+ if (literalTree.getKind() != Tree.Kind.STRING_LITERAL) {
+ return null;
+ }
+ int startPosition = getStartPosition(literalTree);
+ int endPosition = getEndPosition(unit, literalTree);
+ int lineStart = startPosition - lineMap.getColumnNumber(startPosition);
+ int lineEnd = endPosition;
+ while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+ lineEnd++;
+ }
+ if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) {
+ return null;
+ }
+ if (!lintable(input.substring(lineStart, lineEnd))) {
+ return null;
+ }
+ toFix.add(getCurrentPath());
+ return null;
+ }
+ }.scan(new TreePath(unit), null);
+
+ TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
+ for (TreePath path : toFix) {
+ // Find the outermost contiguous enclosing concatenation expression
+ TreePath enclosing = path;
+ while (enclosing.getParentPath().getLeaf().getKind() == Tree.Kind.PLUS) {
+ enclosing = enclosing.getParentPath();
+ }
+ // Is the literal being wrapped the first in a chain of concatenation expressions?
+ // i.e. `ONE + TWO + THREE`
+ // We need this information to handle continuation indents.
+ AtomicBoolean first = new AtomicBoolean(false);
+ // Finds the set of string literals in the concat expression that includes the one that needs
+ // to be wrapped.
+ List<Tree> flat = flatten(input, unit, path, enclosing, first);
+ // Zero-indexed start column
+ int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1;
+
+ // Handling leaving trailing non-string tokens at the end of the literal,
+ // e.g. the trailing `);` in `foo("...");`.
+ int end = getEndPosition(unit, getLast(flat));
+ int lineEnd = end;
+ while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+ lineEnd++;
+ }
+ int trailing = lineEnd - end;
+
+ // Get the original source text of the string literals, excluding `"` and `+`.
+ ImmutableList<String> components = stringComponents(input, unit, flat);
+ replacements.put(
+ Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))),
+ reflow(columnLimit, startColumn, trailing, components, first.get()));
+ }
+ String result = applyReplacements(input, replacements);
+
+ {
+ // We really don't want bugs in this pass to change the behaviour of programs we're
+ // formatting, so check that the pretty-printed AST is the same before and after reformatting.
+ String expected = parse(input, /* allowStringFolding= */ true).toString();
+ String actual = parse(result, /* allowStringFolding= */ true).toString();
+ if (!expected.equals(actual)) {
+ throw new FormatterException(
+ String.format(
+ "Something has gone terribly wrong. Please file a bug.\n\n"
+ + "=== Actual: ===\n%s\n=== Expected: ===\n%s\n",
+ actual, expected));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the source text of the given string literal trees, excluding the leading and trailing
+ * double-quotes and the `+` operator.
+ */
+ private static ImmutableList<String> stringComponents(
+ String input, JCTree.JCCompilationUnit unit, List<Tree> flat) {
+ ImmutableList.Builder<String> result = ImmutableList.builder();
+ for (Tree tree : flat) {
+ // adjust for leading and trailing double quotes
+ String text = input.substring(getStartPosition(tree) + 1, getEndPosition(unit, tree) - 1);
+ int start = 0;
+ for (int idx = 0; idx < text.length(); idx++) {
+ if (CharMatcher.whitespace().matches(text.charAt(idx))) {
+ result.add(text.substring(start, idx));
+ start = idx;
+ continue;
+ }
+ if (hasEscapedWhitespaceAt(text, idx) != -1) {
+ result.add(text.substring(start, idx));
+ start = idx;
+ continue;
+ }
+
+ if (hasEscapedNewlineAt(text, idx) != -1) {
+ int length;
+ while ((length = hasEscapedNewlineAt(text, idx)) != -1) {
+ idx += length;
+ }
+ result.add(text.substring(start, idx));
+ start = idx;
+ continue;
+ }
+ }
+ if (start < text.length()) {
+ result.add(text.substring(start));
+ }
+ }
+ return result.build();
+ }
+
+ static int hasEscapedWhitespaceAt(String input, int idx) {
+ return Stream.of("\\t")
+ .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+ .filter(x -> x != -1)
+ .findFirst()
+ .orElse(-1);
+ }
+
+ static int hasEscapedNewlineAt(String input, int idx) {
+ return Stream.of("\\n\\r", "\\r", "\\n")
+ .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+ .filter(x -> x != -1)
+ .findFirst()
+ .orElse(-1);
+ }
+
+ /**
+ * Reflows the given source text, trying to split on word boundaries.
+ *
+ * @param columnLimit the number of columns to wrap at
+ * @param startColumn the column position of the beginning of the original text
+ * @param trailing extra space to leave after the last line
+ * @param components the text to reflow
+ * @param first0 true if the text includes the beginning of its enclosing concat chain, i.e. a
+ */
+ private static String reflow(
+ int columnLimit,
+ int startColumn,
+ int trailing,
+ ImmutableList<String> components,
+ boolean first0) {
+ // We have space between the start column and the limit to output the first line.
+ // Reserve two spaces for the quotes.
+ int width = columnLimit - startColumn - 2;
+ Deque<String> input = new ArrayDeque<>(components);
+ List<String> lines = new ArrayList<>();
+ boolean first = first0;
+ while (!input.isEmpty()) {
+ int length = 0;
+ List<String> line = new ArrayList<>();
+ if (input.stream().mapToInt(x -> x.length()).sum() <= width) {
+ width -= trailing;
+ }
+ while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) < width)) {
+ String text = input.removeFirst();
+ line.add(text);
+ length += text.length();
+ if (text.endsWith("\\n") || text.endsWith("\\r")) {
+ break;
+ }
+ }
+ if (line.isEmpty()) {
+ line.add(input.removeFirst());
+ }
+ // add the split line to the output, and process whatever's left
+ lines.add(String.join("", line));
+ if (first) {
+ width -= 6; // subsequent lines have a four-space continuation indent and a `+ `
+ first = false;
+ }
+ }
+
+ return lines.stream()
+ .collect(
+ joining(
+ "\"\n" + Strings.repeat(" ", startColumn + (first0 ? 4 : -2)) + "+ \"",
+ "\"",
+ "\""));
+ }
+
+ /**
+ * Flattens the given binary expression tree, and extracts the subset that contains the given path
+ * and any adjacent nodes that are also string literals.
+ */
+ private static List<Tree> flatten(
+ String input,
+ JCTree.JCCompilationUnit unit,
+ TreePath path,
+ TreePath parent,
+ AtomicBoolean firstInChain) {
+ List<Tree> flat = new ArrayList<>();
+
+ // flatten the expression tree with a pre-order traversal
+ ArrayDeque<Tree> todo = new ArrayDeque<>();
+ todo.add(parent.getLeaf());
+ while (!todo.isEmpty()) {
+ Tree first = todo.removeFirst();
+ if (first.getKind() == Tree.Kind.PLUS) {
+ BinaryTree bt = (BinaryTree) first;
+ todo.addFirst(bt.getRightOperand());
+ todo.addFirst(bt.getLeftOperand());
+ } else {
+ flat.add(first);
+ }
+ }
+
+ int idx = flat.indexOf(path.getLeaf());
+ Verify.verify(idx != -1);
+
+ // walk outwards from the leaf for adjacent string literals to also reflow
+ int startIdx = idx;
+ int endIdx = idx + 1;
+ while (startIdx > 0
+ && flat.get(startIdx - 1).getKind() == Tree.Kind.STRING_LITERAL
+ && noComments(input, unit, flat.get(startIdx - 1), flat.get(startIdx))) {
+ startIdx--;
+ }
+ while (endIdx < flat.size()
+ && flat.get(endIdx).getKind() == Tree.Kind.STRING_LITERAL
+ && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) {
+ endIdx++;
+ }
+
+ firstInChain.set(startIdx == 0);
+ return ImmutableList.copyOf(flat.subList(startIdx, endIdx));
+ }
+
+ private static boolean noComments(
+ String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) {
+ return STRING_CONCAT_DELIMITER.matchesAllOf(
+ input.subSequence(getEndPosition(unit, one), getStartPosition(two)));
+ }
+
+ public static final CharMatcher STRING_CONCAT_DELIMITER =
+ CharMatcher.whitespace().or(CharMatcher.anyOf("\"+"));
+
+ private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) {
+ return ((JCTree) tree).getEndPosition(unit.endPositions);
+ }
+
+ private static int getStartPosition(Tree tree) {
+ return ((JCTree) tree).getStartPosition();
+ }
+
+ /**
+ * Match the heuristic in the Google Style checkstyle config which suppresses line length warnings
+ * if the given line contains a run of >80 non-whitespace characters, to allow e.g. URLs.
+ */
+ private static boolean lintable(String line) {
+ return !UNLINTABLE.matcher(line).matches();
+ }
+
+ private static final Pattern UNLINTABLE = Pattern.compile(".*[^ ]{80,}.*");
+
+ /** Returns true if any lines in the given Java source exceed the column limit. */
+ private static boolean longLines(int columnLimit, String input) {
+ // TODO(cushon): consider adding Newlines.lineIterable?
+ Iterator<String> it = Newlines.lineIterator(input);
+ while (it.hasNext()) {
+ String line = it.next();
+ if (line.length() > columnLimit && lintable(line)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Parses the given Java source. */
+ private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding)
+ throws FormatterException {
+ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+ Context context = new Context();
+ context.put(DiagnosticListener.class, diagnostics);
+ Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding));
+ JCTree.JCCompilationUnit unit;
+ JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+ try {
+ fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ SimpleJavaFileObject sjfo =
+ new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return source;
+ }
+ };
+ Log.instance(context).useSource(sjfo);
+ ParserFactory parserFactory = ParserFactory.instance(context);
+ JavacParser parser =
+ parserFactory.newParser(
+ source, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
+ unit = parser.parseCompilationUnit();
+ unit.sourcefile = sjfo;
+ Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+ Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+ if (!Iterables.isEmpty(errorDiagnostics)) {
+ // error handling is done during formatting
+ throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+ }
+ return unit;
+ }
+
+ /** Applies replacements to the given string. */
+ private static String applyReplacements(
+ String javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
+ // process in descending order so the replacement ranges aren't perturbed if any replacements
+ // differ in size from the input
+ Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
+ if (ranges.isEmpty()) {
+ return javaInput;
+ }
+ StringBuilder sb = new StringBuilder(javaInput);
+ for (Map.Entry<Range<Integer>, String> entry : ranges.entrySet()) {
+ Range<Integer> range = entry.getKey();
+ sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
+ }
+ return sb.toString();
+ }
+
+ private StringWrapper() {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
index 77cdf65..169f6ce 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
@@ -46,6 +46,8 @@
" Do not fix the import order. Unused imports will still be removed.",
" --skip-removing-unused-imports",
" Do not remove unused imports. Imports will still be sorted.",
+ " . --skip-reflowing-long-strings",
+ " Do not reflow string literals that exceed the column limit.",
" --dry-run, -n",
" Prints the paths of the files whose contents would change if the formatter were run"
+ " normally.",
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
index ebf5fbd..66802f5 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
@@ -53,6 +53,7 @@
assertThat(options.removeUnusedImports()).isTrue();
assertThat(options.dryRun()).isFalse();
assertThat(options.setExitIfChanged()).isFalse();
+ assertThat(options.reflowLongStrings()).isTrue();
}
@Test
@@ -186,4 +187,12 @@
assertThat(CommandLineOptionsParser.parse(Arrays.asList("Foo.java")).assumeFilename())
.isEmpty();
}
+
+ @Test
+ public void skipReflowLongStrings() {
+ assertThat(
+ CommandLineOptionsParser.parse(Arrays.asList("--skip-reflowing-long-strings"))
+ .reflowLongStrings())
+ .isFalse();
+ }
}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
index 7a3efa6..7af761e 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
@@ -509,4 +509,59 @@
assertThat(main.format("--dry-run", "--assume-filename=Foo.java", "-")).isEqualTo(0);
assertThat(out.toString()).isEqualTo("Foo.java" + System.lineSeparator());
}
+
+ @Test
+ public void reflowLongStrings() throws Exception {
+ String[] input = {
+ "class T {", //
+ " String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
+ + " one had a chance to interrupt\";",
+ "}"
+ };
+ String[] expected = {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
+ + " a\"",
+ " + \" chance to interrupt\";",
+ "}",
+ "",
+ };
+ InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+ StringWriter out = new StringWriter();
+ Main main =
+ new Main(
+ new PrintWriter(out, true),
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+ in);
+ assertThat(main.format("-")).isEqualTo(0);
+ assertThat(out.toString()).isEqualTo(joiner.join(expected));
+ }
+
+ @Test
+ public void noReflowLongStrings() throws Exception {
+ String[] input = {
+ "class T {", //
+ " String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
+ + " one had a chance to interrupt\";",
+ "}"
+ };
+ String[] expected = {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
+ + " a chance to interrupt\";",
+ "}",
+ "",
+ };
+ InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+ StringWriter out = new StringWriter();
+ Main main =
+ new Main(
+ new PrintWriter(out, true),
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+ in);
+ assertThat(main.format("--skip-reflowing-long-strings", "-")).isEqualTo(0);
+ assertThat(out.toString()).isEqualTo(joiner.join(expected));
+ }
}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
new file mode 100644
index 0000000..99f876b
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** {@link StringWrapper}IntegrationTest */
+@RunWith(Parameterized.class)
+public class StringWrapperIntegrationTest {
+
+ @Parameters
+ public static Collection<Object[]> parameters() {
+ String[][][] inputsAndOutputs = {
+ {
+ {
+ "class T {", //
+ " String s =",
+ " \"one long incredibly unbroken sentence\"",
+ " + \" moving from topic to topic\"",
+ " + \" so that no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from\"",
+ " + \" topic to topic so that\"",
+ " + \" no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from topic to topic so that\"",
+ " + \" no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from\"",
+ " + \" topic to topic so that\"",
+ " + \" no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from topic to topic\"",
+ " + \" so that no-one had a chance to interr\"",
+ " + \"upt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from\"",
+ " + \" topic to topic so that\"",
+ " + \" no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"one long incredibly unbroken sentence moving from topic to topic so that"
+ + " no-one had a chance to interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from\"",
+ " + \" topic to topic so that\"",
+ " + \" no-one had a chance to\"",
+ " + \" interrupt\";",
+ "}",
+ },
+ },
+ {
+ {
+ "class T {", //
+ " String s =",
+ " \"one long incredibly unbroken sentence\"",
+ " + \" moving from topic to topic\"",
+ " + 42",
+ " + \" so that no-one had a chance to interr\"",
+ " + \"upt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence moving from\"",
+ " + \" topic to topic\"",
+ " + 42",
+ " + \" so that no-one had a\"",
+ " + \" chance to interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto\"",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"\\n\\none\\nlong\\nincredibly\\nunbroken\\nsentence\\nmoving\\nfrom\\n"
+ + " topic\\nto\\n topic\\nso\\nthat\\nno-one\\nhad\\na\\nchance\\nto\\ninterrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"\\n\\n\"",
+ " + \"one\\n\"",
+ " + \"long\\n\"",
+ " + \"incredibly\\n\"",
+ " + \"unbroken\\n\"",
+ " + \"sentence\\n\"",
+ " + \"moving\\n\"",
+ " + \"from\\n\"",
+ " + \" topic\\n\"",
+ " + \"to\\n\"",
+ " + \" topic\\n\"",
+ " + \"so\\n\"",
+ " + \"that\\n\"",
+ " + \"no-one\\n\"",
+ " + \"had\\n\"",
+ " + \"a\\n\"",
+ " + \"chance\\n\"",
+ " + \"to\\n\"",
+ " + \"interrupt\";",
+ "}",
+ },
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"\\n\\n\\none\\n\\nlong\\n\\nincredibly\\n\\nunbroken\\n\\nsentence\\n\\n"
+ + "moving\\n\\nfrom\\n\\n topic\\n\\nto\\n\\n topic\\n\\nso\\n\\nthat\\n\\nno-one"
+ + "\\n\\nhad\\n\\na\\n\\nchance\\n\\nto\\n\\ninterrupt\\n\\n\\n\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"\\n\\n\\n\"",
+ " + \"one\\n\\n\"",
+ " + \"long\\n\\n\"",
+ " + \"incredibly\\n\\n\"",
+ " + \"unbroken\\n\\n\"",
+ " + \"sentence\\n\\n\"",
+ " + \"moving\\n\\n\"",
+ " + \"from\\n\\n\"",
+ " + \" topic\\n\\n\"",
+ " + \"to\\n\\n\"",
+ " + \" topic\\n\\n\"",
+ " + \"so\\n\\n\"",
+ " + \"that\\n\\n\"",
+ " + \"no-one\\n\\n\"",
+ " + \"had\\n\\n\"",
+ " + \"a\\n\\n\"",
+ " + \"chance\\n\\n\"",
+ " + \"to\\n\\n\"",
+ " + \"interrupt\\n\\n\\n\";",
+ "}",
+ },
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"onelongincrediblyunbrokensenten\\tcemovingfromtopictotopicsothatnoonehada"
+ + "chance tointerrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"onelongincrediblyunbrokensenten\"",
+ " + \"\\tcemovingfromtopictotopicsothatnoonehadachance\"",
+ " + \" tointerrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+ + "chancetointerrupt_____________________)_\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+ + "chancetointerrupt_____________________)_\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"onelongincrediblyunbrokensentencemovingfromtopictotopicsot atnoonehada"
+ + "chancetointerrupt______________________\";;",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"onelongincrediblyunbrokensentencemovingfromtopictotopicsot\"",
+ " + \" atnoonehadachancetointerrupt______________________\";",
+ " ;",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s = \"__ onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+ + "chanceto interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"__ onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto\"",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ {
+ {
+ "class T {", //
+ " String s =",
+ " \"one long incredibly unbroken sentence\"",
+ " // comment",
+ " + \" moving from topic to topic\"",
+ " // comment",
+ " + \" so that no-one had a chance to\"",
+ " // comment",
+ " + \" interrupt\";",
+ "}"
+ },
+ {
+ "class T {",
+ " String s =",
+ " \"one long incredibly unbroken\"",
+ " + \" sentence\"",
+ " // comment",
+ " + \" moving from topic to\"",
+ " + \" topic\"",
+ " // comment",
+ " + \" so that no-one had a\"",
+ " + \" chance to\"",
+ " // comment",
+ " + \" interrupt\";",
+ "}",
+ }
+ },
+ };
+ return Arrays.stream(inputsAndOutputs)
+ .map(
+ inputAndOutput -> {
+ assertThat(inputAndOutput).hasLength(2);
+ return new String[] {
+ Joiner.on('\n').join(inputAndOutput[0]) + '\n', //
+ Joiner.on('\n').join(inputAndOutput[1]) + '\n',
+ };
+ })
+ .collect(toImmutableList());
+ }
+
+ private final String input;
+ private final String output;
+
+ public StringWrapperIntegrationTest(String input, String output) {
+ this.input = input;
+ this.output = output;
+ }
+
+ @Test
+ public void test() throws Exception {
+ assertThat(StringWrapper.wrap(40, new Formatter().formatSource(input))).isEqualTo(output);
+ }
+
+ @Test
+ public void idempotent() throws Exception {
+ String wrap = StringWrapper.wrap(40, new Formatter().formatSource(input));
+ assertThat(wrap).isEqualTo(new Formatter().formatSource(wrap));
+ }
+}