Skip to content

Commit ce3cb59

Browse files
cushongoogle-java-format Team
authored and
google-java-format Team
committed
Re-indent text blocks
PiperOrigin-RevId: 609526784
1 parent 92c609a commit ce3cb59

File tree

5 files changed

+327
-57
lines changed

5 files changed

+327
-57
lines changed

core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java

+3
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,9 @@ public Void visitMemberSelect(MemberSelectTree node, Void unused) {
16671667
public Void visitLiteral(LiteralTree node, Void unused) {
16681668
sync(node);
16691669
String sourceForNode = getSourceForNode(node, getCurrentPath());
1670+
if (sourceForNode.startsWith("\"\"\"")) {
1671+
builder.forcedBreak();
1672+
}
16701673
if (isUnaryMinusLiteral(sourceForNode)) {
16711674
token("-");
16721675
sourceForNode = sourceForNode.substring(1).trim();

core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java

+155-44
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.googlejavaformat.java;
1616

17+
import static com.google.common.collect.ImmutableList.toImmutableList;
1718
import static com.google.common.collect.Iterables.getLast;
1819
import static java.lang.Math.min;
1920
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -44,6 +45,7 @@
4445
import com.sun.tools.javac.util.Position;
4546
import java.io.IOException;
4647
import java.io.UncheckedIOException;
48+
import java.lang.reflect.Method;
4749
import java.net.URI;
4850
import java.util.ArrayDeque;
4951
import java.util.ArrayList;
@@ -59,6 +61,7 @@
5961
import javax.tools.JavaFileObject;
6062
import javax.tools.SimpleJavaFileObject;
6163
import javax.tools.StandardLocation;
64+
import org.checkerframework.checker.nullness.qual.Nullable;
6265

6366
/** Wraps string literals that exceed the column limit. */
6467
public final class StringWrapper {
@@ -72,7 +75,7 @@ public static String wrap(String input, Formatter formatter) throws FormatterExc
7275
*/
7376
static String wrap(final int columnLimit, String input, Formatter formatter)
7477
throws FormatterException {
75-
if (!longLines(columnLimit, input)) {
78+
if (!needWrapping(columnLimit, input)) {
7679
// fast path
7780
return input;
7881
}
@@ -111,20 +114,56 @@ static String wrap(final int columnLimit, String input, Formatter formatter)
111114

112115
private static TreeRangeMap<Integer, String> getReflowReplacements(
113116
int columnLimit, final String input) throws FormatterException {
114-
JCTree.JCCompilationUnit unit = parse(input, /* allowStringFolding= */ false);
115-
String separator = Newlines.guessLineSeparator(input);
117+
return new Reflower(columnLimit, input).getReflowReplacements();
118+
}
119+
120+
private static class Reflower {
121+
122+
private final String input;
123+
private final int columnLimit;
124+
private final String separator;
125+
private final JCTree.JCCompilationUnit unit;
126+
private final Position.LineMap lineMap;
127+
128+
Reflower(int columnLimit, String input) throws FormatterException {
129+
this.columnLimit = columnLimit;
130+
this.input = input;
131+
this.separator = Newlines.guessLineSeparator(input);
132+
this.unit = parse(input, /* allowStringFolding= */ false);
133+
this.lineMap = unit.getLineMap();
134+
}
135+
136+
TreeRangeMap<Integer, String> getReflowReplacements() {
137+
// Paths to string literals that extend past the column limit.
138+
List<TreePath> longStringLiterals = new ArrayList<>();
139+
// Paths to text blocks to be re-indented.
140+
List<Tree> textBlocks = new ArrayList<>();
141+
new LongStringsAndTextBlockScanner(longStringLiterals, textBlocks)
142+
.scan(new TreePath(unit), null);
143+
TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
144+
indentTextBlocks(replacements, textBlocks);
145+
wrapLongStrings(replacements, longStringLiterals);
146+
return replacements;
147+
}
148+
149+
private class LongStringsAndTextBlockScanner extends TreePathScanner<Void, Void> {
150+
151+
private final List<TreePath> longStringLiterals;
152+
private final List<Tree> textBlocks;
153+
154+
LongStringsAndTextBlockScanner(List<TreePath> longStringLiterals, List<Tree> textBlocks) {
155+
this.longStringLiterals = longStringLiterals;
156+
this.textBlocks = textBlocks;
157+
}
116158

117-
// Paths to string literals that extend past the column limit.
118-
List<TreePath> toFix = new ArrayList<>();
119-
final Position.LineMap lineMap = unit.getLineMap();
120-
new TreePathScanner<Void, Void>() {
121159
@Override
122160
public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
123161
if (literalTree.getKind() != Kind.STRING_LITERAL) {
124162
return null;
125163
}
126164
int pos = getStartPosition(literalTree);
127165
if (input.substring(pos, min(input.length(), pos + 3)).equals("\"\"\"")) {
166+
textBlocks.add(literalTree);
128167
return null;
129168
}
130169
Tree parent = getCurrentPath().getParentPath().getLeaf();
@@ -140,44 +179,114 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
140179
if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) {
141180
return null;
142181
}
143-
toFix.add(getCurrentPath());
182+
longStringLiterals.add(getCurrentPath());
144183
return null;
145184
}
146-
}.scan(new TreePath(unit), null);
147-
148-
TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
149-
for (TreePath path : toFix) {
150-
// Find the outermost contiguous enclosing concatenation expression
151-
TreePath enclosing = path;
152-
while (enclosing.getParentPath().getLeaf().getKind() == Tree.Kind.PLUS) {
153-
enclosing = enclosing.getParentPath();
185+
}
186+
187+
private void indentTextBlocks(
188+
TreeRangeMap<Integer, String> replacements, List<Tree> textBlocks) {
189+
for (Tree tree : textBlocks) {
190+
int startPosition = getStartPosition(tree);
191+
int endPosition = getEndPosition(unit, tree);
192+
String text = input.substring(startPosition, endPosition);
193+
194+
// Find the source code of the text block with incidental whitespace removed.
195+
// The first line of the text block is always """, and it does not affect incidental
196+
// whitespace.
197+
ImmutableList<String> initialLines = text.lines().collect(toImmutableList());
198+
String stripped = stripIndent(initialLines.stream().skip(1).collect(joining(separator)));
199+
ImmutableList<String> lines = stripped.lines().collect(toImmutableList());
200+
int deindent =
201+
initialLines.get(1).stripTrailing().length() - lines.get(0).stripTrailing().length();
202+
203+
int startColumn = lineMap.getColumnNumber(startPosition);
204+
String prefix =
205+
(deindent == 0 || lines.stream().anyMatch(x -> x.length() + startColumn > columnLimit))
206+
? ""
207+
: " ".repeat(startColumn - 1);
208+
209+
StringBuilder output = new StringBuilder("\"\"\"");
210+
for (int i = 0; i < lines.size(); i++) {
211+
String line = lines.get(i);
212+
String trimmed = line.stripLeading().stripTrailing();
213+
output.append(separator);
214+
if (!trimmed.isEmpty()) {
215+
// Don't add incidental leading whitespace to empty lines
216+
output.append(prefix);
217+
}
218+
if (i == lines.size() - 1 && trimmed.equals("\"\"\"")) {
219+
// If the trailing line is just """, indenting is more than the prefix of incidental
220+
// whitespace has no effect, and results in a javac text-blocks warning that 'trailing
221+
// white space will be removed'.
222+
output.append("\"\"\"");
223+
} else {
224+
output.append(line);
225+
}
226+
}
227+
replacements.put(Range.closedOpen(startPosition, endPosition), output.toString());
154228
}
155-
// Is the literal being wrapped the first in a chain of concatenation expressions?
156-
// i.e. `ONE + TWO + THREE`
157-
// We need this information to handle continuation indents.
158-
AtomicBoolean first = new AtomicBoolean(false);
159-
// Finds the set of string literals in the concat expression that includes the one that needs
160-
// to be wrapped.
161-
List<Tree> flat = flatten(input, unit, path, enclosing, first);
162-
// Zero-indexed start column
163-
int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1;
164-
165-
// Handling leaving trailing non-string tokens at the end of the literal,
166-
// e.g. the trailing `);` in `foo("...");`.
167-
int end = getEndPosition(unit, getLast(flat));
168-
int lineEnd = end;
169-
while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
170-
lineEnd++;
229+
}
230+
231+
private void wrapLongStrings(
232+
TreeRangeMap<Integer, String> replacements, List<TreePath> longStringLiterals) {
233+
for (TreePath path : longStringLiterals) {
234+
// Find the outermost contiguous enclosing concatenation expression
235+
TreePath enclosing = path;
236+
while (enclosing.getParentPath().getLeaf().getKind() == Kind.PLUS) {
237+
enclosing = enclosing.getParentPath();
238+
}
239+
// Is the literal being wrapped the first in a chain of concatenation expressions?
240+
// i.e. `ONE + TWO + THREE`
241+
// We need this information to handle continuation indents.
242+
AtomicBoolean first = new AtomicBoolean(false);
243+
// Finds the set of string literals in the concat expression that includes the one that
244+
// needs
245+
// to be wrapped.
246+
List<Tree> flat = flatten(input, unit, path, enclosing, first);
247+
// Zero-indexed start column
248+
int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1;
249+
250+
// Handling leaving trailing non-string tokens at the end of the literal,
251+
// e.g. the trailing `);` in `foo("...");`.
252+
int end = getEndPosition(unit, getLast(flat));
253+
int lineEnd = end;
254+
while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
255+
lineEnd++;
256+
}
257+
int trailing = lineEnd - end;
258+
259+
// Get the original source text of the string literals, excluding `"` and `+`.
260+
ImmutableList<String> components = stringComponents(input, unit, flat);
261+
replacements.put(
262+
Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))),
263+
reflow(separator, columnLimit, startColumn, trailing, components, first.get()));
171264
}
172-
int trailing = lineEnd - end;
265+
}
266+
}
267+
268+
private static final Method STRIP_INDENT = getStripIndent();
269+
270+
private static @Nullable Method getStripIndent() {
271+
if (Runtime.version().feature() < 15) {
272+
return null;
273+
}
274+
try {
275+
return String.class.getMethod("stripIndent");
276+
} catch (NoSuchMethodException e) {
277+
throw new LinkageError(e.getMessage(), e);
278+
}
279+
}
173280

174-
// Get the original source text of the string literals, excluding `"` and `+`.
175-
ImmutableList<String> components = stringComponents(input, unit, flat);
176-
replacements.put(
177-
Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))),
178-
reflow(separator, columnLimit, startColumn, trailing, components, first.get()));
281+
private static String stripIndent(String input) {
282+
if (STRIP_INDENT == null) {
283+
return input;
284+
}
285+
try {
286+
return (String) STRIP_INDENT.invoke(input);
287+
} catch (ReflectiveOperationException e) {
288+
throw new LinkageError(e.getMessage(), e);
179289
}
180-
return replacements;
181290
}
182291

