blob: 9b2671a09c27223e890402c8f6d946e05caf056c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018, 2019 Mateusz Matela and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mateusz Matela <mateusz.matela@gmail.com> - Initial API and implementation
*
*******************************************************************************/
package org.eclipse.jdt.internal.formatter;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameLBRACE;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameRBRACE;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNamewhile;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ModuleDeclaration;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
/** Implementation of the "Keep braced code on one line" feature. */
public class OneLineEnforcer extends ASTVisitor {
private final TokenManager tm;
private final DefaultCodeFormatterOptions options;
public OneLineEnforcer(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
this.tm = tokenManager;
this.options = options;
}
@Override
public boolean preVisit2(ASTNode node) {
boolean isMalformed = (node.getFlags() & ASTNode.MALFORMED) != 0;
return !isMalformed;
}
@Override
public void endVisit(TypeDeclaration node) {
if (node.getParent().getLength() == 0)
return; // this is a fake block created by parsing in statements mode
tryKeepOnOneLine(node, node.getName(), node.bodyDeclarations(), this.options.keep_type_declaration_on_one_line);
}
@Override
public void endVisit(EnumDeclaration node) {
List<ASTNode> items = new ArrayList<>();
items.addAll(node.bodyDeclarations());
items.addAll(node.enumConstants());
tryKeepOnOneLine(node, node.getName(), items, this.options.keep_enum_declaration_on_one_line);
}
@Override
public void endVisit(AnnotationTypeDeclaration node) {
tryKeepOnOneLine(node, node.getName(), node.bodyDeclarations(),
this.options.keep_annotation_declaration_on_one_line);
}
@Override
public void endVisit(AnonymousClassDeclaration node) {
if (node.getParent() instanceof EnumConstantDeclaration) {
tryKeepOnOneLine(node, null, node.bodyDeclarations(),
this.options.keep_enum_constant_declaration_on_one_line);
} else {
tryKeepOnOneLine(node, null, node.bodyDeclarations(),
this.options.keep_anonymous_type_declaration_on_one_line);
}
}
@Override
public void endVisit(Block node) {
ASTNode parent = node.getParent();
List<Statement> statements = node.statements();
if (parent.getLength() == 0)
return; // this is a fake block created by parsing in statements mode
String oneLineOption;
if (parent instanceof MethodDeclaration) {
oneLineOption = this.options.keep_method_body_on_one_line;
if (this.options.keep_simple_getter_setter_on_one_line) {
MethodDeclaration method = (MethodDeclaration) parent;
String name = method.getName().getIdentifier();
Type returnType = method.getReturnType2();
boolean returnsVoid = returnType instanceof PrimitiveType
&& ((PrimitiveType) returnType).getPrimitiveTypeCode() == PrimitiveType.VOID;
boolean isGetter = name.matches("(is|get)\\p{Lu}.*") //$NON-NLS-1$
&& !method.isConstructor() && !returnsVoid && method.parameters().isEmpty()
&& statements.size() == 1 && statements.get(0) instanceof ReturnStatement;
boolean isSetter = name.matches("set\\p{Lu}.*") //$NON-NLS-1$
&& !method.isConstructor() && returnsVoid && method.parameters().size() == 1
&& statements.size() == 1 && statements.get(0) instanceof ExpressionStatement
&& ((ExpressionStatement) statements.get(0)).getExpression() instanceof Assignment;
if (isGetter || isSetter)
oneLineOption = DefaultCodeFormatterConstants.ONE_LINE_ALWAYS;
}
} else if (parent instanceof IfStatement && ((IfStatement) parent).getElseStatement() == null) {
oneLineOption = this.options.keep_if_then_body_block_on_one_line;
if (this.options.keep_guardian_clause_on_one_line) {
boolean isGuardian = statements.size() == 1 && (statements.get(0) instanceof ReturnStatement
|| statements.get(0) instanceof ThrowStatement);
// guard clause cannot start with a comment: https://bugs.eclipse.org/58565
int openBraceIndex = this.tm.firstIndexIn(node, TokenNameLBRACE);
isGuardian = isGuardian && !this.tm.get(openBraceIndex + 1).isComment();
if (isGuardian)
oneLineOption = DefaultCodeFormatterConstants.ONE_LINE_ALWAYS;
}
} else if (parent instanceof LambdaExpression) {
oneLineOption = this.options.keep_lambda_body_block_on_one_line;
} else if (parent instanceof ForStatement || parent instanceof EnhancedForStatement
|| parent instanceof WhileStatement) {
oneLineOption = this.options.keep_loop_body_block_on_one_line;
} else if (parent instanceof DoStatement) {
oneLineOption = this.options.keep_loop_body_block_on_one_line;
int openBraceIndex = this.tm.firstIndexIn(node, TokenNameLBRACE);
int closeBraceIndex = this.tm.lastIndexIn(node, TokenNameRBRACE);
Token whileToken = this.tm.firstTokenAfter(node, TokenNamewhile);
int lastIndex = whileToken.getLineBreaksBefore() == 0 ? this.tm.lastIndexIn(parent, -1) : closeBraceIndex;
tryKeepOnOneLine(openBraceIndex, closeBraceIndex, lastIndex, statements, oneLineOption);
return;
} else {
oneLineOption = this.options.keep_code_block_on_one_line;
}
tryKeepOnOneLine(node, null, statements, oneLineOption);
}
@Override
public void endVisit(ModuleDeclaration node) {
tryKeepOnOneLine(node, node.getName(), node.moduleStatements(), this.options.keep_type_declaration_on_one_line);
}
private void tryKeepOnOneLine(ASTNode node, ASTNode nodeBeforeOpenBrace, List<? extends ASTNode> items,
String oneLineOption) {
int openBraceIndex = nodeBeforeOpenBrace == null ? this.tm.firstIndexIn(node, TokenNameLBRACE)
: this.tm.firstIndexAfter(nodeBeforeOpenBrace, TokenNameLBRACE);
int closeBraceIndex = this.tm.lastIndexIn(node, TokenNameRBRACE);
tryKeepOnOneLine(openBraceIndex, closeBraceIndex, closeBraceIndex, items, oneLineOption);
}
private void tryKeepOnOneLine(int openBraceIndex, int closeBraceIndex, int lastIndex, List<? extends ASTNode> items,
String oneLineOption) {
if (DefaultCodeFormatterConstants.ONE_LINE_NEVER.equals(oneLineOption))
return;
if (DefaultCodeFormatterConstants.ONE_LINE_IF_EMPTY.equals(oneLineOption) && !items.isEmpty())
return;
if (DefaultCodeFormatterConstants.ONE_LINE_IF_SINGLE_ITEM.equals(oneLineOption) && items.size() > 1)
return;
if (DefaultCodeFormatterConstants.ONE_LINE_PRESERVE.equals(oneLineOption)
&& this.tm.countLineBreaksBetween(this.tm.get(openBraceIndex), this.tm.get(lastIndex)) > 0)
return;
Set<Integer> breakIndexes = items.stream().map(n -> this.tm.firstIndexIn(n, -1)).collect(Collectors.toSet());
breakIndexes.add(openBraceIndex + 1);
breakIndexes.add(closeBraceIndex);
Token prev = this.tm.get(openBraceIndex);
int startPos = this.tm.getPositionInLine(openBraceIndex);
int pos = startPos + this.tm.getLength(prev, startPos);
for (int i = openBraceIndex + 1; i <= lastIndex; i++) {
Token token = this.tm.get(i);
int preexistingBreaks = this.tm.countLineBreaksBetween(prev, token);
if (this.options.number_of_empty_lines_to_preserve > 0 && preexistingBreaks > 1)
return; // blank line will be preserved
boolean isSpace = prev.isSpaceAfter() || token.isSpaceBefore();
if (prev.isComment() || token.isComment()) {
if (preexistingBreaks > 0)
return; // line break around a comment will be preserved
char charBefore = this.tm.charAt(token.originalStart - 1);
isSpace = isSpace || charBefore == ' ' || charBefore == '\t';
}
if (prev.getLineBreaksAfter() > 0 || token.getLineBreaksBefore() > 0) {
if (!breakIndexes.contains(i))
return; // extra line break within an item, can't remove it
isSpace = isSpace || !(i == closeBraceIndex && i == openBraceIndex + 1);
}
if (isSpace)
pos++;
pos += this.tm.getLength(token, pos);
prev = token;
}
if (!items.isEmpty()) {
if (items.get(0).getParent().getParent() instanceof LambdaExpression)
pos -= startPos; // lambda body could be put in a wrapped line, so only check its own width
if (pos > this.options.page_width)
return; // line width limit exceeded
}
for (Integer i : breakIndexes) {
prev = this.tm.get(i - 1);
prev.clearLineBreaksAfter();
Token token = this.tm.get(i);
token.clearLineBreaksBefore();
if (!items.isEmpty())
token.spaceBefore();
}
}
}