| /* |
| * Copyright 2015 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package com.google.googlejavaformat.java; |
| |
| import static java.util.Comparator.comparing; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.DiscreteDomain; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Range; |
| import com.google.common.collect.RangeSet; |
| import com.google.common.collect.TreeRangeSet; |
| import com.google.googlejavaformat.CommentsHelper; |
| import com.google.googlejavaformat.Input; |
| import com.google.googlejavaformat.Input.Token; |
| import com.google.googlejavaformat.Newlines; |
| import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; |
| import com.google.googlejavaformat.Output; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /* |
| * Throughout this file, {@code i} is an index for input lines, {@code j} is an index for output |
| * lines, {@code ij} is an index into either input or output lines, and {@code k} is an index for |
| * toks. |
| */ |
| |
| /** |
| * {@code JavaOutput} extends {@link Output Output} to represent a Java output document. It includes |
| * methods to emit the output document. |
| */ |
| public final class JavaOutput extends Output { |
| private final String lineSeparator; |
| private final Input javaInput; // Used to follow along while emitting the output. |
| private final CommentsHelper commentsHelper; // Used to re-flow comments. |
| private final Map<Integer, BlankLineWanted> blankLines = new HashMap<>(); // Info on blank lines. |
| private final RangeSet<Integer> partialFormatRanges = TreeRangeSet.create(); |
| |
| private final List<String> mutableLines = new ArrayList<>(); |
| private final int kN; // The number of tokens or comments in the input, excluding the EOF. |
| private int iLine = 0; // Closest corresponding line number on input. |
| private int lastK = -1; // Last {@link Tok} index output. |
| private int newlinesPending = 0; |
| private StringBuilder lineBuilder = new StringBuilder(); |
| private StringBuilder spacesPending = new StringBuilder(); |
| |
| /** |
| * {@code JavaOutput} constructor. |
| * |
| * @param javaInput the {@link Input}, used to match up blank lines in the output |
| * @param commentsHelper the {@link CommentsHelper}, used to rewrite comments |
| */ |
| public JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper) { |
| this.lineSeparator = lineSeparator; |
| this.javaInput = javaInput; |
| this.commentsHelper = commentsHelper; |
| kN = javaInput.getkN(); |
| } |
| |
| @Override |
| public void blankLine(int k, BlankLineWanted wanted) { |
| if (blankLines.containsKey(k)) { |
| blankLines.put(k, blankLines.get(k).merge(wanted)); |
| } else { |
| blankLines.put(k, wanted); |
| } |
| } |
| |
| @Override |
| public void markForPartialFormat(Token start, Token end) { |
| int lo = JavaOutput.startTok(start).getIndex(); |
| int hi = JavaOutput.endTok(end).getIndex(); |
| partialFormatRanges.add(Range.closed(lo, hi)); |
| } |
| |
| // TODO(jdd): Add invariant. |
| @Override |
| public void append(String text, Range<Integer> range) { |
| if (!range.isEmpty()) { |
| boolean sawNewlines = false; |
| // Skip over input line we've passed. |
| int iN = javaInput.getLineCount(); |
| while (iLine < iN |
| && (javaInput.getRanges(iLine).isEmpty() |
| || javaInput.getRanges(iLine).upperEndpoint() <= range.lowerEndpoint())) { |
| if (javaInput.getRanges(iLine).isEmpty()) { |
| // Skipped over a blank line. |
| sawNewlines = true; |
| } |
| ++iLine; |
| } |
| /* |
| * Output blank line if we've called {@link OpsBuilder#blankLine}{@code (true)} here, or if |
| * there's a blank line here and it's a comment. |
| */ |
| BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO); |
| if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) { |
| ++newlinesPending; |
| } |
| } |
| if (Newlines.isNewline(text)) { |
| /* |
| * Don't update range information, and swallow extra newlines. The case below for '\n' is for |
| * block comments. |
| */ |
| if (newlinesPending == 0) { |
| ++newlinesPending; |
| } |
| spacesPending = new StringBuilder(); |
| } else { |
| boolean rangesSet = false; |
| int textN = text.length(); |
| for (int i = 0; i < textN; i++) { |
| char c = text.charAt(i); |
| switch (c) { |
| case ' ': |
| spacesPending.append(' '); |
| break; |
| case '\t': |
| spacesPending.append('\t'); |
| break; |
| case '\r': |
| if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { |
| i++; |
| } |
| // falls through |
| case '\n': |
| spacesPending = new StringBuilder(); |
| ++newlinesPending; |
| break; |
| default: |
| while (newlinesPending > 0) { |
| // drop leading blank lines |
| if (!mutableLines.isEmpty() || lineBuilder.length() > 0) { |
| mutableLines.add(lineBuilder.toString()); |
| } |
| lineBuilder = new StringBuilder(); |
| rangesSet = false; |
| --newlinesPending; |
| } |
| if (spacesPending.length() > 0) { |
| lineBuilder.append(spacesPending); |
| spacesPending = new StringBuilder(); |
| } |
| lineBuilder.append(c); |
| if (!range.isEmpty()) { |
| if (!rangesSet) { |
| while (ranges.size() <= mutableLines.size()) { |
| ranges.add(Formatter.EMPTY_RANGE); |
| } |
| ranges.set(mutableLines.size(), union(ranges.get(mutableLines.size()), range)); |
| rangesSet = true; |
| } |
| } |
| } |
| } |
| } |
| if (!range.isEmpty()) { |
| lastK = range.upperEndpoint(); |
| } |
| } |
| |
| @Override |
| public void indent(int indent) { |
| spacesPending.append(Strings.repeat(" ", indent)); |
| } |
| |
| /** Flush any incomplete last line, then add the EOF token into our data structures. */ |
| public void flush() { |
| String lastLine = lineBuilder.toString(); |
| if (!CharMatcher.whitespace().matchesAllOf(lastLine)) { |
| mutableLines.add(lastLine); |
| } |
| int jN = mutableLines.size(); |
| Range<Integer> eofRange = Range.closedOpen(kN, kN + 1); |
| while (ranges.size() < jN) { |
| ranges.add(Formatter.EMPTY_RANGE); |
| } |
| ranges.add(eofRange); |
| setLines(ImmutableList.copyOf(mutableLines)); |
| } |
| |
| // The following methods can be used after the Output has been built. |
| |
| @Override |
| public CommentsHelper getCommentsHelper() { |
| return commentsHelper; |
| } |
| |
| /** |
| * Emit a list of {@link Replacement}s to convert from input to output. |
| * |
| * @return a list of {@link Replacement}s, sorted by start index, without overlaps |
| */ |
| public ImmutableList<Replacement> getFormatReplacements(RangeSet<Integer> iRangeSet0) { |
| ImmutableList.Builder<Replacement> result = ImmutableList.builder(); |
| Map<Integer, Range<Integer>> kToJ = JavaOutput.makeKToIJ(this); |
| |
| // Expand the token ranges to align with re-formattable boundaries. |
| RangeSet<Integer> breakableRanges = TreeRangeSet.create(); |
| RangeSet<Integer> iRangeSet = iRangeSet0.subRangeSet(Range.closed(0, javaInput.getkN())); |
| for (Range<Integer> iRange : iRangeSet.asRanges()) { |
| Range<Integer> range = expandToBreakableRegions(iRange.canonical(DiscreteDomain.integers())); |
| if (range.equals(EMPTY_RANGE)) { |
| // the range contains only whitespace |
| continue; |
| } |
| breakableRanges.add(range); |
| } |
| |
| // Construct replacements for each reformatted region. |
| for (Range<Integer> range : breakableRanges.asRanges()) { |
| |
| Input.Tok startTok = startTok(javaInput.getToken(range.lowerEndpoint())); |
| Input.Tok endTok = endTok(javaInput.getToken(range.upperEndpoint() - 1)); |
| |
| // Add all output lines in the given token range to the replacement. |
| StringBuilder replacement = new StringBuilder(); |
| |
| int replaceFrom = startTok.getPosition(); |
| // Replace leading whitespace in the input with the whitespace from the formatted file |
| while (replaceFrom > 0) { |
| char previous = javaInput.getText().charAt(replaceFrom - 1); |
| if (!CharMatcher.whitespace().matches(previous)) { |
| break; |
| } |
| replaceFrom--; |
| } |
| |
| int i = kToJ.get(startTok.getIndex()).lowerEndpoint(); |
| // Include leading blank lines from the formatted output, unless the formatted range |
| // starts at the beginning of the file. |
| while (i > 0 && getLine(i - 1).isEmpty()) { |
| i--; |
| } |
| // Write out the formatted range. |
| for (; i < kToJ.get(endTok.getIndex()).upperEndpoint(); i++) { |
| // It's possible to run out of output lines (e.g. if the input ended with |
| // multiple trailing newlines). |
| if (i < getLineCount()) { |
| if (i > 0) { |
| replacement.append(lineSeparator); |
| } |
| replacement.append(getLine(i)); |
| } |
| } |
| |
| int replaceTo = |
| Math.min(endTok.getPosition() + endTok.length(), javaInput.getText().length()); |
| // If the formatted ranged ended in the trailing trivia of the last token before EOF, |
| // format all the way up to EOF to deal with trailing whitespace correctly. |
| if (endTok.getIndex() == javaInput.getkN() - 1) { |
| replaceTo = javaInput.getText().length(); |
| } |
| // Replace trailing whitespace in the input with the whitespace from the formatted file. |
| // If the trailing whitespace in the input includes one or more line breaks, preserve the |
| // whitespace after the last newline to avoid re-indenting the line following the formatted |
| // line. |
| int newline = -1; |
| while (replaceTo < javaInput.getText().length()) { |
| char next = javaInput.getText().charAt(replaceTo); |
| if (!CharMatcher.whitespace().matches(next)) { |
| break; |
| } |
| int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo); |
| if (newlineLength != -1) { |
| newline = replaceTo; |
| // Skip over the entire newline; don't count the second character of \r\n as a newline. |
| replaceTo += newlineLength; |
| } else { |
| replaceTo++; |
| } |
| } |
| if (newline != -1) { |
| replaceTo = newline; |
| } |
| |
| if (newline == -1) { |
| // There wasn't an existing trailing newline; add one. |
| replacement.append(lineSeparator); |
| } |
| for (; i < getLineCount(); i++) { |
| String after = getLine(i); |
| int idx = CharMatcher.whitespace().negate().indexIn(after); |
| if (idx == -1) { |
| // Write out trailing empty lines from the formatted output. |
| replacement.append(lineSeparator); |
| } else { |
| if (newline == -1) { |
| // If there wasn't a trailing newline in the input, indent the next line. |
| replacement.append(after.substring(0, idx)); |
| } |
| break; |
| } |
| } |
| |
| result.add(Replacement.create(replaceFrom, replaceTo, replacement.toString())); |
| } |
| return result.build(); |
| } |
| |
| /** |
| * Expand a token range to start and end on acceptable boundaries for re-formatting. |
| * |
| * @param iRange the {@link Range} of tokens |
| * @return the expanded token range |
| */ |
| private Range<Integer> expandToBreakableRegions(Range<Integer> iRange) { |
| // The original line range. |
| int loTok = iRange.lowerEndpoint(); |
| int hiTok = iRange.upperEndpoint() - 1; |
| |
| // Expand the token indices to formattable boundaries (e.g. edges of statements). |
| if (!partialFormatRanges.contains(loTok) || !partialFormatRanges.contains(hiTok)) { |
| return EMPTY_RANGE; |
| } |
| loTok = partialFormatRanges.rangeContaining(loTok).lowerEndpoint(); |
| hiTok = partialFormatRanges.rangeContaining(hiTok).upperEndpoint(); |
| return Range.closedOpen(loTok, hiTok + 1); |
| } |
| |
| public static String applyReplacements(String input, List<Replacement> replacements) { |
| replacements = new ArrayList<>(replacements); |
| replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed()); |
| StringBuilder writer = new StringBuilder(input); |
| for (Replacement replacement : replacements) { |
| writer.replace( |
| replacement.getReplaceRange().lowerEndpoint(), |
| replacement.getReplaceRange().upperEndpoint(), |
| replacement.getReplacementString()); |
| } |
| return writer.toString(); |
| } |
| |
| /** The earliest position of any Tok in the Token, including leading whitespace. */ |
| public static int startPosition(Token token) { |
| int min = token.getTok().getPosition(); |
| for (Input.Tok tok : token.getToksBefore()) { |
| min = Math.min(min, tok.getPosition()); |
| } |
| return min; |
| } |
| |
| /** The earliest non-whitespace Tok in the Token. */ |
| public static Input.Tok startTok(Token token) { |
| for (Input.Tok tok : token.getToksBefore()) { |
| if (tok.getIndex() >= 0) { |
| return tok; |
| } |
| } |
| return token.getTok(); |
| } |
| |
| /** The last non-whitespace Tok in the Token. */ |
| public static Input.Tok endTok(Token token) { |
| for (int i = token.getToksAfter().size() - 1; i >= 0; i--) { |
| Input.Tok tok = token.getToksAfter().get(i); |
| if (tok.getIndex() >= 0) { |
| return tok; |
| } |
| } |
| return token.getTok(); |
| } |
| |
| private boolean isComment(String text) { |
| return text.startsWith("//") || text.startsWith("/*"); |
| } |
| |
| private static Range<Integer> union(Range<Integer> x, Range<Integer> y) { |
| return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers()); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("iLine", iLine) |
| .add("lastK", lastK) |
| .add("spacesPending", spacesPending.toString().replace("\t", "\\t")) |
| .add("newlinesPending", newlinesPending) |
| .add("blankLines", blankLines) |
| .add("super", super.toString()) |
| .toString(); |
| } |
| } |