com.palantir.javaformat.java.JavaInputAstVisitor Maven / Gradle / Ivy
Show all versions of palantir-java-format Show documentation
/*
* 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.palantir.javaformat.java;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.palantir.javaformat.Indent.If.make;
import static com.palantir.javaformat.OpsBuilder.BlankLineWanted.PRESERVE;
import static com.palantir.javaformat.OpsBuilder.BlankLineWanted.YES;
import static com.palantir.javaformat.doc.FillMode.INDEPENDENT;
import static com.palantir.javaformat.doc.FillMode.UNIFIED;
import static com.palantir.javaformat.java.Trees.getEndPosition;
import static com.palantir.javaformat.java.Trees.getLength;
import static com.palantir.javaformat.java.Trees.getMethodName;
import static com.palantir.javaformat.java.Trees.getSourceForNode;
import static com.palantir.javaformat.java.Trees.getStartPosition;
import static com.palantir.javaformat.java.Trees.operatorName;
import static com.palantir.javaformat.java.Trees.precedence;
import static com.palantir.javaformat.java.Trees.skipParen;
import static com.sun.source.tree.Tree.Kind.ANNOTATION;
import static com.sun.source.tree.Tree.Kind.ARRAY_ACCESS;
import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
import static com.sun.source.tree.Tree.Kind.BLOCK;
import static com.sun.source.tree.Tree.Kind.EXTENDS_WILDCARD;
import static com.sun.source.tree.Tree.Kind.IF;
import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static com.sun.source.tree.Tree.Kind.NEW_CLASS;
import static com.sun.source.tree.Tree.Kind.STRING_LITERAL;
import static com.sun.source.tree.Tree.Kind.UNION_TYPE;
import static com.sun.source.tree.Tree.Kind.VARIABLE;
import static java.util.stream.Collectors.toList;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.base.Verify;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Multiset;
import com.google.common.collect.PeekingIterator;
import com.google.common.collect.Streams;
import com.palantir.javaformat.BreakBehaviours;
import com.palantir.javaformat.CloseOp;
import com.palantir.javaformat.FormattingError;
import com.palantir.javaformat.Indent;
import com.palantir.javaformat.Input;
import com.palantir.javaformat.LastLevelBreakability;
import com.palantir.javaformat.Op;
import com.palantir.javaformat.OpenOp;
import com.palantir.javaformat.OpsBuilder;
import com.palantir.javaformat.OpsBuilder.BlankLineWanted;
import com.palantir.javaformat.PartialInlineability;
import com.palantir.javaformat.doc.Break;
import com.palantir.javaformat.doc.BreakTag;
import com.palantir.javaformat.doc.FillMode;
import com.palantir.javaformat.doc.Token;
import com.palantir.javaformat.java.DimensionHelpers.SortedDims;
import com.palantir.javaformat.java.DimensionHelpers.TypeWithDims;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.BreakTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ContinueTree;
import com.sun.source.tree.DirectiveTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EmptyStatementTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExportsTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.IntersectionTypeTree;
import com.sun.source.tree.LabeledStatementTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.ModuleTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.OpensTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.ProvidesTree;
import com.sun.source.tree.RequiresTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.UnionTypeTree;
import com.sun.source.tree.UsesTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeScanner;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.lang.model.element.Name;
/** An AST visitor that builds a stream of {@link Op}s to format from the given {@link CompilationUnitTree}. */
public class JavaInputAstVisitor extends TreePathScanner {
/**
* Maximum column at which the last dot of a method chain may start. This exists in particular to improve
* readability of builder chains, but also in general to prevent hard to spot extra actions at the end of a method
* chain.
*/
private static final int METHOD_CHAIN_COLUMN_LIMIT = 80;
/** Direction for Annotations (usually VERTICAL). */
protected enum Direction {
VERTICAL,
HORIZONTAL;
boolean isVertical() {
return this == VERTICAL;
}
}
/** Whether to break or not. */
enum BreakOrNot {
YES,
NO;
boolean isYes() {
return this == YES;
}
}
/** Whether to collapse empty blocks. */
protected enum CollapseEmptyOrNot {
YES,
NO;
static CollapseEmptyOrNot valueOf(boolean b) {
return b ? YES : NO;
}
boolean isYes() {
return this == YES;
}
}
/** Whether to allow leading blank lines in blocks. */
protected enum AllowLeadingBlankLine {
YES,
NO;
static AllowLeadingBlankLine valueOf(boolean b) {
return b ? YES : NO;
}
}
/** Whether to allow trailing blank lines in blocks. */
protected enum AllowTrailingBlankLine {
YES,
NO;
static AllowTrailingBlankLine valueOf(boolean b) {
return b ? YES : NO;
}
}
/** Whether to include braces. */
protected enum BracesOrNot {
YES,
NO;
boolean isYes() {
return this == YES;
}
}
/** Whether or not to include dimensions. */
enum DimensionsOrNot {
YES,
NO;
boolean isYes() {
return this == YES;
}
}
/** Whether or not the declaration is Varargs. */
enum VarArgsOrNot {
YES,
NO;
static VarArgsOrNot valueOf(boolean b) {
return b ? YES : NO;
}
boolean isYes() {
return this == YES;
}
static VarArgsOrNot fromVariable(VariableTree node) {
return valueOf((((JCTree.JCVariableDecl) node).mods.flags & Flags.VARARGS) == Flags.VARARGS);
}
}
/** Whether the formal parameter declaration is a receiver. */
enum ReceiverParameter {
YES,
NO;
boolean isYes() {
return this == YES;
}
}
/** Whether these declarations are the first in the block. */
protected enum FirstDeclarationsOrNot {
YES,
NO;
boolean isYes() {
return this == YES;
}
}
protected final OpsBuilder builder;
protected static final Indent.Const ZERO = Indent.Const.ZERO;
protected final int indentMultiplier;
protected final Indent.Const minusTwo;
protected final Indent.Const minusFour;
protected final Indent.Const plusTwo;
protected final Indent.Const plusFour;
private static ImmutableList breakList(Optional breakTag) {
return ImmutableList.of(Break.make(FillMode.UNIFIED, " ", ZERO, breakTag));
}
private static ImmutableList breakFillList(Optional breakTag) {
return ImmutableList.of(
OpenOp.make(ZERO), Break.make(FillMode.INDEPENDENT, " ", ZERO, breakTag), CloseOp.make());
}
private static ImmutableList forceBreakList(Optional breakTag) {
return ImmutableList.of(Break.make(FillMode.FORCED, "", Indent.Const.ZERO, breakTag));
}
private static final ImmutableList EMPTY_LIST = ImmutableList.of();
/**
* Allow multi-line filling (of array initializers, argument lists, and boolean expressions) for items with length
* less than or equal to this threshold.
*/
private static final int MAX_ITEM_LENGTH_FOR_FILLING = 10;
/**
* The {@code Visitor} constructor.
*
* @param builder the {@link OpsBuilder}
*/
public JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) {
this.builder = builder;
this.indentMultiplier = indentMultiplier;
minusTwo = Indent.Const.make(-2, indentMultiplier);
minusFour = Indent.Const.make(-4, indentMultiplier);
plusTwo = Indent.Const.make(+2, indentMultiplier);
plusFour = Indent.Const.make(+4, indentMultiplier);
}
/** A record of whether we have visited into an expression. */
private final Deque inExpression = new ArrayDeque<>(ImmutableList.of(false));
private boolean inExpression() {
return inExpression.peekLast();
}
@Override
public Void scan(Tree tree, Void unused) {
inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast());
int previous = builder.depth();
try {
super.scan(tree, null);
} catch (FormattingError e) {
throw e;
} catch (Throwable t) {
throw new FormattingError(builder.diagnostic(Throwables.getStackTraceAsString(t)));
} finally {
inExpression.removeLast();
}
builder.checkClosed(previous);
return null;
}
@Override
public Void visitCompilationUnit(CompilationUnitTree node, Void unused) {
boolean first = true;
if (node.getPackageName() != null) {
markForPartialFormat();
visitPackage(node.getPackageName(), node.getPackageAnnotations());
builder.forcedBreak();
first = false;
}
dropEmptyDeclarations();
if (!node.getImports().isEmpty()) {
if (!first) {
builder.blankLineWanted(BlankLineWanted.YES);
}
for (ImportTree importDeclaration : node.getImports()) {
markForPartialFormat();
builder.blankLineWanted(PRESERVE);
scan(importDeclaration, null);
builder.forcedBreak();
}
first = false;
}
dropEmptyDeclarations();
for (Tree type : node.getTypeDecls()) {
if (type.getKind() == Tree.Kind.IMPORT) {
// javac treats extra semicolons in the import list as type declarations
// TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
continue;
}
if (!first) {
builder.blankLineWanted(BlankLineWanted.YES);
}
markForPartialFormat();
scan(type, null);
builder.forcedBreak();
first = false;
dropEmptyDeclarations();
}
handleModule(first, node);
// set a partial format marker at EOF to make sure we can format the entire file
markForPartialFormat();
return null;
}
protected void handleModule(boolean first, CompilationUnitTree node) {}
/** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */
protected void dropEmptyDeclarations() {
if (builder.peekToken().equals(Optional.of(";"))) {
while (builder.peekToken().equals(Optional.of(";"))) {
builder.forcedBreak();
markForPartialFormat();
token(";");
}
}
}
@Override
public Void visitClass(ClassTree tree, Void unused) {
switch (tree.getKind()) {
case ANNOTATION_TYPE:
visitAnnotationType(tree);
break;
case CLASS:
case INTERFACE:
visitClassDeclaration(tree);
break;
case ENUM:
visitEnumDeclaration(tree);
break;
default:
throw new IllegalArgumentException(tree.getKind().name());
}
return null;
}
public void visitAnnotationType(ClassTree node) {
sync(node);
builder.open(ZERO);
visitAndBreakModifiers(
node.getModifiers(), Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty());
builder.open(ZERO);
token("@");
token("interface");
builder.breakOp(" ");
visit(node.getSimpleName());
builder.close();
builder.close();
if (node.getMembers() == null) {
builder.open(plusFour);
token(";");
builder.close();
} else {
addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
}
builder.guessToken(";");
}
@Override
public Void visitArrayAccess(ArrayAccessTree node, Void unused) {
sync(node);
visitDot(node);
return null;
}
@Override
public Void visitNewArray(NewArrayTree node, Void unused) {
if (node.getType() != null) {
builder.open(plusFour, BreakBehaviours.breakThisLevel(), LastLevelBreakability.CHECK_INNER);
token("new");
builder.space();
TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES);
Tree base = extractedDims.node;
Deque dimExpressions = new ArrayDeque<>(node.getDimensions());
Deque> annotations = new ArrayDeque<>();
annotations.add(ImmutableList.copyOf(node.getAnnotations()));
annotations.addAll(node.getDimAnnotations());
annotations.addAll(extractedDims.dims);
scan(base, null);
builder.open(ZERO);
maybeAddDims(dimExpressions, annotations);
builder.close();
builder.close();
}
if (node.getInitializers() != null) {
if (node.getType() != null) {
builder.space();
}
visitArrayInitializer(node.getInitializers());
}
return null;
}
public boolean visitArrayInitializer(List extends ExpressionTree> expressions) {
int cols;
if (expressions.isEmpty()) {
tokenBreakTrailingComment("{", plusTwo);
if (builder.peekToken().equals(Optional.of(","))) {
token(",");
}
token("}", plusTwo);
} else if ((cols = argumentsAreTabular(expressions)) != -1) {
builder.open(ZERO, BreakBehaviours.breakThisLevel(), LastLevelBreakability.ACCEPT_INLINE_CHAIN);
builder.open(plusTwo, BreakBehaviours.breakThisLevel(), LastLevelBreakability.ACCEPT_INLINE_CHAIN);
token("{");
builder.forcedBreak();
boolean first = true;
for (Iterable extends ExpressionTree> row : Lists.partition(expressions, cols)) {
if (!first) {
builder.forcedBreak();
}
builder.open(row.iterator().next().getKind() == NEW_ARRAY || cols == 1 ? ZERO : plusFour);
boolean firstInRow = true;
for (ExpressionTree item : row) {
if (!firstInRow) {
token(",");
builder.breakToFill(" ");
}
scan(item, null);
firstInRow = false;
}
builder.guessToken(",");
builder.close();
first = false;
}
builder.close();
builder.breakOp();
token("}", plusTwo);
builder.close();
} else {
// Special-case the formatting of array initializers inside annotations
// to more eagerly use a one-per-line layout.
boolean inMemberValuePair = false;
// walk up past the enclosing NewArrayTree (and maybe an enclosing AssignmentTree)
TreePath path = getCurrentPath();
for (int i = 0; i < 2; i++) {
if (path == null) {
break;
}
if (path.getLeaf().getKind() == ANNOTATION) {
inMemberValuePair = true;
break;
}
path = path.getParentPath();
}
boolean shortItems = hasOnlyShortItems(expressions);
boolean allowFilledElementsOnOwnLine = shortItems || !inMemberValuePair;
builder.open(ZERO, BreakBehaviours.breakThisLevel(), LastLevelBreakability.ACCEPT_INLINE_CHAIN);
builder.open(plusTwo, BreakBehaviours.breakThisLevel(), LastLevelBreakability.ACCEPT_INLINE_CHAIN);
tokenBreakTrailingComment("{", plusTwo);
boolean hasTrailingComma = hasTrailingToken(builder.getInput(), expressions, ",");
builder.breakOp(hasTrailingComma ? FillMode.FORCED : FillMode.UNIFIED, "", ZERO);
if (allowFilledElementsOnOwnLine) {
builder.open(ZERO);
}
boolean first = true;
FillMode fillMode = shortItems ? FillMode.INDEPENDENT : FillMode.UNIFIED;
for (ExpressionTree expression : expressions) {
if (!first) {
token(",");
builder.breakOp(fillMode, " ", ZERO);
}
scan(expression, null);
first = false;
}
builder.guessToken(",");
if (allowFilledElementsOnOwnLine) {
builder.close();
}
builder.close();
builder.breakOp();
token("}", plusTwo);
builder.close();
}
return false;
}
private boolean hasOnlyShortItems(List extends ExpressionTree> expressions) {
for (ExpressionTree expression : expressions) {
int startPosition = getStartPosition(expression);
if (builder.actualSize(startPosition, getEndPosition(expression, getCurrentPath()) - startPosition)
>= MAX_ITEM_LENGTH_FOR_FILLING) {
return false;
}
}
return true;
}
@Override
public Void visitArrayType(ArrayTypeTree node, Void unused) {
sync(node);
visitAnnotatedArrayType(node);
return null;
}
private void visitAnnotatedArrayType(Tree node) {
TypeWithDims extractedDims = DimensionHelpers.extractDims(node, SortedDims.YES);
builder.open(plusFour);
scan(extractedDims.node, null);
Deque> dims = new ArrayDeque<>(extractedDims.dims);
maybeAddDims(dims);
Verify.verify(dims.isEmpty());
builder.close();
}
@Override
public Void visitAssert(AssertTree node, Void unused) {
sync(node);
builder.open(ZERO);
token("assert");
builder.space();
builder.open(node.getDetail() == null ? ZERO : plusFour);
scan(node.getCondition(), null);
if (node.getDetail() != null) {
builder.breakOp(" ");
token(":");
builder.space();
scan(node.getDetail(), null);
}
builder.close();
builder.close();
token(";");
return null;
}
@Override
public Void visitAssignment(AssignmentTree node, Void unused) {
sync(node);
builder.open(OpenOp.builder()
.plusIndent(plusFour)
.breakBehaviour(BreakBehaviours.breakOnlyIfInnerLevelsThenFitOnOneLine(false))
.build());
scan(node.getVariable(), null);
builder.space();
splitToken(operatorName(node));
builder.breakOp(" ");
scan(node.getExpression(), null);
builder.close();
return null;
}
@Override
public Void visitBlock(BlockTree node, Void unused) {
visitBlock(node, CollapseEmptyOrNot.NO, AllowLeadingBlankLine.NO, AllowTrailingBlankLine.NO);
return null;
}
@Override
public Void visitCompoundAssignment(CompoundAssignmentTree node, Void unused) {
sync(node);
builder.open(OpenOp.builder()
.plusIndent(plusFour)
.breakBehaviour(BreakBehaviours.breakOnlyIfInnerLevelsThenFitOnOneLine(false))
.build());
scan(node.getVariable(), null);
builder.space();
splitToken(operatorName(node));
builder.breakOp(" ");
scan(node.getExpression(), null);
builder.close();
return null;
}
@Override
public Void visitBreak(BreakTree node, Void unused) {
sync(node);
builder.open(plusFour);
token("break");
if (node.getLabel() != null) {
builder.breakOp(" ");
visit(node.getLabel());
}
builder.close();
token(";");
return null;
}
@Override
public Void visitTypeCast(TypeCastTree node, Void unused) {
sync(node);
builder.open(
plusFour,
BreakBehaviours.preferBreakingLastInnerLevel(true),
LastLevelBreakability.ACCEPT_INLINE_CHAIN);
token("(");
scan(node.getType(), null);
token(")");
builder.breakOp(" ");
scan(node.getExpression(), null);
builder.close();
return null;
}
@Override
public Void visitNewClass(NewClassTree node, Void unused) {
sync(node);
LastLevelBreakability breakabilityIfLastLevel = node.getClassBody() != null
? LastLevelBreakability.ACCEPT_INLINE_CHAIN
: LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER;
builder.open(OpenOp.builder()
.debugName("visitNewClass")
.plusIndent(ZERO)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(true))
.breakabilityIfLastLevel(breakabilityIfLastLevel)
.build());
if (node.getEnclosingExpression() != null) {
scan(node.getEnclosingExpression(), null);
builder.breakOp();
token(".");
}
token("new");
builder.space();
addTypeArguments(node.getTypeArguments(), plusFour);
if (node.getClassBody() != null) {
builder.addAll(visitModifiers(node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()));
}
scan(node.getIdentifier(), null);
addArguments(node.getArguments(), plusFour);
builder.close();
if (node.getClassBody() != null) {
addBodyDeclarations(node.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
}
return null;
}
@Override
public Void visitConditionalExpression(ConditionalExpressionTree node, Void unused) {
sync(node);
builder.open(plusFour);
scan(node.getCondition(), null);
builder.breakOp(" ");
token("?");
builder.space();
scan(node.getTrueExpression(), null);
builder.breakOp(" ");
token(":");
builder.space();
scan(node.getFalseExpression(), null);
builder.close();
return null;
}
@Override
public Void visitContinue(ContinueTree node, Void unused) {
sync(node);
builder.open(plusFour);
token("continue");
if (node.getLabel() != null) {
builder.breakOp(" ");
visit(node.getLabel());
}
token(";");
builder.close();
return null;
}
@Override
public Void visitDoWhileLoop(DoWhileLoopTree node, Void unused) {
sync(node);
token("do");
visitStatement(
node.getStatement(), CollapseEmptyOrNot.YES, AllowLeadingBlankLine.YES, AllowTrailingBlankLine.YES);
if (node.getStatement().getKind() == BLOCK) {
builder.space();
} else {
builder.breakOp(" ");
}
token("while");
builder.space();
token("(");
scan(skipParen(node.getCondition()), null);
token(")");
token(";");
return null;
}
@Override
public Void visitEmptyStatement(EmptyStatementTree node, Void unused) {
sync(node);
dropEmptyDeclarations();
return null;
}
@Override
public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void unused) {
sync(node);
builder.open(ZERO);
token("for");
builder.space();
token("(");
builder.open(ZERO);
visitToDeclare(
DeclarationKind.NONE,
Direction.HORIZONTAL,
node.getVariable(),
Optional.of(node.getExpression()),
":",
/* trailing= */ Optional.empty());
builder.close();
token(")");
builder.close();
visitStatement(
node.getStatement(), CollapseEmptyOrNot.YES, AllowLeadingBlankLine.YES, AllowTrailingBlankLine.NO);
return null;
}
private void visitEnumConstantDeclaration(VariableTree enumConstant) {
for (AnnotationTree annotation : enumConstant.getModifiers().getAnnotations()) {
scan(annotation, null);
builder.forcedBreak();
}
visit(enumConstant.getName());
NewClassTree init = ((NewClassTree) enumConstant.getInitializer());
if (init.getArguments().isEmpty()) {
builder.guessToken("(");
builder.guessToken(")");
} else {
addArguments(init.getArguments(), plusFour);
}
if (init.getClassBody() != null) {
addBodyDeclarations(init.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
}
}
public boolean visitEnumDeclaration(ClassTree node) {
sync(node);
builder.open(ZERO);
visitAndBreakModifiers(
node.getModifiers(), Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty());
builder.open(plusFour);
token("enum");
builder.breakOp(" ");
visit(node.getSimpleName());
builder.close();
builder.close();
if (!node.getImplementsClause().isEmpty()) {
builder.open(plusFour);
builder.breakOp(" ");
builder.open(plusFour);
token("implements");
builder.breakOp(" ");
builder.open(ZERO);
boolean first = true;
for (Tree superInterfaceType : node.getImplementsClause()) {
if (!first) {
token(",");
builder.breakToFill(" ");
}
scan(superInterfaceType, null);
first = false;
}
builder.close();
builder.close();
builder.close();
}
builder.space();
tokenBreakTrailingComment("{", plusTwo);
ArrayList enumConstants = new ArrayList<>();
ArrayList members = new ArrayList<>();
for (Tree member : node.getMembers()) {
if (member instanceof JCTree.JCVariableDecl) {
JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) member;
if ((variableDecl.mods.flags & Flags.ENUM) == Flags.ENUM) {
enumConstants.add(variableDecl);
continue;
}
}
members.add(member);
}
if (enumConstants.isEmpty() && members.isEmpty()) {
if (builder.peekToken().equals(Optional.of(";"))) {
builder.open(plusTwo);
builder.forcedBreak();
token(";");
builder.forcedBreak();
dropEmptyDeclarations();
builder.close();
builder.open(ZERO);
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusTwo);
builder.close();
} else {
builder.open(ZERO);
builder.blankLineWanted(BlankLineWanted.NO);
token("}");
builder.close();
}
} else {
builder.open(plusTwo);
builder.blankLineWanted(BlankLineWanted.NO);
builder.forcedBreak();
builder.open(ZERO);
boolean first = true;
for (VariableTree enumConstant : enumConstants) {
if (!first) {
token(",");
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.PRESERVE);
}
markForPartialFormat();
visitEnumConstantDeclaration(enumConstant);
first = false;
}
if (builder.peekToken().orElse("").equals(",")) {
token(",");
builder.forcedBreak(); // The ";" goes on its own line.
}
builder.close();
builder.close();
if (builder.peekToken().equals(Optional.of(";"))) {
builder.open(plusTwo);
token(";");
builder.forcedBreak();
dropEmptyDeclarations();
builder.close();
}
builder.open(ZERO);
addBodyDeclarations(members, BracesOrNot.NO, FirstDeclarationsOrNot.NO);
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusTwo);
builder.close();
}
builder.guessToken(";");
return false;
}
@Override
public Void visitMemberReference(MemberReferenceTree node, Void unused) {
sync(node);
builder.open(OpenOp.builder()
.plusIndent(plusFour)
.debugName("methodReference")
.breakabilityIfLastLevel(LastLevelBreakability.CHECK_INNER)
.breakBehaviour(BreakBehaviours.inlineSuffix())
.build());
scan(node.getQualifierExpression(), null);
builder.open(ZERO);
builder.breakOp();
builder.op("::");
addTypeArguments(node.getTypeArguments(), plusFour);
switch (node.getMode()) {
case INVOKE:
visit(node.getName());
break;
case NEW:
token("new");
break;
default:
throw new IllegalArgumentException(node.getMode().name());
}
builder.close();
builder.close();
return null;
}
@Override
public Void visitExpressionStatement(ExpressionStatementTree node, Void unused) {
sync(node);
scan(node.getExpression(), null);
token(";");
return null;
}
@Override
public Void visitVariable(VariableTree node, Void unused) {
sync(node);
visitVariables(ImmutableList.of(node), DeclarationKind.NONE, inlineAnnotationDirection(node.getModifiers()));
return null;
}
void visitVariables(List fragments, DeclarationKind declarationKind, Direction annotationDirection) {
if (fragments.size() == 1) {
VariableTree fragment = fragments.get(0);
declareOne(
declarationKind,
annotationDirection,
Optional.of(fragment.getModifiers()),
fragment.getType(),
/* name= */ fragment.getName(),
"",
"=",
Optional.ofNullable(fragment.getInitializer()),
Optional.of(";"),
/* receiverExpression= */ Optional.empty(),
Optional.ofNullable(variableFragmentDims(true, 0, fragment.getType())));
} else {
declareMany(fragments, annotationDirection);
}
}
private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) {
if (type == null) {
return null;
}
if (first) {
return DimensionHelpers.extractDims(type, SortedDims.YES);
}
TypeWithDims dims = DimensionHelpers.extractDims(type, SortedDims.NO);
return new TypeWithDims(
null, leadingDims > 0 ? dims.dims.subList(0, dims.dims.size() - leadingDims) : dims.dims);
}
@Override
public Void visitForLoop(ForLoopTree node, Void unused) {
sync(node);
token("for");
builder.space();
token("(");
builder.open(plusFour);
builder.open(
node.getInitializer().size() > 1
&& node.getInitializer().get(0).getKind() == Tree.Kind.EXPRESSION_STATEMENT
? plusFour
: ZERO);
if (!node.getInitializer().isEmpty()) {
if (node.getInitializer().get(0).getKind() == VARIABLE) {
PeekingIterator it =
Iterators.peekingIterator(node.getInitializer().iterator());
visitVariables(variableFragments(it, it.next()), DeclarationKind.NONE, Direction.HORIZONTAL);
} else {
boolean first = true;
builder.open(ZERO);
for (StatementTree t : node.getInitializer()) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(((ExpressionStatementTree) t).getExpression(), null);
first = false;
}
token(";");
builder.close();
}
} else {
token(";");
}
builder.close();
builder.breakOp(" ");
if (node.getCondition() != null) {
scan(node.getCondition(), null);
}
token(";");
if (!node.getUpdate().isEmpty()) {
builder.breakOp(" ");
builder.open(node.getUpdate().size() <= 1 ? ZERO : plusFour);
boolean firstUpdater = true;
for (ExpressionStatementTree updater : node.getUpdate()) {
if (!firstUpdater) {
token(",");
builder.breakToFill(" ");
}
scan(updater.getExpression(), null);
firstUpdater = false;
}
builder.guessToken(";");
builder.close();
} else {
builder.space();
}
builder.close();
token(")");
visitStatement(
node.getStatement(), CollapseEmptyOrNot.YES, AllowLeadingBlankLine.YES, AllowTrailingBlankLine.NO);
return null;
}
@Override
public Void visitIf(IfTree node, Void unused) {
sync(node);
// Collapse chains of else-ifs.
List expressions = new ArrayList<>();
List statements = new ArrayList<>();
while (true) {
expressions.add(node.getCondition());
statements.add(node.getThenStatement());
if (node.getElseStatement() != null && node.getElseStatement().getKind() == IF) {
node = (IfTree) node.getElseStatement();
} else {
break;
}
}
builder.open(ZERO);
boolean first = true;
boolean followingBlock = false;
int expressionsN = expressions.size();
for (int i = 0; i < expressionsN; i++) {
if (!first) {
if (followingBlock) {
builder.space();
} else {
builder.forcedBreak();
}
token("else");
builder.space();
}
token("if");
builder.space();
token("(");
scan(skipParen(expressions.get(i)), null);
token(")");
// An empty block can collapse to "{}" if there are no if/else or else clauses
boolean onlyClause = expressionsN == 1 && node.getElseStatement() == null;
// Trailing blank lines are permitted if this isn't the last clause
boolean trailingClauses = i < expressionsN - 1 || node.getElseStatement() != null;
visitStatement(
statements.get(i),
CollapseEmptyOrNot.valueOf(onlyClause),
AllowLeadingBlankLine.YES,
AllowTrailingBlankLine.valueOf(trailingClauses));
followingBlock = statements.get(i).getKind() == BLOCK;
first = false;
}
if (node.getElseStatement() != null) {
if (followingBlock) {
builder.space();
} else {
builder.forcedBreak();
}
token("else");
visitStatement(
node.getElseStatement(),
CollapseEmptyOrNot.NO,
AllowLeadingBlankLine.YES,
AllowTrailingBlankLine.NO);
}
builder.close();
return null;
}
@Override
public Void visitImport(ImportTree node, Void unused) {
sync(node);
token("import");
builder.space();
if (node.isStatic()) {
token("static");
builder.space();
}
visitName(node.getQualifiedIdentifier());
token(";");
// TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
dropEmptyDeclarations();
return null;
}
@Override
public Void visitBinary(BinaryTree node, Void unused) {
sync(node);
/*
* Collect together all operators with same precedence to clean up indentation.
*/
List operands = new ArrayList<>();
List operators = new ArrayList<>();
walkInfix(precedence(node), node, operands, operators);
boolean isStringConcat = isStringConcat(node);
boolean shouldPreserveNewlines = isStringConcat && lineSpan(node) > 2;
FillMode fillMode = hasOnlyShortItems(operands) || isStringConcat ? INDEPENDENT : UNIFIED;
builder.open(
plusFour,
BreakBehaviours.breakThisLevel(),
LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER);
scan(operands.get(0), null);
int operatorsN = operators.size();
boolean shouldEnforceNewline = builder.mostRecentTokenFollowedByNewline();
for (int i = 0; i < operatorsN; i++) {
FillMode nextFillMode = shouldPreserveNewlines && shouldEnforceNewline ? UNIFIED : fillMode;
builder.breakOp(nextFillMode, " ", ZERO);
builder.op(operators.get(i));
shouldEnforceNewline = builder.mostRecentTokenFollowedByNewline();
builder.space();
scan(operands.get(i + 1), null);
shouldEnforceNewline = shouldEnforceNewline || builder.mostRecentTokenFollowedByNewline();
}
builder.close();
return null;
}
@Override
public Void visitInstanceOf(InstanceOfTree node, Void unused) {
sync(node);
builder.open(plusFour);
scan(node.getExpression(), null);
builder.breakOp(" ");
builder.open(ZERO);
token("instanceof");
builder.breakOp(" ");
scan(node.getType(), null);
builder.close();
builder.close();
return null;
}
@Override
public Void visitIntersectionType(IntersectionTypeTree node, Void unused) {
sync(node);
builder.open(plusFour);
boolean first = true;
for (Tree type : node.getBounds()) {
if (!first) {
builder.breakToFill(" ");
token("&");
builder.space();
}
scan(type, null);
first = false;
}
builder.close();
return null;
}
@Override
public Void visitLabeledStatement(LabeledStatementTree node, Void unused) {
sync(node);
builder.open(ZERO);
visit(node.getLabel());
token(":");
builder.forcedBreak();
builder.close();
scan(node.getStatement(), null);
return null;
}
@Override
public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) {
sync(node);
boolean statementBody = node.getBodyKind() == LambdaExpressionTree.BodyKind.STATEMENT;
visitLambdaExpression(node, statementBody);
return null;
}
protected void visitLambdaExpression(LambdaExpressionTree node, boolean statementBody) {
boolean parens = builder.peekToken().equals(Optional.of("("));
builder.open("lambda arguments", parens ? plusFour : ZERO);
if (parens) {
token("(");
}
boolean first = true;
for (VariableTree parameter : node.getParameters()) {
if (!first) {
token(",");
builder.breakOp(" ");
}
visitVariables(
ImmutableList.of(parameter),
DeclarationKind.NONE,
inlineAnnotationDirection(parameter.getModifiers()));
first = false;
}
if (parens) {
token(")");
}
builder.close();
builder.space();
builder.op("->");
builder.open(OpenOp.builder()
.debugName("lambda body")
.plusIndent(statementBody ? ZERO : plusFour)
.breakBehaviour(
statementBody
? BreakBehaviours.preferBreakingLastInnerLevel(true)
: BreakBehaviours.breakOnlyIfInnerLevelsThenFitOnOneLine(false))
.breakabilityIfLastLevel(
statementBody
? LastLevelBreakability.ACCEPT_INLINE_CHAIN
: LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER)
.build());
if (statementBody) {
builder.space();
} else {
builder.breakOp(" ");
}
if (node.getBody().getKind() == Tree.Kind.BLOCK) {
visitBlock(
(BlockTree) node.getBody(),
CollapseEmptyOrNot.YES,
AllowLeadingBlankLine.NO,
AllowTrailingBlankLine.NO);
} else {
scan(node.getBody(), null);
}
builder.close();
}
@Override
public Void visitAnnotation(AnnotationTree node, Void unused) {
sync(node);
if (visitSingleMemberAnnotation(node)) {
return null;
}
builder.open(ZERO);
token("@");
scan(node.getAnnotationType(), null);
if (!node.getArguments().isEmpty()) {
builder.open(plusFour);
token("(");
builder.breakOp();
boolean first = true;
// Format the member value pairs one-per-line if any of them are
// initialized with arrays.
boolean hasArrayInitializer = Iterables.any(node.getArguments(), JavaInputAstVisitor::isArrayValue);
for (ExpressionTree argument : node.getArguments()) {
if (!first) {
token(",");
if (hasArrayInitializer) {
builder.forcedBreak();
} else {
builder.breakOp(" ");
}
}
if (argument instanceof AssignmentTree) {
visitAnnotationArgument((AssignmentTree) argument);
} else {
scan(argument, null);
}
first = false;
}
token(")");
builder.close();
builder.close();
return null;
} else if (builder.peekToken().equals(Optional.of("("))) {
token("(");
token(")");
}
builder.close();
return null;
}
private static boolean isArrayValue(ExpressionTree argument) {
if (!(argument instanceof AssignmentTree)) {
return false;
}
ExpressionTree expression = ((AssignmentTree) argument).getExpression();
return expression instanceof NewArrayTree && ((NewArrayTree) expression).getType() == null;
}
public void visitAnnotationArgument(AssignmentTree node) {
boolean isArrayInitializer = node.getExpression().getKind() == NEW_ARRAY;
sync(node);
builder.open(
isArrayInitializer ? ZERO : plusFour,
BreakBehaviours.preferBreakingLastInnerLevel(true),
LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER);
scan(node.getVariable(), null);
builder.space();
token("=");
if (isArrayInitializer) {
builder.space();
} else {
builder.breakOp(" ");
}
scan(node.getExpression(), null);
builder.close();
}
@Override
public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) {
sync(node);
ExpressionTree base = node.getUnderlyingType();
if (base instanceof MemberSelectTree) {
MemberSelectTree selectTree = (MemberSelectTree) base;
scan(selectTree.getExpression(), null);
token(".");
visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
builder.breakToFill(" ");
visit(selectTree.getIdentifier());
} else if (base instanceof ArrayTypeTree) {
visitAnnotatedArrayType(node);
} else {
visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
builder.breakToFill(" ");
scan(base, null);
}
return null;
}
// TODO(fawind): Use Flags.COMPACT_RECORD_CONSTRUCTOR once if/when we drop support for Java 11
protected static final long COMPACT_RECORD_CONSTRUCTOR = 1L << 51;
protected static final long RECORD = 1L << 61;
@Override
public Void visitMethod(MethodTree node, Void unused) {
sync(node);
List extends AnnotationTree> annotations = node.getModifiers().getAnnotations();
List extends AnnotationTree> returnTypeAnnotations = ImmutableList.of();
boolean isRecordConstructor =
(((JCTree.JCMethodDecl) node).mods.flags & COMPACT_RECORD_CONSTRUCTOR) == COMPACT_RECORD_CONSTRUCTOR;
if (!node.getTypeParameters().isEmpty() && !annotations.isEmpty()) {
int typeParameterStart = getStartPosition(node.getTypeParameters().get(0));
for (int i = 0; i < annotations.size(); i++) {
if (getStartPosition(annotations.get(i)) > typeParameterStart) {
returnTypeAnnotations = annotations.subList(i, annotations.size());
annotations = annotations.subList(0, i);
break;
}
}
}
builder.addAll(
visitModifiers(annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty()));
Tree baseReturnType = null;
Deque> dims = null;
if (node.getReturnType() != null) {
TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getReturnType(), SortedDims.YES);
baseReturnType = extractedDims.node;
dims = new ArrayDeque<>(extractedDims.dims);
}
builder.open(plusFour);
BreakTag breakBeforeName = new BreakTag();
BreakTag breakBeforeType = new BreakTag();
builder.open(ZERO);
{
boolean first = true;
if (!node.getTypeParameters().isEmpty()) {
token("<");
typeParametersRest(node.getTypeParameters(), plusFour);
if (!returnTypeAnnotations.isEmpty()) {
builder.breakToFill(" ");
visitAnnotations(returnTypeAnnotations, BreakOrNot.NO, BreakOrNot.NO);
}
first = false;
}
boolean openedNameAndTypeScope = false;
// constructor-like declarations that don't match the name of the enclosing class are
// parsed as method declarations with a null return type
if (baseReturnType != null) {
if (!first) {
builder.breakOp(INDEPENDENT, " ", ZERO, Optional.of(breakBeforeType));
} else {
first = false;
}
if (!openedNameAndTypeScope) {
builder.open(make(breakBeforeType, plusFour, ZERO));
openedNameAndTypeScope = true;
}
scan(baseReturnType, null);
maybeAddDims(dims);
}
if (!first) {
builder.breakOp(FillMode.INDEPENDENT, " ", ZERO, Optional.of(breakBeforeName));
} else {
first = false;
}
if (!openedNameAndTypeScope) {
builder.open(ZERO);
openedNameAndTypeScope = true;
}
String name = node.getName().toString();
if (name.equals("")) {
name = builder.peekToken().get();
}
token(name);
if (!isRecordConstructor) {
token("(");
}
// end of name and type scope
builder.close();
}
builder.close();
builder.open(Indent.If.make(breakBeforeName, plusFour, ZERO));
builder.open(Indent.If.make(breakBeforeType, plusFour, ZERO));
builder.open(ZERO);
{
if (!isRecordConstructor) {
if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) {
// Break before args.
builder.breakToFill("");
visitFormals(Optional.ofNullable(node.getReceiverParameter()), node.getParameters());
}
token(")");
}
if (dims != null) {
maybeAddDims(dims);
}
if (!node.getThrows().isEmpty()) {
builder.breakToFill(" ");
builder.open(plusFour);
{
visitThrowsClause(node.getThrows());
}
builder.close();
}
if (node.getDefaultValue() != null) {
builder.space();
token("default");
if (node.getDefaultValue().getKind() == Tree.Kind.NEW_ARRAY) {
builder.open(minusFour);
{
builder.space();
scan(node.getDefaultValue(), null);
}
builder.close();
} else {
builder.open(ZERO);
{
builder.breakToFill(" ");
scan(node.getDefaultValue(), null);
}
builder.close();
}
}
}
builder.close();
builder.close();
builder.close();
if (node.getBody() == null) {
token(";");
} else {
builder.space();
builder.token("{", Token.RealOrImaginary.REAL, plusTwo, Optional.of(plusTwo));
}
builder.close();
if (node.getBody() != null) {
methodBody(node);
}
return null;
}
private void methodBody(MethodTree node) {
if (node.getBody().getStatements().isEmpty()) {
builder.blankLineWanted(BlankLineWanted.NO);
} else {
builder.open(plusTwo);
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.PRESERVE);
visitStatements(node.getBody().getStatements(), false);
builder.close();
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.NO);
markForPartialFormat();
}
token("}", plusTwo);
}
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
sync(node);
if (handleLogStatement(node)) {
return null;
}
visitDot(node);
return null;
}
/**
* Special-cases log statements, to output:
*
* {@code
* logger.atInfo().log(
* "Number of foos: %d, foos.size());
* }
*
* Instead of:
*
*
{@code
* logger
* .atInfo()
* .log(
* "Number of foos: %d, foos.size());
* }
*/
private boolean handleLogStatement(MethodInvocationTree node) {
if (!getMethodName(node).contentEquals("log")) {
return false;
}
Deque parts = new ArrayDeque<>();
ExpressionTree curr = node;
while (curr instanceof MethodInvocationTree) {
MethodInvocationTree method = (MethodInvocationTree) curr;
parts.addFirst(method);
if (!LOG_METHODS.contains(getMethodName(method).toString())) {
return false;
}
curr = Trees.getMethodReceiver(method);
}
if (!(curr instanceof IdentifierTree)) {
return false;
}
parts.addFirst(curr);
visitDotWithPrefix(ImmutableList.copyOf(parts), false, ImmutableList.of(parts.size() - 1), INDEPENDENT);
return true;
}
static final ImmutableSet LOG_METHODS = ImmutableSet.of(
"at",
"atConfig",
"atDebug",
"atFine",
"atFiner",
"atFinest",
"atInfo",
"atMostEvery",
"atSevere",
"atWarning",
"every",
"log",
"logVarargs",
"perUnique",
"withCause",
"withStackTrace");
private static List handleStream(List parts) {
return indexes(parts.stream(), p -> {
if (!(p instanceof MethodInvocationTree)) {
return false;
}
Name name = getMethodName((MethodInvocationTree) p);
return Stream.of("stream", "parallelStream", "toBuilder").anyMatch(name::contentEquals);
})
.collect(toList());
}
private static Stream indexes(Stream stream, Predicate predicate) {
return Streams.mapWithIndex(stream, (x, i) -> predicate.apply(x) ? i : -1)
.filter(x -> x != -1);
}
@Override
public Void visitMemberSelect(MemberSelectTree node, Void unused) {
sync(node);
visitDot(node);
return null;
}
@Override
public Void visitLiteral(LiteralTree node, Void unused) {
sync(node);
String sourceForNode = getSourceForNode(node, getCurrentPath());
// A negative numeric literal -n is usually represented as unary minus on n,
// but that doesn't work for integer or long MIN_VALUE. The parser works
// around that by representing it directly as a signed literal (with no
// unary minus), but the lexer still expects two tokens.
if (sourceForNode.startsWith("-")) {
token("-");
sourceForNode = sourceForNode.substring(1).trim();
}
token(sourceForNode);
return null;
}
private void visitPackage(ExpressionTree packageName, List extends AnnotationTree> packageAnnotations) {
if (!packageAnnotations.isEmpty()) {
for (AnnotationTree annotation : packageAnnotations) {
builder.forcedBreak();
scan(annotation, null);
}
builder.forcedBreak();
}
builder.open(plusFour);
token("package");
builder.space();
visitName(packageName);
builder.close();
token(";");
}
@Override
public Void visitParameterizedType(ParameterizedTypeTree node, Void unused) {
sync(node);
if (node.getTypeArguments().isEmpty()) {
scan(node.getType(), null);
token("<");
token(">");
} else {
builder.open(plusFour);
scan(node.getType(), null);
token("<");
builder.breakOp();
builder.open(ZERO);
boolean first = true;
for (Tree typeArgument : node.getTypeArguments()) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(typeArgument, null);
first = false;
}
builder.close();
builder.close();
token(">");
}
return null;
}
@Override
public Void visitParenthesized(ParenthesizedTree node, Void unused) {
token("(");
scan(node.getExpression(), null);
token(")");
return null;
}
@Override
public Void visitUnary(UnaryTree node, Void unused) {
sync(node);
String operatorName = operatorName(node);
if (((JCTree) node).getTag().isPostUnaryOp()) {
scan(node.getExpression(), null);
splitToken(operatorName);
} else {
splitToken(operatorName);
if (ambiguousUnaryOperator(node, operatorName)) {
builder.space();
}
scan(node.getExpression(), null);
}
return null;
}
private void splitToken(String operatorName) {
for (int i = 0; i < operatorName.length(); i++) {
token(String.valueOf(operatorName.charAt(i)));
}
}
private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) {
switch (node.getKind()) {
case UNARY_MINUS:
case UNARY_PLUS:
break;
default:
return false;
}
if (!(node.getExpression() instanceof UnaryTree)) {
return false;
}
JCTree.Tag tag = ((JCTree) node.getExpression()).getTag();
if (tag.isPostUnaryOp()) {
return false;
}
if (!operatorName(node).startsWith(operatorName)) {
return false;
}
return true;
}
@Override
public Void visitPrimitiveType(PrimitiveTypeTree node, Void unused) {
sync(node);
switch (node.getPrimitiveTypeKind()) {
case BOOLEAN:
token("boolean");
break;
case BYTE:
token("byte");
break;
case SHORT:
token("short");
break;
case INT:
token("int");
break;
case LONG:
token("long");
break;
case CHAR:
token("char");
break;
case FLOAT:
token("float");
break;
case DOUBLE:
token("double");
break;
case VOID:
token("void");
break;
default:
throw new RuntimeException(node.getPrimitiveTypeKind().name());
}
return null;
}
public boolean visit(Name name) {
token(name.toString());
return false;
}
@Override
public Void visitReturn(ReturnTree node, Void unused) {
sync(node);
token("return");
if (node.getExpression() != null) {
builder.space();
scan(node.getExpression(), null);
}
token(";");
return null;
}
// TODO(cushon): is this worth special-casing?
boolean visitSingleMemberAnnotation(AnnotationTree node) {
if (node.getArguments().size() != 1) {
return false;
}
ExpressionTree value = getOnlyElement(node.getArguments());
if (value.getKind() == ASSIGNMENT) {
return false;
}
boolean isArrayInitializer = value.getKind() == NEW_ARRAY;
builder.open(
isArrayInitializer ? ZERO : plusFour,
BreakBehaviours.preferBreakingLastInnerLevel(true),
LastLevelBreakability.CHECK_INNER);
token("@");
scan(node.getAnnotationType(), null);
token("(");
if (!isArrayInitializer) {
builder.breakOp();
}
scan(value, null);
builder.close();
token(")");
return true;
}
@Override
public Void visitCase(CaseTree node, Void unused) {
sync(node);
markForPartialFormat();
builder.forcedBreak();
if (node.getExpression() == null) {
token("default", plusTwo);
token(":");
} else {
token("case", plusTwo);
builder.space();
scan(node.getExpression(), null);
token(":");
}
boolean isBlock =
node.getStatements().size() == 1 && node.getStatements().get(0).getKind() == BLOCK;
builder.open(isBlock ? ZERO : plusTwo);
if (isBlock) {
builder.space();
}
visitStatements(node.getStatements(), isBlock);
builder.close();
return null;
}
@Override
public Void visitSwitch(SwitchTree node, Void unused) {
sync(node);
visitSwitch(node.getExpression(), node.getCases());
return null;
}
protected void visitSwitch(ExpressionTree expression, List extends CaseTree> cases) {
token("switch");
builder.space();
token("(");
scan(skipParen(expression), null);
token(")");
builder.space();
tokenBreakTrailingComment("{", plusTwo);
builder.blankLineWanted(BlankLineWanted.NO);
builder.open(plusTwo);
boolean first = true;
for (CaseTree caseTree : cases) {
if (!first) {
builder.blankLineWanted(BlankLineWanted.PRESERVE);
}
scan(caseTree, null);
first = false;
}
builder.close();
builder.forcedBreak();
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusFour);
}
@Override
public Void visitSynchronized(SynchronizedTree node, Void unused) {
sync(node);
token("synchronized");
builder.space();
token("(");
builder.open(plusFour);
builder.breakOp();
scan(skipParen(node.getExpression()), null);
builder.close();
token(")");
builder.space();
scan(node.getBlock(), null);
return null;
}
@Override
public Void visitThrow(ThrowTree node, Void unused) {
sync(node);
token("throw");
builder.space();
scan(node.getExpression(), null);
token(";");
return null;
}
@Override
public Void visitTry(TryTree node, Void unused) {
sync(node);
builder.open(ZERO);
token("try");
builder.space();
if (!node.getResources().isEmpty()) {
token("(");
builder.open(node.getResources().size() > 1 ? plusFour : ZERO);
boolean first = true;
for (Tree resource : node.getResources()) {
if (!first) {
builder.forcedBreak();
}
if (resource instanceof VariableTree) {
VariableTree variableTree = (VariableTree) resource;
declareOne(
DeclarationKind.PARAMETER,
inlineAnnotationDirection(variableTree.getModifiers()),
Optional.of(variableTree.getModifiers()),
variableTree.getType(),
/* name= */ variableTree.getName(),
"",
"=",
Optional.ofNullable(variableTree.getInitializer()),
/* trailing= */ Optional.empty(),
/* receiverExpression= */ Optional.empty(),
/* typeWithDims= */ Optional.empty());
} else {
// TODO(cushon): think harder about what to do with `try (resource1; resource2) {}`
scan(resource, null);
}
if (builder.peekToken().equals(Optional.of(";"))) {
token(";");
builder.space();
}
first = false;
}
if (builder.peekToken().equals(Optional.of(";"))) {
token(";");
builder.space();
}
token(")");
builder.close();
builder.space();
}
// An empty try-with-resources body can collapse to "{}" if there are no trailing catch or
// finally blocks.
boolean trailingClauses = !node.getCatches().isEmpty() || node.getFinallyBlock() != null;
visitBlock(
node.getBlock(),
CollapseEmptyOrNot.valueOf(!trailingClauses),
AllowLeadingBlankLine.YES,
AllowTrailingBlankLine.valueOf(trailingClauses));
for (int i = 0; i < node.getCatches().size(); i++) {
CatchTree catchClause = node.getCatches().get(i);
trailingClauses = i < node.getCatches().size() - 1 || node.getFinallyBlock() != null;
visitCatchClause(catchClause, AllowTrailingBlankLine.valueOf(trailingClauses));
}
if (node.getFinallyBlock() != null) {
builder.space();
token("finally");
builder.space();
visitBlock(
node.getFinallyBlock(),
CollapseEmptyOrNot.NO,
AllowLeadingBlankLine.YES,
AllowTrailingBlankLine.NO);
}
builder.close();
return null;
}
public void visitClassDeclaration(ClassTree node) {
sync(node);
List breaks = visitModifiers(
node.getModifiers(), Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty());
List extends Tree> permitsTypes = getPermitsClause(node);
boolean hasSuperclassType = node.getExtendsClause() != null;
boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
boolean hasPermitsTypes = !permitsTypes.isEmpty();
builder.addAll(breaks);
token(node.getKind() == Tree.Kind.INTERFACE ? "interface" : "class");
builder.space();
visit(node.getSimpleName());
if (!node.getTypeParameters().isEmpty()) {
token("<");
}
builder.open(plusFour);
{
if (!node.getTypeParameters().isEmpty()) {
typeParametersRest(
node.getTypeParameters(),
hasSuperclassType || hasSuperInterfaceTypes || hasPermitsTypes ? plusFour : ZERO);
}
if (hasSuperclassType) {
builder.breakToFill(" ");
token("extends");
builder.space();
scan(node.getExtendsClause(), null);
}
classDeclarationTypeList(
node.getKind() == Tree.Kind.INTERFACE ? "extends" : "implements", node.getImplementsClause());
classDeclarationTypeList("permits", permitsTypes);
}
builder.close();
if (node.getMembers() == null) {
token(";");
} else {
addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
}
dropEmptyDeclarations();
}
@Override
public Void visitTypeParameter(TypeParameterTree node, Void unused) {
sync(node);
builder.open(ZERO);
visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.YES);
visit(node.getName());
if (!node.getBounds().isEmpty()) {
builder.space();
token("extends");
builder.open(plusFour);
builder.breakOp(" ");
builder.open(plusFour);
boolean first = true;
for (Tree typeBound : node.getBounds()) {
if (!first) {
builder.breakToFill(" ");
token("&");
builder.space();
}
scan(typeBound, null);
first = false;
}
builder.close();
builder.close();
}
builder.close();
return null;
}
@Override
public Void visitUnionType(UnionTypeTree node, Void unused) {
throw new IllegalStateException("expected manual descent into union types");
}
@Override
public Void visitWhileLoop(WhileLoopTree node, Void unused) {
sync(node);
token("while");
builder.space();
token("(");
scan(skipParen(node.getCondition()), null);
token(")");
visitStatement(
node.getStatement(), CollapseEmptyOrNot.YES, AllowLeadingBlankLine.YES, AllowTrailingBlankLine.NO);
return null;
}
@Override
public Void visitWildcard(WildcardTree node, Void unused) {
sync(node);
builder.open(ZERO);
token("?");
if (node.getBound() != null) {
builder.open(plusFour);
builder.space();
token(node.getKind() == EXTENDS_WILDCARD ? "extends" : "super");
builder.breakOp(" ");
scan(node.getBound(), null);
builder.close();
}
builder.close();
return null;
}
// Helper methods.
/** Helper method for annotations. */
void visitAnnotations(List extends AnnotationTree> annotations, BreakOrNot breakBefore, BreakOrNot breakAfter) {
if (!annotations.isEmpty()) {
if (breakBefore.isYes()) {
builder.breakToFill(" ");
}
boolean first = true;
for (AnnotationTree annotation : annotations) {
if (!first) {
builder.breakToFill(" ");
}
scan(annotation, null);
first = false;
}
if (breakAfter.isYes()) {
builder.breakToFill(" ");
}
}
}
/** Helper method for blocks. */
protected void visitBlock(
BlockTree node,
CollapseEmptyOrNot collapseEmptyOrNot,
AllowLeadingBlankLine allowLeadingBlankLine,
AllowTrailingBlankLine allowTrailingBlankLine) {
sync(node);
if (node.isStatic()) {
token("static");
builder.space();
}
if (collapseEmptyOrNot.isYes() && node.getStatements().isEmpty()) {
if (builder.peekToken().equals(Optional.of(";"))) {
// TODO(cushon): is this needed?
token(";");
} else {
tokenBreakTrailingComment("{", plusTwo);
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusTwo);
}
} else {
builder.open(ZERO);
builder.open(plusTwo);
tokenBreakTrailingComment("{", plusTwo);
if (allowLeadingBlankLine == AllowLeadingBlankLine.NO) {
builder.blankLineWanted(BlankLineWanted.NO);
} else {
builder.blankLineWanted(BlankLineWanted.PRESERVE);
}
visitStatements(node.getStatements(), false);
builder.close();
builder.forcedBreak();
builder.close();
if (allowTrailingBlankLine == AllowTrailingBlankLine.NO) {
builder.blankLineWanted(BlankLineWanted.NO);
} else {
builder.blankLineWanted(BlankLineWanted.PRESERVE);
}
markForPartialFormat();
token("}", plusTwo);
}
}
/** Helper method for statements. */
private void visitStatement(
StatementTree node,
CollapseEmptyOrNot collapseEmptyOrNot,
AllowLeadingBlankLine allowLeadingBlank,
AllowTrailingBlankLine allowTrailingBlank) {
sync(node);
switch (node.getKind()) {
case BLOCK:
builder.space();
visitBlock((BlockTree) node, collapseEmptyOrNot, allowLeadingBlank, allowTrailingBlank);
break;
default:
builder.open(plusTwo);
builder.breakOp(" ");
scan(node, null);
builder.close();
}
}
protected void visitStatements(List extends StatementTree> statements, boolean inlineFirst) {
boolean first = true;
PeekingIterator it = Iterators.peekingIterator(statements.iterator());
dropEmptyDeclarations();
while (it.hasNext()) {
StatementTree tree = it.next();
if (!(inlineFirst && first)) {
builder.forcedBreak();
}
if (!first) {
builder.blankLineWanted(BlankLineWanted.PRESERVE);
}
markForPartialFormat();
first = false;
List fragments = variableFragments(it, tree);
if (!fragments.isEmpty()) {
visitVariables(
fragments,
DeclarationKind.NONE,
canLocalHaveHorizontalAnnotations(fragments.get(0).getModifiers()));
} else {
scan(tree, null);
}
}
}
/** Output combined modifiers and annotations and the trailing break. */
void visitAndBreakModifiers(
ModifiersTree modifiers, Direction annotationDirection, Optional declarationAnnotationBreak) {
builder.addAll(visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak));
}
@Override
public Void visitModifiers(ModifiersTree node, Void unused) {
throw new IllegalStateException("expected manual descent into modifiers");
}
/** Output combined modifiers and annotations and returns the trailing break. */
protected List visitModifiers(
ModifiersTree modifiersTree,
Direction annotationsDirection,
Optional declarationAnnotationBreak) {
return visitModifiers(modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak);
}
private List visitModifiers(
List extends AnnotationTree> annotationTrees,
Direction annotationsDirection,
Optional declarationAnnotationBreak) {
if (annotationTrees.isEmpty() && !nextIsModifier()) {
return EMPTY_LIST;
}
Deque annotations = new ArrayDeque<>(annotationTrees);
builder.open(ZERO);
boolean first = true;
boolean lastWasAnnotation = false;
while (!annotations.isEmpty()) {
if (nextIsModifier()) {
break;
}
if (!first) {
builder.addAll(
annotationsDirection.isVertical()
? forceBreakList(declarationAnnotationBreak)
: breakList(declarationAnnotationBreak));
}
scan(annotations.removeFirst(), null);
first = false;
lastWasAnnotation = true;
}
builder.close();
ImmutableList trailingBreak = annotationsDirection.isVertical()
? forceBreakList(declarationAnnotationBreak)
: breakList(declarationAnnotationBreak);
if (annotations.isEmpty() && !nextIsModifier()) {
return trailingBreak;
}
if (lastWasAnnotation) {
builder.addAll(trailingBreak);
}
builder.open(ZERO);
first = true;
while (nextIsModifier() || !annotations.isEmpty()) {
if (!first) {
builder.addAll(breakFillList(Optional.empty()));
}
if (nextIsModifier()) {
String currentToken = builder.peekToken().get();
token(currentToken);
if (currentToken.equals("non")) {
token(builder.peekToken().get());
token(builder.peekToken().get());
}
} else {
scan(annotations.removeFirst(), null);
lastWasAnnotation = true;
}
first = false;
}
builder.close();
return breakFillList(Optional.empty());
}
boolean nextIsModifier() {
switch (builder.peekToken().get()) {
case "public":
case "protected":
case "private":
case "abstract":
case "static":
case "final":
case "transient":
case "volatile":
case "synchronized":
case "native":
case "strictfp":
case "default":
case "sealed":
case "non":
case "-":
return true;
default:
return false;
}
}
@Override
public Void visitCatch(CatchTree node, Void unused) {
throw new IllegalStateException("expected manual descent into catch trees");
}
/** Helper method for {@link CatchTree}s. */
private void visitCatchClause(CatchTree node, AllowTrailingBlankLine allowTrailingBlankLine) {
sync(node);
builder.space();
token("catch");
builder.space();
token("(");
builder.open(plusFour);
VariableTree ex = node.getParameter();
if (ex.getType().getKind() == UNION_TYPE) {
builder.open(ZERO);
visitUnionType(ex);
builder.close();
} else {
// TODO(cushon): don't break after here for consistency with for, while, etc.
builder.breakToFill();
builder.open(ZERO);
scan(ex, null);
builder.close();
}
builder.close();
token(")");
builder.space();
visitBlock(node.getBlock(), CollapseEmptyOrNot.NO, AllowLeadingBlankLine.YES, allowTrailingBlankLine);
}
/** Formats a union type declaration in a catch clause. */
private void visitUnionType(VariableTree declaration) {
UnionTypeTree type = (UnionTypeTree) declaration.getType();
builder.open(ZERO);
sync(declaration);
visitAndBreakModifiers(
declaration.getModifiers(), Direction.HORIZONTAL, /* declarationAnnotationBreak= */ Optional.empty());
List extends Tree> union = type.getTypeAlternatives();
boolean first = true;
for (int i = 0; i < union.size() - 1; i++) {
if (!first) {
builder.breakOp(" ");
token("|");
builder.space();
} else {
first = false;
}
scan(union.get(i), null);
}
builder.breakOp(" ");
token("|");
builder.space();
Tree last = union.get(union.size() - 1);
declareOne(
DeclarationKind.NONE,
Direction.HORIZONTAL,
/* modifiers= */ Optional.empty(),
last,
/* name= */ declaration.getName(),
/* op= */ "",
"=",
Optional.ofNullable(declaration.getInitializer()),
/* trailing= */ Optional.empty(),
/* receiverExpression= */ Optional.empty(),
/* typeWithDims= */ Optional.empty());
builder.close();
}
/** Accumulate the operands and operators. */
private static void walkInfix(
int precedence, ExpressionTree expression, List operands, List operators) {
if (expression instanceof BinaryTree) {
BinaryTree binaryTree = (BinaryTree) expression;
if (precedence(binaryTree) == precedence) {
walkInfix(precedence, binaryTree.getLeftOperand(), operands, operators);
operators.add(operatorName(expression));
walkInfix(precedence, binaryTree.getRightOperand(), operands, operators);
} else {
operands.add(expression);
}
} else {
operands.add(expression);
}
}
protected void visitFormals(Optional receiver, List extends VariableTree> parameters) {
if (!receiver.isPresent() && parameters.isEmpty()) {
return;
}
builder.open(ZERO);
boolean first = true;
if (receiver.isPresent()) {
// TODO(jdd): Use builders.
declareOne(
DeclarationKind.PARAMETER,
Direction.HORIZONTAL,
Optional.of(receiver.get().getModifiers()),
receiver.get().getType(),
/* name= */ receiver.get().getName(),
"",
"",
/* initializer= */ Optional.empty(),
!parameters.isEmpty() ? Optional.of(",") : Optional.empty(),
Optional.of(receiver.get().getNameExpression()),
/* typeWithDims= */ Optional.empty());
first = false;
}
for (int i = 0; i < parameters.size(); i++) {
VariableTree parameter = parameters.get(i);
if (!first) {
builder.breakOp(" ");
}
visitToDeclare(
DeclarationKind.PARAMETER,
Direction.HORIZONTAL,
parameter,
/* initializer= */ Optional.empty(),
"=",
i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty());
first = false;
}
builder.close();
}
// /** Helper method for {@link MethodDeclaration}s. */
private void visitThrowsClause(List extends ExpressionTree> thrownExceptionTypes) {
token("throws");
builder.breakToFill(" ");
boolean first = true;
for (ExpressionTree thrownExceptionType : thrownExceptionTypes) {
if (!first) {
token(",");
builder.breakToFill(" ");
}
scan(thrownExceptionType, null);
first = false;
}
}
@Override
public Void visitIdentifier(IdentifierTree node, Void unused) {
sync(node);
token(node.getName().toString());
return null;
}
@Override
public Void visitModule(ModuleTree node, Void unused) {
for (AnnotationTree annotation : node.getAnnotations()) {
scan(annotation, null);
builder.forcedBreak();
}
if (node.getModuleType() == ModuleTree.ModuleKind.OPEN) {
token("open");
builder.space();
}
token("module");
builder.space();
scan(node.getName(), null);
builder.space();
if (node.getDirectives().isEmpty()) {
tokenBreakTrailingComment("{", plusTwo);
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusTwo);
} else {
builder.open(plusTwo);
token("{");
builder.forcedBreak();
Optional previousDirective = Optional.empty();
for (DirectiveTree directiveTree : node.getDirectives()) {
markForPartialFormat();
builder.blankLineWanted(
previousDirective
.map(k -> !k.equals(directiveTree.getKind()))
.orElse(false)
? BlankLineWanted.YES
: BlankLineWanted.NO);
builder.forcedBreak();
scan(directiveTree, null);
previousDirective = Optional.of(directiveTree.getKind());
}
builder.close();
builder.forcedBreak();
token("}");
}
return null;
}
private void visitDirective(
String name,
String separator,
ExpressionTree nameExpression,
@Nullable List extends ExpressionTree> items) {
token(name);
builder.space();
scan(nameExpression, null);
if (items != null) {
builder.open(plusFour);
builder.space();
token(separator);
builder.forcedBreak();
boolean first = true;
for (ExpressionTree item : items) {
if (!first) {
token(",");
builder.forcedBreak();
}
scan(item, null);
first = false;
}
token(";");
builder.close();
} else {
token(";");
}
}
@Override
public Void visitExports(ExportsTree node, Void unused) {
visitDirective("exports", "to", node.getPackageName(), node.getModuleNames());
return null;
}
@Override
public Void visitOpens(OpensTree node, Void unused) {
visitDirective("opens", "to", node.getPackageName(), node.getModuleNames());
return null;
}
@Override
public Void visitProvides(ProvidesTree node, Void unused) {
visitDirective("provides", "with", node.getServiceName(), node.getImplementationNames());
return null;
}
@Override
public Void visitRequires(RequiresTree node, Void unused) {
token("requires");
builder.space();
while (true) {
if (builder.peekToken().equals(Optional.of("static"))) {
token("static");
builder.space();
} else if (builder.peekToken().equals(Optional.of("transitive"))) {
token("transitive");
builder.space();
} else {
break;
}
}
scan(node.getModuleName(), null);
token(";");
return null;
}
@Override
public Void visitUses(UsesTree node, Void unused) {
token("uses");
builder.space();
scan(node.getServiceName(), null);
token(";");
return null;
}
/** Helper method for import declarations, names, and qualified names. */
private void visitName(Tree node) {
Deque stack = new ArrayDeque<>();
for (; node instanceof MemberSelectTree; node = ((MemberSelectTree) node).getExpression()) {
stack.addFirst(((MemberSelectTree) node).getIdentifier());
}
stack.addFirst(((IdentifierTree) node).getName());
boolean first = true;
for (Name name : stack) {
if (!first) {
token(".");
}
token(name.toString());
first = false;
}
}
private void visitToDeclare(
DeclarationKind kind,
Direction annotationsDirection,
VariableTree node,
Optional initializer,
String equals,
Optional trailing) {
sync(node);
declareOne(
kind,
annotationsDirection,
Optional.of(node.getModifiers()),
node.getType(),
node.getName(),
"",
equals,
initializer,
trailing,
/* receiverExpression= */ Optional.empty(),
/* typeWithDims= */ Optional.empty());
}
/** Does not omit the leading '<', which should be associated with the type name. */
protected void typeParametersRest(List extends TypeParameterTree> typeParameters, Indent plusIndent) {
builder.open(plusIndent);
builder.breakOp();
builder.open(ZERO);
boolean first = true;
for (TypeParameterTree typeParameter : typeParameters) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(typeParameter, null);
first = false;
}
token(">");
builder.close();
builder.close();
}
/**
* Output a "." node.
*
* @param node0 the "." node
*/
void visitDot(ExpressionTree node0) {
ExpressionTree node = node0;
// collect a flattened list of "."-separated items
// e.g. ImmutableList.builder().add(1).build() -> [ImmutableList, builder(), add(1), build()]
Deque stack = new ArrayDeque<>();
LOOP:
do {
stack.addFirst(node);
if (node.getKind() == ARRAY_ACCESS) {
node = getArrayBase(node);
}
switch (node.getKind()) {
case MEMBER_SELECT:
node = ((MemberSelectTree) node).getExpression();
break;
case METHOD_INVOCATION:
node = getMethodReceiver((MethodInvocationTree) node);
break;
case IDENTIFIER:
node = null;
break LOOP;
default:
// If the dot chain starts with a primary expression
// (e.g. a class instance creation, or a conditional expression)
// then remove it from the list and deal with it first.
node = stack.removeFirst();
break LOOP;
}
} while (node != null);
List items = new ArrayList<>(stack);
boolean needDot = false;
// The dot chain started with a primary expression: output it normally, and indent
// the rest of the chain +4.
if (node != null) {
// Exception: if it's an anonymous class declaration, we don't need to
// break and indent after the trailing '}'.
if (node.getKind() == NEW_CLASS && ((NewClassTree) node).getClassBody() != null) {
builder.open(ZERO);
scan(getArrayBase(node), null);
token(".");
} else {
builder.open(OpenOp.builder()
.debugName("visitDot")
.plusIndent(plusFour)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(true))
.breakabilityIfLastLevel(
LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER)
.columnLimitBeforeLastBreak(METHOD_CHAIN_COLUMN_LIMIT)
.isSimple(false)
.build());
scan(getArrayBase(node), null);
builder.breakOp();
needDot = true;
}
formatArrayIndices(getArrayIndices(node));
if (stack.isEmpty()) {
builder.close();
return;
}
}
Set prefixes = new LinkedHashSet<>();
// Check if the dot chain has a prefix that looks like a type name, so we can
// treat the type name-shaped part as a single syntactic unit.
TypeNameClassifier.typePrefixLength(simpleNames(stack)).ifPresent(prefixes::add);
int invocationCount = 0;
int firstInvocationIndex = -1;
{
for (int i = 0; i < items.size(); i++) {
ExpressionTree expression = items.get(i);
if (expression.getKind() == METHOD_INVOCATION) {
if (i > 0 || node != null) {
// we only want dereference invocations
invocationCount++;
}
if (firstInvocationIndex < 0) {
firstInvocationIndex = i;
}
}
}
}
// If there's only one invocation, treat leading field accesses as a single
// unit. In the normal case we want to preserve the alignment of subsequent
// method calls, and would emit e.g.:
//
// myField
// .foo()
// .bar();
//
// But if there's no 'bar()' to worry about the alignment of we prefer:
//
// myField.foo();
//
// to:
//
// myField
// .foo();
//
if (invocationCount == 1 && firstInvocationIndex > 0) {
prefixes.add(firstInvocationIndex);
}
if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) {
switch (((IdentifierTree) items.get(0)).getName().toString()) {
case "this":
case "super":
prefixes.add(1);
break;
default:
break;
}
}
List streamPrefixes = handleStream(items);
streamPrefixes.forEach(x -> prefixes.add(x.intValue()));
if (!prefixes.isEmpty()) {
visitDotWithPrefix(items, needDot, prefixes, streamPrefixes.isEmpty() ? INDEPENDENT : UNIFIED);
} else {
visitRegularDot(items, needDot);
}
if (node != null) {
builder.close();
}
}
/**
* Output a "regular" chain of dereferences, possibly in builder-style. Break before every dot.
*
* @param items in the chain
* @param needDot whether a leading dot is needed
*/
private void visitRegularDot(List items, boolean needDot) {
boolean trailingDereferences = items.size() > 1;
boolean needDot0 = needDot;
if (!needDot0) {
// Verified `preferBreakIfLastLevel = true` is good here, see B20128760.
// This level can come after either:
// * checkArgument( -- B19950815 (!trailingDereferences)
// * foo.bar() -- palantir-chains-lambdas ( trailingDereferences)
builder.open(OpenOp.builder()
.debugName("visitRegularDot")
.plusIndent(plusFour)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(false))
.breakabilityIfLastLevel(LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER)
.columnLimitBeforeLastBreak(METHOD_CHAIN_COLUMN_LIMIT)
.isSimple(!trailingDereferences)
.build());
}
// don't break after the first element if it is every small, unless the
// chain starts with another expression
int minLength = indentMultiplier * 4;
int length = needDot0 ? minLength : 0;
for (ExpressionTree e : items) {
if (needDot) {
if (length > minLength) {
builder.breakOp(Break.builder()
.fillMode(FillMode.UNIFIED)
.flat("")
.plusIndent(ZERO)
.hasColumnLimit(shouldHaveColumnLimit(e))
.build());
}
token(".");
length++;
}
if (!fillFirstArgument(e, items, trailingDereferences ? ZERO : minusFour)) {
BreakTag tyargTag = new BreakTag();
dotExpressionUpToArgs(e, Optional.of(tyargTag));
Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
dotExpressionArgsAndParen(e, tyargIndent, (trailingDereferences || needDot) ? plusFour : ZERO);
}
length += getLength(e, getCurrentPath());
needDot = true;
}
if (!needDot0) {
builder.close();
}
}
// avoid formattings like:
//
// when(
// something
// .happens())
// .thenReturn(result);
//
private boolean fillFirstArgument(ExpressionTree e, List items, Indent indent) {
// is there a trailing dereference?
if (items.size() < 2) {
return false;
}
// don't special-case calls nested inside expressions
if (e.getKind() != METHOD_INVOCATION) {
return false;
}
MethodInvocationTree methodInvocation = (MethodInvocationTree) e;
Name name = getMethodName(methodInvocation);
if (!(methodInvocation.getMethodSelect() instanceof IdentifierTree)
|| name.length() > 4
|| !methodInvocation.getTypeArguments().isEmpty()
|| methodInvocation.getArguments().size() != 1) {
return false;
}
builder.open(ZERO);
builder.open(indent);
visit(name);
token("(");
ExpressionTree arg = getOnlyElement(methodInvocation.getArguments());
scan(arg, null);
builder.close();
token(")");
builder.close();
return true;
}
/**
* Output a chain of dereferences where some prefix should be treated as a single syntactic unit, either because it
* looks like a type name or because there is only a single method invocation in the chain.
*
* @param items in the chain
* @param needDot whether a leading dot is needed
* @param prefixes the terminal indices of 'prefixes' of the expression that should be treated as a syntactic unit
*/
private void visitDotWithPrefix(
List items, boolean needDot, Collection prefixes, FillMode prefixFillMode) {
// Are there method invocations or field accesses after the prefix?
boolean trailingDereferences = !prefixes.isEmpty() && getLast(prefixes) < items.size() - 1;
boolean hasMethodInvocations = items.stream().anyMatch(expr -> expr.getKind() == METHOD_INVOCATION);
builder.open(OpenOp.builder()
.debugName("visitDotWithPrefix")
.plusIndent(plusFour)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(false))
.breakabilityIfLastLevel(
hasMethodInvocations
? LastLevelBreakability.ACCEPT_INLINE_CHAIN_IF_SIMPLE_OTHERWISE_CHECK_INNER
: LastLevelBreakability.CHECK_INNER)
.partialInlineability(PartialInlineability.IF_FIRST_LEVEL_FITS)
.columnLimitBeforeLastBreak(METHOD_CHAIN_COLUMN_LIMIT)
.isSimple(!trailingDereferences)
.build());
for (int times = 0; times < prefixes.size(); times++) {
builder.open("prefix " + times, ZERO);
}
Deque unconsumedPrefixes = new ArrayDeque<>(ImmutableSortedSet.copyOf(prefixes));
BreakTag nameTag = new BreakTag();
for (int i = 0; i < items.size(); i++) {
ExpressionTree e = items.get(i);
if (needDot) {
FillMode fillMode;
if (!unconsumedPrefixes.isEmpty() && i <= unconsumedPrefixes.peekFirst()) {
fillMode = prefixFillMode;
} else {
fillMode = FillMode.UNIFIED;
}
builder.breakOp(Break.builder()
.fillMode(fillMode)
.flat("")
.plusIndent(ZERO)
.optTag(Optional.of(nameTag))
.hasColumnLimit(shouldHaveColumnLimit(e))
.build());
token(".");
}
BreakTag tyargTag = new BreakTag();
dotExpressionUpToArgs(e, Optional.of(tyargTag));
if (!unconsumedPrefixes.isEmpty() && i == unconsumedPrefixes.peekFirst()) {
builder.close();
unconsumedPrefixes.removeFirst();
}
Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
Indent argsIndent = Indent.If.make(nameTag, plusFour, trailingDereferences ? plusFour : ZERO);
dotExpressionArgsAndParen(e, tyargIndent, argsIndent);
needDot = true;
}
builder.close();
}
/**
* We only want to limit the max column of expressions that are method invocations. All of these expressions below
* would have a column limit.
*
*
* - {@code foo().bar()}
*
- {@code foo().bar()[0]}
*
- {@code foo().bar()[0][0]}
*
*
* Whereas an expression like a name {@code com.palantir.foo.bar.Baz} would not.
*/
private boolean shouldHaveColumnLimit(ExpressionTree expr) {
return getArrayBase(expr).getKind() == METHOD_INVOCATION;
}
/** Returns the simple names of expressions in a "." chain. */
private List simpleNames(Deque stack) {
ImmutableList.Builder simpleNames = ImmutableList.builder();
OUTER:
for (ExpressionTree expression : stack) {
boolean isArray = expression.getKind() == ARRAY_ACCESS;
expression = getArrayBase(expression);
switch (expression.getKind()) {
case MEMBER_SELECT:
simpleNames.add(
((MemberSelectTree) expression).getIdentifier().toString());
break;
case IDENTIFIER:
simpleNames.add(((IdentifierTree) expression).getName().toString());
break;
case METHOD_INVOCATION:
simpleNames.add(
getMethodName((MethodInvocationTree) expression).toString());
break OUTER;
default:
break OUTER;
}
if (isArray) {
break OUTER;
}
}
return simpleNames.build();
}
private void dotExpressionUpToArgs(ExpressionTree expression, Optional tyargTag) {
expression = getArrayBase(expression);
switch (expression.getKind()) {
case MEMBER_SELECT:
MemberSelectTree fieldAccess = (MemberSelectTree) expression;
visit(fieldAccess.getIdentifier());
break;
case METHOD_INVOCATION:
MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
if (!methodInvocation.getTypeArguments().isEmpty()) {
builder.open(plusFour);
addTypeArguments(methodInvocation.getTypeArguments(), ZERO);
// TODO(jdd): Should indent the name -4.
builder.breakOp(FillMode.UNIFIED, "", ZERO, tyargTag);
builder.close();
}
visit(getMethodName(methodInvocation));
break;
case IDENTIFIER:
visit(((IdentifierTree) expression).getName());
break;
default:
scan(expression, null);
break;
}
}
/** Returns the base expression of an erray access, e.g. given {@code foo[0][0]} returns {@code foo}. */
private ExpressionTree getArrayBase(ExpressionTree node) {
while (node instanceof ArrayAccessTree) {
node = ((ArrayAccessTree) node).getExpression();
}
return node;
}
private ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
ExpressionTree select = methodInvocation.getMethodSelect();
return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
}
private void dotExpressionArgsAndParen(ExpressionTree expression, Indent tyargIndent, Indent indent) {
Deque indices = getArrayIndices(expression);
expression = getArrayBase(expression);
switch (expression.getKind()) {
case METHOD_INVOCATION:
// Note: we don't BREAK_HERE because we want to make sure that the last argument is actually
// breakable in a way we prefer.
builder.open(OpenOp.builder()
.plusIndent(tyargIndent)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(true))
.breakabilityIfLastLevel(LastLevelBreakability.CHECK_INNER)
.debugName("dotExpressionArgsAndParen")
.build());
MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
addArguments(methodInvocation.getArguments(), indent);
builder.close();
break;
default:
break;
}
formatArrayIndices(indices);
}
/** Lays out one or more array indices. Does not output the expression for the array itself. */
private void formatArrayIndices(Deque indices) {
if (indices.isEmpty()) {
return;
}
builder.open(ZERO);
do {
token("[");
builder.breakToFill();
scan(indices.removeLast(), null);
token("]");
} while (!indices.isEmpty());
builder.close();
}
/**
* Returns all array indices for the given expression, e.g. given {@code foo[0][0]} returns the expressions for
* {@code [0][0]}.
*/
private Deque getArrayIndices(ExpressionTree expression) {
Deque indices = new ArrayDeque<>();
while (expression instanceof ArrayAccessTree) {
ArrayAccessTree array = (ArrayAccessTree) expression;
indices.addLast(array.getIndex());
expression = array.getExpression();
}
return indices;
}
/** Helper methods for method invocations. */
void addTypeArguments(List extends Tree> typeArguments, Indent plusIndent) {
if (typeArguments == null || typeArguments.isEmpty()) {
return;
}
token("<");
builder.open(plusIndent);
boolean first = true;
for (Tree typeArgument : typeArguments) {
if (!first) {
token(",");
builder.breakToFill(" ");
}
scan(typeArgument, null);
first = false;
}
builder.close();
token(">");
}
/**
* Add arguments to a method invocation, etc. The arguments indented {@code plusFour}, filled, from the current
* indent. The arguments may be output two at a time if they seem to be arguments to a map constructor, etc.
*
* @param arguments the arguments
* @param plusIndent the extra indent for the arguments
*/
void addArguments(List extends ExpressionTree> arguments, Indent plusIndent) {
/*
`preferBreakingLastInnerLevel` here in order to avoid immediately breaking a long
invocation that can be one-lined:
.method(
arg1, arg2, param -> {
// body
});
This is so that the downstream 'preferBreakingLastInnerLevel' level made by
argList can be attempted without preemptively breaking after the opening bracket '('.
`breakThisLevel` would break B20701054 ( `analysis().analyze(⏎` )
However we definitely don't wanna look inside for B18479811
Solution: argList should CHECK_INNER.
*/
builder.open(OpenOp.builder()
.debugName("addArguments")
.plusIndent(plusIndent)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(false))
.breakabilityIfLastLevel(LastLevelBreakability.CHECK_INNER)
.isSimple(arguments.size() <= 1)
.build());
token("(");
if (!arguments.isEmpty()) {
if (arguments.size() % 2 == 0 && argumentsAreTabular(arguments) == 2) {
builder.forcedBreak();
builder.open(ZERO);
boolean first = true;
for (int i = 0; i < arguments.size() - 1; i += 2) {
ExpressionTree argument0 = arguments.get(i);
ExpressionTree argument1 = arguments.get(i + 1);
if (!first) {
token(",");
builder.forcedBreak();
}
builder.open(plusFour);
scan(argument0, null);
token(",");
builder.breakOp(" ");
scan(argument1, null);
builder.close();
first = false;
}
builder.close();
} else if (isFormatMethod(arguments)) {
builder.breakOp();
builder.open(ZERO);
scan(arguments.get(0), null);
token(",");
builder.breakOp(" ");
builder.open(ZERO);
argList(arguments.subList(1, arguments.size()));
builder.close();
builder.close();
} else {
builder.breakOp();
argList(arguments);
}
}
token(")");
builder.close();
}
private void argList(List extends ExpressionTree> arguments) {
builder.open(OpenOp.builder()
.debugName("argList")
.plusIndent(ZERO)
.breakBehaviour(BreakBehaviours.preferBreakingLastInnerLevel(true))
.breakabilityIfLastLevel(LastLevelBreakability.CHECK_INNER)
.isSimple(arguments.size() <= 1)
.build());
boolean first = true;
FillMode fillMode = hasOnlyShortItems(arguments) ? FillMode.INDEPENDENT : FillMode.UNIFIED;
for (ExpressionTree argument : arguments) {
if (!first) {
token(",");
builder.breakOp(fillMode, " ", ZERO);
}
scan(argument, null);
first = false;
}
builder.close();
}
/**
* Identifies String formatting methods like {@link String#format} which we prefer to format as:
*
* {@code
* String.format(
* "the format string: %s %s %s",
* arg, arg, arg);
* }
*
* And not:
*
*
{@code
* String.format(
* "the format string: %s %s %s",
* arg,
* arg,
* arg);
* }
*/
private boolean isFormatMethod(List extends ExpressionTree> arguments) {
if (arguments.size() < 2) {
return false;
}
return isFormatString(arguments.get(0));
}
private static final Pattern FORMAT_SPECIFIER = Pattern.compile("%|\\{[0-9]\\}");
private boolean isStringConcat(ExpressionTree first) {
final boolean[] stringConcat = {false};
new TreeScanner() {
@Override
public void scan(JCTree tree) {
if (tree == null) {
return;
}
switch (tree.getKind()) {
case STRING_LITERAL:
stringConcat[0] = true;
break;
case PLUS:
super.scan(tree);
break;
default:
break;
}
}
}.scan((JCTree) first);
return stringConcat[0];
}
private boolean isFormatString(ExpressionTree first) {
final boolean[] stringLiteral = {true};
final boolean[] formatString = {false};
new TreeScanner() {
@Override
public void scan(JCTree tree) {
if (tree == null) {
return;
}
switch (tree.getKind()) {
case STRING_LITERAL:
break;
case PLUS:
super.scan(tree);
break;
default:
stringLiteral[0] = false;
break;
}
if (tree.getKind() == STRING_LITERAL) {
Object value = ((LiteralTree) tree).getValue();
if (value instanceof String
&& FORMAT_SPECIFIER.matcher(value.toString()).find()) {
formatString[0] = true;
}
}
}
}.scan((JCTree) first);
return stringLiteral[0] && formatString[0];
}
/** Returns the number of columns if the arguments arg laid out in a grid, or else {@code -1}. */
private int argumentsAreTabular(List extends ExpressionTree> arguments) {
if (arguments.isEmpty()) {
return -1;
}
List> rows = new ArrayList<>();
PeekingIterator it = Iterators.peekingIterator(arguments.iterator());
int start0 = actualColumn(it.peek());
{
List row = new ArrayList<>();
row.add(it.next());
while (it.hasNext() && actualColumn(it.peek()) > start0) {
row.add(it.next());
}
if (!it.hasNext()) {
return -1;
}
if (rowLength(row) <= 1) {
return -1;
}
rows.add(row);
}
while (it.hasNext()) {
List row = new ArrayList<>();
int start = actualColumn(it.peek());
if (start != start0) {
return -1;
}
row.add(it.next());
while (it.hasNext() && actualColumn(it.peek()) > start0) {
row.add(it.next());
}
rows.add(row);
}
int size0 = rows.get(0).size();
if (!expressionsAreParallel(rows, 0, rows.size())) {
return -1;
}
for (int i = 1; i < size0; i++) {
if (!expressionsAreParallel(rows, i, rows.size() / 2 + 1)) {
return -1;
}
}
// if there are only two rows, they must be the same length
if (rows.size() == 2) {
if (size0 == rows.get(1).size()) {
return size0;
}
return -1;
}
// allow a ragged trailing row for >= 3 columns
for (int i = 1; i < rows.size() - 1; i++) {
if (size0 != rows.get(i).size()) {
return -1;
}
}
if (size0 < getLast(rows).size()) {
return -1;
}
return size0;
}
static int rowLength(List extends ExpressionTree> row) {
int size = 0;
for (ExpressionTree tree : row) {
if (tree.getKind() != NEW_ARRAY) {
size++;
continue;
}
NewArrayTree array = (NewArrayTree) tree;
if (array.getInitializers() == null) {
size++;
continue;
}
size += rowLength(array.getInitializers());
}
return size;
}
private Integer actualColumn(ExpressionTree expression) {
Map positionToColumnMap = builder.getInput().getPositionToColumnMap();
return positionToColumnMap.get(builder.actualStartColumn(getStartPosition(expression)));
}
/** How many lines does this node take up in the input. Returns at least 1. */
int lineSpan(Tree node) {
ImmutableRangeMap positionTokenMap =
builder.getInput().getPositionTokenMap();
int startPosition = getStartPosition(node);
int endPosition = getEndPosition(node, getCurrentPath());
// The last token will not be in the range map if it's whitespace, because of JavaOutput.endTok's filtering.
// Thus, we go back until we find a "real" last token.
// If all tokens down from endPosition are null, then we are guaranteed stop at startPosition.
while (endPosition > startPosition && positionTokenMap.get(endPosition) == null) {
endPosition--;
}
Input.Token startToken = positionTokenMap.get(startPosition);
Input.Token endToken = positionTokenMap.get(endPosition);
return lineNumberAt(endToken) - lineNumberAt(startToken) + 1;
}
private int lineNumberAt(Input.Token token) {
return builder.getInput().getLineNumber(token.getTok().getPosition());
}
/** Returns true if {@code atLeastM} of the expressions in the given column are the same kind. */
private static boolean expressionsAreParallel(List> rows, int column, int atLeastM) {
Multiset nodeTypes = HashMultiset.create();
for (List extends ExpressionTree> row : rows) {
if (column >= row.size()) {
continue;
}
nodeTypes.add(row.get(column).getKind());
}
for (Multiset.Entry nodeType : nodeTypes.entrySet()) {
if (nodeType.getCount() >= atLeastM) {
return true;
}
}
return false;
}
// General helper functions.
enum DeclarationKind {
NONE,
FIELD,
PARAMETER
}
/** Declare one variable or variable-like thing. */
@SuppressWarnings("TooManyArguments")
int declareOne(
DeclarationKind kind,
Direction annotationsDirection,
Optional modifiers,
Tree type,
Name name,
String op,
String equals,
Optional initializer,
Optional trailing,
Optional receiverExpression,
Optional typeWithDims) {
BreakTag typeBreak = new BreakTag();
BreakTag verticalAnnotationBreak = new BreakTag();
// If the node is a field declaration, try to output any declaration
// annotations in-line. If the entire declaration doesn't fit on a single
// line, fall back to one-per-line.
boolean isField = kind == DeclarationKind.FIELD;
if (isField) {
builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
}
Deque> dims =
new ArrayDeque<>(typeWithDims.isPresent() ? typeWithDims.get().dims : Collections.emptyList());
int baseDims = 0;
builder.open(
kind == DeclarationKind.PARAMETER
&& (modifiers.isPresent()
&& !modifiers.get().getAnnotations().isEmpty())
? plusFour
: ZERO);
{
if (modifiers.isPresent()) {
visitAndBreakModifiers(modifiers.get(), annotationsDirection, Optional.of(verticalAnnotationBreak));
}
boolean isVar = builder.peekToken().get().equals("var")
&& (!name.contentEquals("var") || builder.peekToken(1).get().equals("var"));
boolean hasType = type != null || isVar;
builder.open(hasType ? plusFour : ZERO);
{
builder.open(ZERO);
{
builder.open(ZERO);
{
if (typeWithDims.isPresent() && typeWithDims.get().node != null) {
scan(typeWithDims.get().node, null);
int totalDims = dims.size();
builder.open(plusFour);
maybeAddDims(dims);
builder.close();
baseDims = totalDims - dims.size();
} else if (isVar) {
token("var");
} else {
scan(type, null);
}
}
builder.close();
if (hasType) {
builder.breakOp(FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak));
}
// conditionally ident the name and initializer +4 if the type spans
// multiple lines
builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
if (receiverExpression.isPresent()) {
scan(receiverExpression.get(), null);
} else {
visit(name);
}
builder.op(op);
}
maybeAddDims(dims);
builder.close();
}
builder.close();
if (initializer.isPresent()) {
builder.space();
token(equals);
if (initializer.get().getKind() == Tree.Kind.NEW_ARRAY
&& ((NewArrayTree) initializer.get()).getType() == null) {
builder.open(minusFour);
builder.space();
initializer.get().accept(this, null);
builder.close();
} else {
if (builder.peekToken().get().equals("switch")) {
// TODO(fawind): Don't break switch expression assignment
builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
} else {
builder.open(
Indent.If.make(typeBreak, plusFour, ZERO),
BreakBehaviours.breakOnlyIfInnerLevelsThenFitOnOneLine(true),
LastLevelBreakability.ABORT);
}
{
builder.breakToFill(" ");
scan(initializer.get(), null);
}
builder.close();
}
}
if (trailing.isPresent() && builder.peekToken().equals(trailing)) {
builder.guessToken(trailing.get());
}
// end of conditional name and initializer indent
builder.close();
}
builder.close();
if (isField) {
builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
}
return baseDims;
}
private void maybeAddDims(Deque> annotations) {
maybeAddDims(new ArrayDeque<>(), annotations);
}
/**
* The compiler does not always preserve the concrete syntax of annotated array dimensions, and mixed-notation array
* dimensions. Use look-ahead to preserve the original syntax.
*
* It is assumed that any number of regular dimension specifiers ({@code []} with no annotations) may be present
* in the input.
*
* @param dimExpressions an ordered list of dimension expressions (e.g. the {@code 0} in {@code new int[0]}
* @param annotations an ordered list of type annotations grouped by dimension (e.g. {@code [[@A, @B], [@C]]} for
* {@code int @A [] @B @C []}
*/
private void maybeAddDims(Deque dimExpressions, Deque> annotations) {
boolean lastWasAnnotation = false;
while (builder.peekToken().isPresent()) {
switch (builder.peekToken().get()) {
case "@":
if (annotations.isEmpty()) {
return;
}
List extends AnnotationTree> dimAnnotations = annotations.removeFirst();
if (dimAnnotations.isEmpty()) {
continue;
}
builder.breakToFill(" ");
visitAnnotations(dimAnnotations, BreakOrNot.NO, BreakOrNot.NO);
lastWasAnnotation = true;
break;
case "[":
if (lastWasAnnotation) {
builder.breakToFill(" ");
} else {
builder.breakToFill();
}
token("[");
if (!builder.peekToken().get().equals("]")) {
scan(dimExpressions.removeFirst(), null);
}
token("]");
lastWasAnnotation = false;
break;
case ".":
if (!builder.peekToken().get().equals(".")
|| !builder.peekToken(1).get().equals(".")) {
return;
}
if (lastWasAnnotation) {
builder.breakToFill(" ");
} else {
builder.breakToFill();
}
builder.op("...");
lastWasAnnotation = false;
break;
default:
return;
}
}
}
private void declareMany(List fragments, Direction annotationDirection) {
builder.open(ZERO);
ModifiersTree modifiers = fragments.get(0).getModifiers();
Tree type = fragments.get(0).getType();
visitAndBreakModifiers(modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.empty());
builder.open(plusFour);
builder.open(ZERO);
TypeWithDims extractedDims = DimensionHelpers.extractDims(type, SortedDims.YES);
Deque> dims = new ArrayDeque<>(extractedDims.dims);
scan(extractedDims.node, null);
int baseDims = dims.size();
maybeAddDims(dims);
baseDims = baseDims - dims.size();
boolean first = true;
for (VariableTree fragment : fragments) {
if (!first) {
token(",");
}
TypeWithDims fragmentDims = variableFragmentDims(first, baseDims, fragment.getType());
dims = new ArrayDeque<>(fragmentDims.dims);
builder.breakOp(" ");
builder.open(ZERO);
maybeAddDims(dims);
visit(fragment.getName());
maybeAddDims(dims);
ExpressionTree initializer = fragment.getInitializer();
if (initializer != null) {
builder.space();
token("=");
builder.open(plusFour);
builder.breakOp(" ");
scan(initializer, null);
builder.close();
}
builder.close();
if (first) {
builder.close();
}
first = false;
}
builder.close();
token(";");
builder.close();
}
/** Add a list of declarations. */
protected void addBodyDeclarations(
List extends Tree> bodyDeclarations, BracesOrNot braces, FirstDeclarationsOrNot first0) {
if (bodyDeclarations.isEmpty()) {
if (braces.isYes()) {
builder.space();
tokenBreakTrailingComment("{", plusTwo);
builder.blankLineWanted(BlankLineWanted.NO);
builder.open(ZERO);
token("}", plusTwo);
builder.close();
}
} else {
if (braces.isYes()) {
builder.space();
tokenBreakTrailingComment("{", plusTwo);
builder.open(ZERO, BreakBehaviours.breakThisLevel(), LastLevelBreakability.ACCEPT_INLINE_CHAIN);
}
builder.open(plusTwo);
boolean first = first0.isYes();
boolean lastOneGotBlankLineBefore = false;
PeekingIterator it = Iterators.peekingIterator(bodyDeclarations.iterator());
while (it.hasNext()) {
Tree bodyDeclaration = it.next();
dropEmptyDeclarations();
builder.forcedBreak();
boolean thisOneGetsBlankLineBefore =
bodyDeclaration.getKind() != VARIABLE || hasJavaDoc(bodyDeclaration);
if (first) {
builder.blankLineWanted(PRESERVE);
} else if (!first && (thisOneGetsBlankLineBefore || lastOneGotBlankLineBefore)) {
builder.blankLineWanted(YES);
}
markForPartialFormat();
if (bodyDeclaration.getKind() == VARIABLE) {
visitVariables(
variableFragments(it, bodyDeclaration),
DeclarationKind.FIELD,
// We always want field annotations to be vertical
Direction.VERTICAL);
} else {
scan(bodyDeclaration, null);
}
first = false;
lastOneGotBlankLineBefore = thisOneGetsBlankLineBefore;
}
dropEmptyDeclarations();
builder.forcedBreak();
builder.close();
builder.forcedBreak();
markForPartialFormat();
if (braces.isYes()) {
builder.blankLineWanted(BlankLineWanted.NO);
token("}", plusTwo);
builder.close();
}
}
}
/** Gets the permits clause for the given node. This is only available in Java 15 and later. */
protected List extends Tree> getPermitsClause(ClassTree node) {
return ImmutableList.of();
}
private void classDeclarationTypeList(String token, List extends Tree> types) {
if (types.isEmpty()) {
return;
}
builder.breakToFill(" ");
builder.open(types.size() > 1 ? plusFour : ZERO);
token(token);
builder.space();
boolean first = true;
for (Tree type : types) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(type, null);
first = false;
}
builder.close();
}
/**
* The parser expands multi-variable declarations into separate single-variable declarations. All of the fragments
* in the original declaration have the same start position, so we use that as a signal to collect them and preserve
* the multi-variable declaration in the output.
*
* e.g. {@code int x, y;} is parsed as {@code int x; int y;}.
*/
private List variableFragments(PeekingIterator extends Tree> it, Tree first) {
List fragments = new ArrayList<>();
if (first.getKind() == VARIABLE) {
int start = getStartPosition(first);
fragments.add((VariableTree) first);
while (it.hasNext() && it.peek().getKind() == VARIABLE && getStartPosition(it.peek()) == start) {
fragments.add((VariableTree) it.next());
}
}
return fragments;
}
/** Does this declaration have javadoc preceding it? */
private boolean hasJavaDoc(Tree bodyDeclaration) {
int position = ((JCTree) bodyDeclaration).getStartPosition();
Input.Token token = builder.getInput().getPositionTokenMap().get(position);
if (token != null) {
for (Input.Tok tok : token.getToksBefore()) {
if (tok.getText().startsWith("/**")) {
return true;
}
}
}
return false;
}
private static Optional extends Input.Token> getNextToken(Input input, int position) {
return Optional.ofNullable(input.getPositionTokenMap().get(position));
}
/** Does this list of trees end with the specified token? */
private boolean hasTrailingToken(Input input, List extends Tree> nodes, String token) {
if (nodes.isEmpty()) {
return false;
}
Tree lastNode = getLast(nodes);
Optional extends Input.Token> nextToken = getNextToken(input, getEndPosition(lastNode, getCurrentPath()));
return nextToken.isPresent() && nextToken.get().getTok().getText().equals(token);
}
/**
* Can a local with a set of modifiers be declared with horizontal annotations? This is currently true if there is
* at most one marker annotation, and no others.
*
* @param modifiers the list of {@link ModifiersTree}s
* @return whether the local can be declared with horizontal annotations
*/
private Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) {
int markerAnnotations = 0;
for (AnnotationTree annotation : modifiers.getAnnotations()) {
if (annotation.getArguments().isEmpty()) {
markerAnnotations++;
}
}
return markerAnnotations <= 1
&& markerAnnotations == modifiers.getAnnotations().size()
? Direction.HORIZONTAL
: Direction.VERTICAL;
}
/**
* Should a declaration with a set of modifiers be declared with horizontal annotations? This is currently true if
* all annotations are marker annotations.
*/
private Direction inlineAnnotationDirection(ModifiersTree modifiers) {
for (AnnotationTree annotation : modifiers.getAnnotations()) {
if (!annotation.getArguments().isEmpty()) {
return Direction.VERTICAL;
}
}
return Direction.HORIZONTAL;
}
/**
* Emit a {@link Token}.
*
* @param token the {@link String} to wrap in a {@link Token}
*/
protected void token(String token) {
builder.token(token, Token.RealOrImaginary.REAL, ZERO, /* breakAndIndentTrailingComment= */ Optional.empty());
}
/**
* Emit a {@link Token}.
*
* @param token the {@link String} to wrap in a {@link Token}
* @param plusIndentCommentsBefore extra indent for comments before this token
*/
protected void token(String token, Indent plusIndentCommentsBefore) {
builder.token(
token,
Token.RealOrImaginary.REAL,
plusIndentCommentsBefore,
/* breakAndIndentTrailingComment= */ Optional.empty());
}
/** Emit a {@link Token}, and breaks and indents trailing javadoc or block comments. */
void tokenBreakTrailingComment(String token, Indent breakAndIndentTrailingComment) {
builder.token(token, Token.RealOrImaginary.REAL, ZERO, Optional.of(breakAndIndentTrailingComment));
}
protected void markForPartialFormat() {
if (!inExpression()) {
builder.markForPartialFormat();
}
}
/**
* Sync to position in the input. If we've skipped outputting any tokens that were present in the input tokens,
* output them here and complain.
*
* @param node the ASTNode holding the input position
*/
protected void sync(Tree node) {
builder.sync(((JCTree) node).getStartPosition());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("builder", builder).toString();
}
}