blob: e663c9603c121279888d502b262f119005d2d538 [file] [log] [blame]
/*
* 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;
import static com.google.common.collect.Iterables.getLast;
import com.google.common.base.MoreObjects;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.Iterators;
import com.google.common.collect.Range;
import com.google.googlejavaformat.Output.BreakTag;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* {@link com.google.googlejavaformat.java.JavaInputAstVisitor JavaInputAstVisitor} outputs a
* sequence of {@link Op}s using {@link OpsBuilder}. This linear sequence is then transformed by
* {@link DocBuilder} into a tree-structured {@code Doc}. The top-level {@code Doc} is a {@link
* Level}, which contains a sequence of {@code Doc}s, including other {@link Level}s. Leaf {@code
* Doc}s are {@link Token}s, representing language-level tokens; {@link Tok}s, which may also
* represent non-token {@link Input.Tok}s, including comments and other white-space; {@link Space}s,
* representing single spaces; and {@link Break}s, which represent optional line-breaks.
*/
public abstract class Doc {
/**
* Each {@link Break} in a {@link Level} is either {@link FillMode#UNIFIED} or {@link
* FillMode#INDEPENDENT}.
*/
public enum FillMode {
/**
* If a {@link Level} will not fit on one line, all of its {@code UNIFIED} {@link Break}s will
* be broken.
*/
UNIFIED,
/**
* If a {@link Level} will not fit on one line, its {@code INDEPENDENT} {@link Break}s will be
* broken independently of each other, to fill in the {@link Level}.
*/
INDEPENDENT,
/**
* A {@code FORCED} {@link Break} will always be broken, and a {@link Level} it appears in will
* not fit on one line.
*/
FORCED
}
/** State for writing. */
public static final class State {
final int lastIndent;
final int indent;
final int column;
final boolean mustBreak;
State(int lastIndent, int indent, int column, boolean mustBreak) {
this.lastIndent = lastIndent;
this.indent = indent;
this.column = column;
this.mustBreak = mustBreak;
}
public State(int indent0, int column0) {
this(indent0, indent0, column0, false);
}
State withColumn(int column) {
return new State(lastIndent, indent, column, mustBreak);
}
State withMustBreak(boolean mustBreak) {
return new State(lastIndent, indent, column, mustBreak);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("lastIndent", lastIndent)
.add("indent", indent)
.add("column", column)
.add("mustBreak", mustBreak)
.toString();
}
}
private static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
// Memoized width; Float.POSITIVE_INFINITY if contains forced breaks.
private boolean widthComputed = false;
private float width = 0.0F;
// Memoized flat; not defined (and never computed) if contains forced breaks.
private boolean flatComputed = false;
private String flat = "";
// Memoized Range.
private boolean rangeComputed = false;
private Range<Integer> range = EMPTY_RANGE;
/**
* Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken.
*
* @return the width
*/
final float getWidth() {
if (!widthComputed) {
width = computeWidth();
widthComputed = true;
}
return width;
}
/**
* Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc}
* contains forced breaks.
*
* @return the flat-string value
*/
final String getFlat() {
if (!flatComputed) {
flat = computeFlat();
flatComputed = true;
}
return flat;
}
/**
* Return the {@link Range} of a {@code Doc}.
*
* @return the {@code Doc}'s {@link Range}
*/
final Range<Integer> range() {
if (!rangeComputed) {
range = computeRange();
rangeComputed = true;
}
return range;
}
/**
* Compute the {@code Doc}'s width.
*
* @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken
*/
abstract float computeWidth();
/**
* Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks.
*
* @return the flat value
*/
abstract String computeFlat();
/**
* Compute the {@code Doc}'s {@link Range} of {@link Input.Token}s.
*
* @return the {@link Range}
*/
abstract Range<Integer> computeRange();
/**
* Make breaking decisions for a {@code Doc}.
*
* @param maxWidth the maximum line width
* @param state the current output state
* @return the new output state
*/
public abstract State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state);
/** Write a {@code Doc} to an {@link Output}, after breaking decisions have been made. */
public abstract void write(Output output);
/** A {@code Level} inside a {@link Doc}. */
static final class Level extends Doc {
private final Indent plusIndent; // The extra indent following breaks.
private final List<Doc> docs = new ArrayList<>(); // The elements of the level.
private Level(Indent plusIndent) {
this.plusIndent = plusIndent;
}
/**
* Factory method for {@code Level}s.
*
* @param plusIndent the extra indent inside the {@code Level}
* @return the new {@code Level}
*/
static Level make(Indent plusIndent) {
return new Level(plusIndent);
}
/**
* Add a {@link Doc} to the {@code Level}.
*
* @param doc the {@link Doc} to add
*/
void add(Doc doc) {
docs.add(doc);
}
@Override
float computeWidth() {
float thisWidth = 0.0F;
for (Doc doc : docs) {
thisWidth += doc.getWidth();
}
return thisWidth;
}
@Override
String computeFlat() {
StringBuilder builder = new StringBuilder();
for (Doc doc : docs) {
builder.append(doc.getFlat());
}
return builder.toString();
}
@Override
Range<Integer> computeRange() {
Range<Integer> docRange = EMPTY_RANGE;
for (Doc doc : docs) {
docRange = union(docRange, doc.range());
}
return docRange;
}
// State that needs to be preserved between calculating breaks and
// writing output.
// TODO(cushon): represent phases as separate immutable data.
/** True if the entire {@link Level} fits on one line. */
boolean oneLine = false;
/**
* Groups of {@link Doc}s that are children of the current {@link Level}, separated by {@link
* Break}s.
*/
List<List<Doc>> splits = new ArrayList<>();
/** {@link Break}s between {@link Doc}s in the current {@link Level}. */
List<Break> breaks = new ArrayList<>();
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
float thisWidth = getWidth();
if (state.column + thisWidth <= maxWidth) {
oneLine = true;
return state.withColumn(state.column + (int) thisWidth);
}
State broken =
computeBroken(
commentsHelper, maxWidth, new State(state.indent + plusIndent.eval(), state.column));
return state.withColumn(broken.column);
}
private static void splitByBreaks(List<Doc> docs, List<List<Doc>> splits, List<Break> breaks) {
splits.clear();
breaks.clear();
splits.add(new ArrayList<>());
for (Doc doc : docs) {
if (doc instanceof Break) {
breaks.add((Break) doc);
splits.add(new ArrayList<>());
} else {
getLast(splits).add(doc);
}
}
}
/** Compute breaks for a {@link Level} that spans multiple lines. */
private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State state) {
splitByBreaks(docs, splits, breaks);
state =
computeBreakAndSplit(
commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.empty(), splits.get(0));
// Handle following breaks and split.
for (int i = 0; i < breaks.size(); i++) {
state =
computeBreakAndSplit(
commentsHelper, maxWidth, state, Optional.of(breaks.get(i)), splits.get(i + 1));
}
return state;
}
/** Lay out a Break-separated group of Docs in the current Level. */
private static State computeBreakAndSplit(
CommentsHelper commentsHelper,
int maxWidth,
State state,
Optional<Break> optBreakDoc,
List<Doc> split) {
float breakWidth = optBreakDoc.isPresent() ? optBreakDoc.get().getWidth() : 0.0F;
float splitWidth = getWidth(split);
boolean shouldBreak =
(optBreakDoc.isPresent() && optBreakDoc.get().fillMode == FillMode.UNIFIED)
|| state.mustBreak
|| state.column + breakWidth + splitWidth > maxWidth;
if (optBreakDoc.isPresent()) {
state = optBreakDoc.get().computeBreaks(state, state.lastIndent, shouldBreak);
}
boolean enoughRoom = state.column + splitWidth <= maxWidth;
state = computeSplit(commentsHelper, maxWidth, split, state.withMustBreak(false));
if (!enoughRoom) {
state = state.withMustBreak(true); // Break after, too.
}
return state;
}
private static State computeSplit(
CommentsHelper commentsHelper, int maxWidth, List<Doc> docs, State state) {
for (Doc doc : docs) {
state = doc.computeBreaks(commentsHelper, maxWidth, state);
}
return state;
}
@Override
public void write(Output output) {
if (oneLine) {
output.append(getFlat(), range()); // This is defined because width is finite.
} else {
writeFilled(output);
}
}
private void writeFilled(Output output) {
// Handle first split.
for (Doc doc : splits.get(0)) {
doc.write(output);
}
// Handle following breaks and split.
for (int i = 0; i < breaks.size(); i++) {
breaks.get(i).write(output);
for (Doc doc : splits.get(i + 1)) {
doc.write(output);
}
}
}
/**
* Get the width of a sequence of {@link Doc}s.
*
* @param docs the {@link Doc}s
* @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken
*/
static float getWidth(List<Doc> docs) {
float width = 0.0F;
for (Doc doc : docs) {
width += doc.getWidth();
}
return width;
}
private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(INTEGERS);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("plusIndent", plusIndent)
.add("docs", docs)
.toString();
}
}
/** A leaf {@link Doc} for a token. */
public static final class Token extends Doc implements Op {
/** Is a Token a real token, or imaginary (e.g., a token generated incorrectly, or an EOF)? */
public enum RealOrImaginary {
REAL,
IMAGINARY;
boolean isReal() {
return this == REAL;
}
}
private final Input.Token token;
private final RealOrImaginary realOrImaginary;
private final Indent plusIndentCommentsBefore;
private final Optional<Indent> breakAndIndentTrailingComment;
private Token(
Input.Token token,
RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
this.token = token;
this.realOrImaginary = realOrImaginary;
this.plusIndentCommentsBefore = plusIndentCommentsBefore;
this.breakAndIndentTrailingComment = breakAndIndentTrailingComment;
}
/**
* How much extra to indent comments before the {@code Token}.
*
* @return the extra indent
*/
Indent getPlusIndentCommentsBefore() {
return plusIndentCommentsBefore;
}
/** Force a line break and indent trailing javadoc or block comments. */
Optional<Indent> breakAndIndentTrailingComment() {
return breakAndIndentTrailingComment;
}
/**
* Make a {@code Token}.
*
* @param token the {@link Input.Token} to wrap
* @param realOrImaginary did this {@link Input.Token} appear in the input, or was it generated
* incorrectly?
* @param plusIndentCommentsBefore extra {@code plusIndent} for comments just before this token
* @return the new {@code Token}
*/
static Op make(
Input.Token token,
Doc.Token.RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
return new Token(
token, realOrImaginary, plusIndentCommentsBefore, breakAndIndentTrailingComment);
}
/**
* Return the wrapped {@link Input.Token}.
*
* @return the {@link Input.Token}
*/
Input.Token getToken() {
return token;
}
/**
* Is the token good? That is, does it match an {@link Input.Token}?
*
* @return whether the @code Token} is good
*/
RealOrImaginary realOrImaginary() {
return realOrImaginary;
}
@Override
public void add(DocBuilder builder) {
builder.add(this);
}
@Override
float computeWidth() {
return token.getTok().length();
}
@Override
String computeFlat() {
return token.getTok().getOriginalText();
}
@Override
Range<Integer> computeRange() {
return Range.singleton(token.getTok().getIndex()).canonical(INTEGERS);
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
String text = token.getTok().getOriginalText();
return state.withColumn(state.column + text.length());
}
@Override
public void write(Output output) {
String text = token.getTok().getOriginalText();
output.append(text, range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("token", token)
.add("realOrImaginary", realOrImaginary)
.add("plusIndentCommentsBefore", plusIndentCommentsBefore)
.toString();
}
}
/** A Leaf node in a {@link Doc} for a non-breaking space. */
static final class Space extends Doc implements Op {
private static final Space SPACE = new Space();
private Space() {}
/**
* Factor method for {@code Space}.
*
* @return the new {@code Space}
*/
static Space make() {
return SPACE;
}
@Override
public void add(DocBuilder builder) {
builder.add(this);
}
@Override
float computeWidth() {
return 1.0F;
}
@Override
String computeFlat() {
return " ";
}
@Override
Range<Integer> computeRange() {
return EMPTY_RANGE;
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
return state.withColumn(state.column + 1);
}
@Override
public void write(Output output) {
output.append(" ", range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).toString();
}
}
/** A leaf node in a {@link Doc} for an optional break. */
public static final class Break extends Doc implements Op {
private final FillMode fillMode;
private final String flat;
private final Indent plusIndent;
private final Optional<BreakTag> optTag;
private Break(FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
this.fillMode = fillMode;
this.flat = flat;
this.plusIndent = plusIndent;
this.optTag = optTag;
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @return the new {@code Break}
*/
public static Break make(FillMode fillMode, String flat, Indent plusIndent) {
return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty());
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @param optTag an optional tag for remembering whether the break was taken
* @return the new {@code Break}
*/
public static Break make(
FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
return new Break(fillMode, flat, plusIndent, optTag);
}
/**
* Make a forced {@code Break}.
*
* @return the new forced {@code Break}
*/
public static Break makeForced() {
return make(FillMode.FORCED, "", Indent.Const.ZERO);
}
/**
* Return the {@code Break}'s extra indent.
*
* @return the extra indent
*/
int getPlusIndent() {
return plusIndent.eval();
}
/**
* Is the {@code Break} forced?
*
* @return whether the {@code Break} is forced
*/
boolean isForced() {
return fillMode == FillMode.FORCED;
}
@Override
public void add(DocBuilder builder) {
builder.breakDoc(this);
}
@Override
float computeWidth() {
return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length();
}
@Override
String computeFlat() {
return flat;
}
@Override
Range<Integer> computeRange() {
return EMPTY_RANGE;
}
/** Was this break taken? */
boolean broken;
/** New indent after this break. */
int newIndent;
public State computeBreaks(State state, int lastIndent, boolean broken) {
if (optTag.isPresent()) {
optTag.get().recordBroken(broken);
}
if (broken) {
this.broken = true;
this.newIndent = Math.max(lastIndent + plusIndent.eval(), 0);
return state.withColumn(newIndent);
} else {
this.broken = false;
this.newIndent = -1;
return state.withColumn(state.column + flat.length());
}
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
// Updating the state for {@link Break}s requires deciding if the break
// should be taken.
// TODO(cushon): this hierarchy is wrong, create a separate interface
// for unbreakable Docs?
throw new UnsupportedOperationException("Did you mean computeBreaks(State, int, boolean)?");
}
@Override
public void write(Output output) {
if (broken) {
output.append("\n", EMPTY_RANGE);
output.indent(newIndent);
} else {
output.append(flat, range());
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("fillMode", fillMode)
.add("flat", flat)
.add("plusIndent", plusIndent)
.add("optTag", optTag)
.toString();
}
}
/** A leaf node in a {@link Doc} for a non-token. */
static final class Tok extends Doc implements Op {
private final Input.Tok tok;
private Tok(Input.Tok tok) {
this.tok = tok;
}
/**
* Factory method for a {@code Tok}.
*
* @param tok the {@link Input.Tok} to wrap
* @return the new {@code Tok}
*/
static Tok make(Input.Tok tok) {
return new Tok(tok);
}
@Override
public void add(DocBuilder builder) {
builder.add(this);
}
@Override
float computeWidth() {
int idx = Newlines.firstBreak(tok.getOriginalText());
// only count the first line of multi-line block comments
if (tok.isComment()) {
if (idx > 0) {
return idx;
} else if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
// Account for line comments with missing spaces, see computeFlat.
return tok.length() + 1;
} else {
return tok.length();
}
}
return idx != -1 ? Float.POSITIVE_INFINITY : (float) tok.length();
}
@Override
String computeFlat() {
// TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a single
// line. That's fine for multi-line comment reflowing, but problematic for adding missing
// spaces in line comments.
if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
return "// " + tok.getOriginalText().substring("//".length());
}
return tok.getOriginalText();
}
@Override
Range<Integer> computeRange() {
return Range.singleton(tok.getIndex()).canonical(INTEGERS);
}
String text;
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
text = commentsHelper.rewrite(tok, maxWidth, state.column);
int firstLineLength = text.length() - Iterators.getLast(Newlines.lineOffsetIterator(text));
return state.withColumn(state.column + firstLineLength);
}
@Override
public void write(Output output) {
output.append(text, range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("tok", tok).toString();
}
}
}