1414
1515package com .google .googlejavaformat .java ;
1616
17+ import static com .google .common .collect .ImmutableList .toImmutableList ;
1718import static com .google .common .collect .Iterables .getLast ;
1819import static java .lang .Math .min ;
1920import static java .nio .charset .StandardCharsets .UTF_8 ;
4445import com .sun .tools .javac .util .Position ;
4546import java .io .IOException ;
4647import java .io .UncheckedIOException ;
48+ import java .lang .reflect .Method ;
4749import java .net .URI ;
4850import java .util .ArrayDeque ;
4951import java .util .ArrayList ;
5961import javax .tools .JavaFileObject ;
6062import javax .tools .SimpleJavaFileObject ;
6163import javax .tools .StandardLocation ;
64+ import org .checkerframework .checker .nullness .qual .Nullable ;
6265
6366/** Wraps string literals that exceed the column limit. */
6467public 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 );
0 commit comments