14
14
15
15
package com.google.googlejavaformat.java;
16
16
17
+
import static com.google.common.collect.ImmutableList.toImmutableList;
17
18
import static com.google.common.collect.Iterables.getLast;
18
19
import static java.lang.Math.min;
19
20
import static java.nio.charset.StandardCharsets.UTF_8;
44
45
import com.sun.tools.javac.util.Position;
45
46
import java.io.IOException;
46
47
import java.io.UncheckedIOException;
48
+
import java.lang.reflect.Method;
47
49
import java.net.URI;
48
50
import java.util.ArrayDeque;
49
51
import java.util.ArrayList;
59
61
import javax.tools.JavaFileObject;
60
62
import javax.tools.SimpleJavaFileObject;
61
63
import javax.tools.StandardLocation;
64
+
import org.checkerframework.checker.nullness.qual.Nullable;
62
65
63
66
/** Wraps string literals that exceed the column limit. */
64
67
public final class StringWrapper {
@@ -72,7 +75,7 @@ public static String wrap(String input, Formatter formatter) throws FormatterExc
72
75
*/
73
76
static String wrap(final int columnLimit, String input, Formatter formatter)
74
77
throws FormatterException {
75
-
if (!longLines(columnLimit, input)) {
78
+
if (!needWrapping(columnLimit, input)) {
76
79
// fast path
77
80
return input;
78
81
}
@@ -111,20 +114,56 @@ static String wrap(final int columnLimit, String input, Formatter formatter)
111
114
112
115
private static TreeRangeMap<Integer, String> getReflowReplacements(
113
116
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
+
}
116
158
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>() {
121
159
@Override
122
160
public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
123
161
if (literalTree.getKind() != Kind.STRING_LITERAL) {
124
162
return null;
125
163
}
126
164
int pos = getStartPosition(literalTree);
127
165
if (input.substring(pos, min(input.length(), pos + 3)).equals("\"\"\"")) {
166
+
textBlocks.add(literalTree);
128
167
return null;
129
168
}
130
169
Tree parent = getCurrentPath().getParentPath().getLeaf();
@@ -140,44 +179,114 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
140
179
if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) {
141
180
return null;
142
181
}
143
-
toFix.add(getCurrentPath());
182
+
longStringLiterals.add(getCurrentPath());
144
183
return null;
145
184
}
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());
154
228
}
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()));
171
264
}
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
+
}
173
280
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);
179
289
}
180
-
return replacements;
181
290
}
182
291
183
292
/**
@@ -364,13 +473,16 @@ private static int getStartPosition(Tree tree) {
364
473
return ((JCTree) tree).getStartPosition();
365
474
}
366
475
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) {
369
481
// TODO(cushon): consider adding Newlines.lineIterable?
370
482
Iterator<String> it = Newlines.lineIterator(input);
371
483
while (it.hasNext()) {
372
484
String line = it.next();
373
-
if (line.length() > columnLimit) {
485
+
if (line.length() > columnLimit || line.contains("\"\"\"")) {
374
486
return true;
375
487
}
376
488
}
@@ -385,7 +497,6 @@ private static JCTree.JCCompilationUnit parse(String source, boolean allowString
385
497
context.put(DiagnosticListener.class, diagnostics);
386
498
Options.instance(context).put("--enable-preview", "true");
387
499
Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding));
388
-
JCTree.JCCompilationUnit unit;
389
500
JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
390
501
try {
391
502
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
@@ -404,7 +515,7 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
404
515
JavacParser parser =
405
516
parserFactory.newParser(
406
517
source, /* keepDocComments= */ true, /* keepEndPos= */ true, /* keepLineMap= */ true);
407
-
unit = parser.parseCompilationUnit();
518
+
JCTree.JCCompilationUnit unit = parser.parseCompilationUnit();
408
519
unit.sourcefile = sjfo;
409
520
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
410
521
Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4