183292
/**
@@ -364,13 +473,16 @@ private static int getStartPosition(Tree tree) {
364473
return ((JCTree) tree).getStartPosition();
365474
}
366475

367-
/** Returns true if any lines in the given Java source exceed the column limit. */
368-
private static boolean longLines(int columnLimit, String input) {
476+
/**
477+
* Returns true if any lines in the given Java source exceed the column limit, or contain a {@code
478+
* """} that could indicate a text block.
479+
*/
480+
private static boolean needWrapping(int columnLimit, String input) {
369481
// TODO(cushon): consider adding Newlines.lineIterable?
370482
Iterator<String> it = Newlines.lineIterator(input);
371483
while (it.hasNext()) {
372484
String line = it.next();
373-
if (line.length() > columnLimit) {
485+
if (line.length() > columnLimit || line.contains("\"\"\"")) {
374486
return true;
375487
}
376488
}
@@ -385,7 +497,6 @@ private static JCTree.JCCompilationUnit parse(String source, boolean allowString
385497
context.put(DiagnosticListener.class, diagnostics);
386498
Options.instance(context).put("--enable-preview", "true");
387499
Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding));
388-
JCTree.JCCompilationUnit unit;
389500
JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
390501
try {
391502
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
@@ -404,7 +515,7 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
404515
JavacParser parser =
405516
parserFactory.newParser(
406517
source, /* keepDocComments= */ true, /* keepEndPos= */ true, /* keepLineMap= */ true);
407-
unit = parser.parseCompilationUnit();
518+
JCTree.JCCompilationUnit unit = parser.parseCompilationUnit();
408519
unit.sourcefile = sjfo;
409520
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
410521
Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);

core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java

+80-9
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,90 @@ public void textBlock() throws Exception {
6060
lines(
6161
"package com.mypackage;",
6262
"public class ReproBug {",
63-
" private String myString;",
64-
" private ReproBug() {",
65-
" String str =",
66-
" \"\"\"",
67-
" "
68-
+ " {\"sourceEndpoint\":\"ri.something.1-1.object-internal.1\",\"targetEndpoint\":\"ri.some"
69-
+ "thing.1-1.object-internal.2\",\"typeId\":\"typeId\"}\"\"\";",
70-
" myString = str;",
71-
" }",
63+
" private String myString;",
64+
" private ReproBug() {",
65+
" String str =",
66+
" \"\"\"",
67+
"{\"sourceEndpoint\":\"ri.something.1-1.object-internal.1\",\"targetEndpoint"
68+
+ "\":\"ri.something.1-1.object-internal.2\",\"typeId\":\"typeId\"}\"\"\";",
69+
" myString = str;",
70+
" }",
7271
"}");
7372
assertThat(StringWrapper.wrap(100, input, new Formatter())).isEqualTo(input);
7473
}
7574

75+
// Test that whitespace handling on text block lines only removes spaces, not other control
76+
// characters.
77+
@Test
78+
public void textBlockControlCharacter() throws Exception {
79+
assumeTrue(Runtime.version().feature() >= 15);
80+
// We want an actual control character in the Java source being formatted, not a unicode escape,
81+
// i.e. the escape below doesn't need to be double-escaped.
82+
String input =
83+
lines(
84+
"package p;",
85+
"public class T {",
86+
" String s =",
87+
" \"\"\"",
88+
" \u0007lorem",
89+
" \u0007",
90+
" ipsum",
91+
" \"\"\";",
92+
"}");
93+
String actual = StringWrapper.wrap(100, input, new Formatter());
94+
assertThat(actual).isEqualTo(input);
95+
}
96+
97+
@Test
98+
public void textBlockTrailingWhitespace() throws Exception {
99+
assumeTrue(Runtime.version().feature() >= 15);
100+
String input =
101+
lines(
102+
"public class T {",
103+
" String s =",
104+
" \"\"\"",
105+
" lorem ",
106+
" ipsum",
107+
" \"\"\";",
108+
"}");
109+
String expected =
110+
lines(
111+
"public class T {",
112+
" String s =",
113+
" \"\"\"",
114+
" lorem",
115+
" ipsum",
116+
" \"\"\";",
117+
"}");
118+
String actual = StringWrapper.wrap(100, input, new Formatter());
119+
assertThat(actual).isEqualTo(expected);
120+
}
121+
122+
@Test
123+
public void textBlockSpaceTabMix() throws Exception {
124+
assumeTrue(Runtime.version().feature() >= 15);
125+
String input =
126+
lines(
127+
"public class T {",
128+
" String s =",
129+
" \"\"\"",
130+
" lorem",
131+
" \tipsum",
132+
" \"\"\";",
133+
"}");
134+
String expected =
135+
lines(
136+
"public class T {",
137+
" String s =",
138+
" \"\"\"",
139+
" lorem",
140+
" ipsum",
141+
" \"\"\";",
142+
"}");
143+
String actual = StringWrapper.wrap(100, input, new Formatter());
144+
assertThat(actual).isEqualTo(expected);
145+
}
146+
76147
private static String lines(String... line) {
77148
return Joiner.on('\n').join(line) + '\n';
78149
}

0 commit comments

Comments
 (0)