• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.google.googlejavaformat.java;
16 
17 import static java.util.Comparator.comparing;
18 
19 import com.google.common.base.CharMatcher;
20 import com.google.common.base.MoreObjects;
21 import com.google.common.base.Strings;
22 import com.google.common.collect.DiscreteDomain;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.Range;
25 import com.google.common.collect.RangeSet;
26 import com.google.common.collect.TreeRangeSet;
27 import com.google.googlejavaformat.CommentsHelper;
28 import com.google.googlejavaformat.Input;
29 import com.google.googlejavaformat.Input.Token;
30 import com.google.googlejavaformat.Newlines;
31 import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
32 import com.google.googlejavaformat.Output;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 
38 /*
39  * Throughout this file, {@code i} is an index for input lines, {@code j} is an index for output
40  * lines, {@code ij} is an index into either input or output lines, and {@code k} is an index for
41  * toks.
42  */
43 
44 /**
45  * {@code JavaOutput} extends {@link Output Output} to represent a Java output document. It includes
46  * methods to emit the output document.
47  */
48 public final class JavaOutput extends Output {
49   private final String lineSeparator;
50   private final Input javaInput; // Used to follow along while emitting the output.
51   private final CommentsHelper commentsHelper; // Used to re-flow comments.
52   private final Map<Integer, BlankLineWanted> blankLines = new HashMap<>(); // Info on blank lines.
53   private final RangeSet<Integer> partialFormatRanges = TreeRangeSet.create();
54 
55   private final List<String> mutableLines = new ArrayList<>();
56   private final int kN; // The number of tokens or comments in the input, excluding the EOF.
57   private int iLine = 0; // Closest corresponding line number on input.
58   private int lastK = -1; // Last {@link Tok} index output.
59   private int newlinesPending = 0;
60   private StringBuilder lineBuilder = new StringBuilder();
61   private StringBuilder spacesPending = new StringBuilder();
62 
63   /**
64    * {@code JavaOutput} constructor.
65    *
66    * @param javaInput the {@link Input}, used to match up blank lines in the output
67    * @param commentsHelper the {@link CommentsHelper}, used to rewrite comments
68    */
JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper)69   public JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper) {
70     this.lineSeparator = lineSeparator;
71     this.javaInput = javaInput;
72     this.commentsHelper = commentsHelper;
73     kN = javaInput.getkN();
74   }
75 
76   @Override
blankLine(int k, BlankLineWanted wanted)77   public void blankLine(int k, BlankLineWanted wanted) {
78     if (blankLines.containsKey(k)) {
79       blankLines.put(k, blankLines.get(k).merge(wanted));
80     } else {
81       blankLines.put(k, wanted);
82     }
83   }
84 
85   @Override
markForPartialFormat(Token start, Token end)86   public void markForPartialFormat(Token start, Token end) {
87     int lo = JavaOutput.startTok(start).getIndex();
88     int hi = JavaOutput.endTok(end).getIndex();
89     partialFormatRanges.add(Range.closed(lo, hi));
90   }
91 
92   // TODO(jdd): Add invariant.
93   @Override
append(String text, Range<Integer> range)94   public void append(String text, Range<Integer> range) {
95     if (!range.isEmpty()) {
96       boolean sawNewlines = false;
97       // Skip over input line we've passed.
98       int iN = javaInput.getLineCount();
99       while (iLine < iN
100           && (javaInput.getRanges(iLine).isEmpty()
101               || javaInput.getRanges(iLine).upperEndpoint() <= range.lowerEndpoint())) {
102         if (javaInput.getRanges(iLine).isEmpty()) {
103           // Skipped over a blank line.
104           sawNewlines = true;
105         }
106         ++iLine;
107       }
108       /*
109        * Output blank line if we've called {@link OpsBuilder#blankLine}{@code (true)} here, or if
110        * there's a blank line here and it's a comment.
111        */
112       BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO);
113       if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) {
114         ++newlinesPending;
115       }
116     }
117     if (Newlines.isNewline(text)) {
118       /*
119        * Don't update range information, and swallow extra newlines. The case below for '\n' is for
120        * block comments.
121        */
122       if (newlinesPending == 0) {
123         ++newlinesPending;
124       }
125       spacesPending = new StringBuilder();
126     } else {
127       boolean rangesSet = false;
128       int textN = text.length();
129       for (int i = 0; i < textN; i++) {
130         char c = text.charAt(i);
131         switch (c) {
132           case ' ':
133             spacesPending.append(' ');
134             break;
135           case '\t':
136             spacesPending.append('\t');
137             break;
138           case '\r':
139             if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
140               i++;
141             }
142             // falls through
143           case '\n':
144             spacesPending = new StringBuilder();
145             ++newlinesPending;
146             break;
147           default:
148             while (newlinesPending > 0) {
149               // drop leading blank lines
150               if (!mutableLines.isEmpty() || lineBuilder.length() > 0) {
151                 mutableLines.add(lineBuilder.toString());
152               }
153               lineBuilder = new StringBuilder();
154               rangesSet = false;
155               --newlinesPending;
156             }
157             if (spacesPending.length() > 0) {
158               lineBuilder.append(spacesPending);
159               spacesPending = new StringBuilder();
160             }
161             lineBuilder.append(c);
162             if (!range.isEmpty()) {
163               if (!rangesSet) {
164                 while (ranges.size() <= mutableLines.size()) {
165                   ranges.add(Formatter.EMPTY_RANGE);
166                 }
167                 ranges.set(mutableLines.size(), union(ranges.get(mutableLines.size()), range));
168                 rangesSet = true;
169               }
170             }
171         }
172       }
173     }
174     if (!range.isEmpty()) {
175       lastK = range.upperEndpoint();
176     }
177   }
178 
179   @Override
indent(int indent)180   public void indent(int indent) {
181     spacesPending.append(Strings.repeat(" ", indent));
182   }
183 
184   /** Flush any incomplete last line, then add the EOF token into our data structures. */
flush()185   public void flush() {
186     String lastLine = lineBuilder.toString();
187     if (!CharMatcher.whitespace().matchesAllOf(lastLine)) {
188       mutableLines.add(lastLine);
189     }
190     int jN = mutableLines.size();
191     Range<Integer> eofRange = Range.closedOpen(kN, kN + 1);
192     while (ranges.size() < jN) {
193       ranges.add(Formatter.EMPTY_RANGE);
194     }
195     ranges.add(eofRange);
196     setLines(ImmutableList.copyOf(mutableLines));
197   }
198 
199   // The following methods can be used after the Output has been built.
200 
201   @Override
getCommentsHelper()202   public CommentsHelper getCommentsHelper() {
203     return commentsHelper;
204   }
205 
206   /**
207    * Emit a list of {@link Replacement}s to convert from input to output.
208    *
209    * @return a list of {@link Replacement}s, sorted by start index, without overlaps
210    */
getFormatReplacements(RangeSet<Integer> iRangeSet0)211   public ImmutableList<Replacement> getFormatReplacements(RangeSet<Integer> iRangeSet0) {
212     ImmutableList.Builder<Replacement> result = ImmutableList.builder();
213     Map<Integer, Range<Integer>> kToJ = JavaOutput.makeKToIJ(this);
214 
215     // Expand the token ranges to align with re-formattable boundaries.
216     RangeSet<Integer> breakableRanges = TreeRangeSet.create();
217     RangeSet<Integer> iRangeSet = iRangeSet0.subRangeSet(Range.closed(0, javaInput.getkN()));
218     for (Range<Integer> iRange : iRangeSet.asRanges()) {
219       Range<Integer> range = expandToBreakableRegions(iRange.canonical(DiscreteDomain.integers()));
220       if (range.equals(EMPTY_RANGE)) {
221         // the range contains only whitespace
222         continue;
223       }
224       breakableRanges.add(range);
225     }
226 
227     // Construct replacements for each reformatted region.
228     for (Range<Integer> range : breakableRanges.asRanges()) {
229 
230       Input.Tok startTok = startTok(javaInput.getToken(range.lowerEndpoint()));
231       Input.Tok endTok = endTok(javaInput.getToken(range.upperEndpoint() - 1));
232 
233       // Add all output lines in the given token range to the replacement.
234       StringBuilder replacement = new StringBuilder();
235 
236       int replaceFrom = startTok.getPosition();
237       // Replace leading whitespace in the input with the whitespace from the formatted file
238       while (replaceFrom > 0) {
239         char previous = javaInput.getText().charAt(replaceFrom - 1);
240         if (!CharMatcher.whitespace().matches(previous)) {
241           break;
242         }
243         replaceFrom--;
244       }
245 
246       int i = kToJ.get(startTok.getIndex()).lowerEndpoint();
247       // Include leading blank lines from the formatted output, unless the formatted range
248       // starts at the beginning of the file.
249       while (i > 0 && getLine(i - 1).isEmpty()) {
250         i--;
251       }
252       // Write out the formatted range.
253       for (; i < kToJ.get(endTok.getIndex()).upperEndpoint(); i++) {
254         // It's possible to run out of output lines (e.g. if the input ended with
255         // multiple trailing newlines).
256         if (i < getLineCount()) {
257           if (i > 0) {
258             replacement.append(lineSeparator);
259           }
260           replacement.append(getLine(i));
261         }
262       }
263 
264       int replaceTo =
265           Math.min(endTok.getPosition() + endTok.length(), javaInput.getText().length());
266       // If the formatted ranged ended in the trailing trivia of the last token before EOF,
267       // format all the way up to EOF to deal with trailing whitespace correctly.
268       if (endTok.getIndex() == javaInput.getkN() - 1) {
269         replaceTo = javaInput.getText().length();
270       }
271       // Replace trailing whitespace in the input with the whitespace from the formatted file.
272       // If the trailing whitespace in the input includes one or more line breaks, preserve the
273       // whitespace after the last newline to avoid re-indenting the line following the formatted
274       // line.
275       int newline = -1;
276       while (replaceTo < javaInput.getText().length()) {
277         char next = javaInput.getText().charAt(replaceTo);
278         if (!CharMatcher.whitespace().matches(next)) {
279           break;
280         }
281         int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo);
282         if (newlineLength != -1) {
283           newline = replaceTo;
284           // Skip over the entire newline; don't count the second character of \r\n as a newline.
285           replaceTo += newlineLength;
286         } else {
287           replaceTo++;
288         }
289       }
290       if (newline != -1) {
291         replaceTo = newline;
292       }
293 
294       if (newline == -1) {
295         // There wasn't an existing trailing newline; add one.
296         replacement.append(lineSeparator);
297       }
298       for (; i < getLineCount(); i++) {
299         String after = getLine(i);
300         int idx = CharMatcher.whitespace().negate().indexIn(after);
301         if (idx == -1) {
302           // Write out trailing empty lines from the formatted output.
303           replacement.append(lineSeparator);
304         } else {
305           if (newline == -1) {
306             // If there wasn't a trailing newline in the input, indent the next line.
307             replacement.append(after.substring(0, idx));
308           }
309           break;
310         }
311       }
312 
313       result.add(Replacement.create(replaceFrom, replaceTo, replacement.toString()));
314     }
315     return result.build();
316   }
317 
318   /**
319    * Expand a token range to start and end on acceptable boundaries for re-formatting.
320    *
321    * @param iRange the {@link Range} of tokens
322    * @return the expanded token range
323    */
expandToBreakableRegions(Range<Integer> iRange)324   private Range<Integer> expandToBreakableRegions(Range<Integer> iRange) {
325     // The original line range.
326     int loTok = iRange.lowerEndpoint();
327     int hiTok = iRange.upperEndpoint() - 1;
328 
329     // Expand the token indices to formattable boundaries (e.g. edges of statements).
330     if (!partialFormatRanges.contains(loTok) || !partialFormatRanges.contains(hiTok)) {
331       return EMPTY_RANGE;
332     }
333     loTok = partialFormatRanges.rangeContaining(loTok).lowerEndpoint();
334     hiTok = partialFormatRanges.rangeContaining(hiTok).upperEndpoint();
335     return Range.closedOpen(loTok, hiTok + 1);
336   }
337 
applyReplacements(String input, List<Replacement> replacements)338   public static String applyReplacements(String input, List<Replacement> replacements) {
339     replacements = new ArrayList<>(replacements);
340     replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed());
341     StringBuilder writer = new StringBuilder(input);
342     for (Replacement replacement : replacements) {
343       writer.replace(
344           replacement.getReplaceRange().lowerEndpoint(),
345           replacement.getReplaceRange().upperEndpoint(),
346           replacement.getReplacementString());
347     }
348     return writer.toString();
349   }
350 
351   /** The earliest position of any Tok in the Token, including leading whitespace. */
startPosition(Token token)352   public static int startPosition(Token token) {
353     int min = token.getTok().getPosition();
354     for (Input.Tok tok : token.getToksBefore()) {
355       min = Math.min(min, tok.getPosition());
356     }
357     return min;
358   }
359 
360   /** The earliest non-whitespace Tok in the Token. */
startTok(Token token)361   public static Input.Tok startTok(Token token) {
362     for (Input.Tok tok : token.getToksBefore()) {
363       if (tok.getIndex() >= 0) {
364         return tok;
365       }
366     }
367     return token.getTok();
368   }
369 
370   /** The last non-whitespace Tok in the Token. */
endTok(Token token)371   public static Input.Tok endTok(Token token) {
372     for (int i = token.getToksAfter().size() - 1; i >= 0; i--) {
373       Input.Tok tok = token.getToksAfter().get(i);
374       if (tok.getIndex() >= 0) {
375         return tok;
376       }
377     }
378     return token.getTok();
379   }
380 
isComment(String text)381   private boolean isComment(String text) {
382     return text.startsWith("//") || text.startsWith("/*");
383   }
384 
union(Range<Integer> x, Range<Integer> y)385   private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
386     return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers());
387   }
388 
389   @Override
toString()390   public String toString() {
391     return MoreObjects.toStringHelper(this)
392         .add("iLine", iLine)
393         .add("lastK", lastK)
394         .add("spacesPending", spacesPending.toString().replace("\t", "\\t"))
395         .add("newlinesPending", newlinesPending)
396         .add("blankLines", blankLines)
397         .add("super", super.toString())
398         .toString();
399   }
400 }
401