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 );
0 commit comments