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