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 - Initial API and implementation
/******************************************************************************* * 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.
/** 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(); } } }