AbstractExpressionHandler.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2021 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.indentation;
import java.util.Arrays;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
/**
* Abstract base class for all handlers.
*
*/
public abstract class AbstractExpressionHandler {
/**
* The instance of {@code IndentationCheck} using this handler.
*/
private final IndentationCheck indentCheck;
/** The AST which is handled by this handler. */
private final DetailAST mainAst;
/** Name used during output to user. */
private final String typeName;
/** Containing AST handler. */
private final AbstractExpressionHandler parent;
/** Indentation amount for this handler. */
private IndentLevel indent;
/**
* Construct an instance of this handler with the given indentation check,
* name, abstract syntax tree, and parent handler.
*
* @param indentCheck the indentation check
* @param typeName the name of the handler
* @param expr the abstract syntax tree
* @param parent the parent handler
*/
protected AbstractExpressionHandler(IndentationCheck indentCheck, String typeName,
DetailAST expr, AbstractExpressionHandler parent) {
this.indentCheck = indentCheck;
this.typeName = typeName;
mainAst = expr;
this.parent = parent;
}
/**
* Check the indentation of the expression we are handling.
*/
public abstract void checkIndentation();
/**
* Get the indentation amount for this handler. For performance reasons,
* this value is cached. The first time this method is called, the
* indentation amount is computed and stored. On further calls, the stored
* value is returned.
*
* @return the expected indentation amount
* @noinspection WeakerAccess
*/
public final IndentLevel getIndent() {
if (indent == null) {
indent = getIndentImpl();
}
return indent;
}
/**
* Compute the indentation amount for this handler.
*
* @return the expected indentation amount
*/
protected IndentLevel getIndentImpl() {
return parent.getSuggestedChildIndent(this);
}
/**
* Indentation level suggested for a child element. Children don't have
* to respect this, but most do.
*
* @param child child AST (so suggestion level can differ based on child
* type)
*
* @return suggested indentation for child
* @noinspection WeakerAccess
*/
public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
return new IndentLevel(getIndent(), getBasicOffset());
}
/**
* Log an indentation error.
*
* @param ast the expression that caused the error
* @param subtypeName the type of the expression
* @param actualIndent the actual indent level of the expression
*/
protected final void logError(DetailAST ast, String subtypeName,
int actualIndent) {
logError(ast, subtypeName, actualIndent, getIndent());
}
/**
* Log an indentation error.
*
* @param ast the expression that caused the error
* @param subtypeName the type of the expression
* @param actualIndent the actual indent level of the expression
* @param expectedIndent the expected indent level of the expression
*/
protected final void logError(DetailAST ast, String subtypeName,
int actualIndent, IndentLevel expectedIndent) {
final String typeStr;
if (subtypeName.isEmpty()) {
typeStr = "";
}
else {
typeStr = " " + subtypeName;
}
String messageKey = IndentationCheck.MSG_ERROR;
if (expectedIndent.isMultiLevel()) {
messageKey = IndentationCheck.MSG_ERROR_MULTI;
}
indentCheck.indentationLog(ast, messageKey,
typeName + typeStr, actualIndent, expectedIndent);
}
/**
* Log child indentation error.
*
* @param ast the abstract syntax tree that causes the error
* @param actualIndent the actual indent level of the expression
* @param expectedIndent the expected indent level of the expression
*/
private void logChildError(DetailAST ast,
int actualIndent,
IndentLevel expectedIndent) {
String messageKey = IndentationCheck.MSG_CHILD_ERROR;
if (expectedIndent.isMultiLevel()) {
messageKey = IndentationCheck.MSG_CHILD_ERROR_MULTI;
}
indentCheck.indentationLog(ast, messageKey,
typeName, actualIndent, expectedIndent);
}
/**
* Determines if the given expression is at the start of a line.
*
* @param ast the expression to check
*
* @return true if it is, false otherwise
*/
protected final boolean isOnStartOfLine(DetailAST ast) {
return getLineStart(ast) == expandedTabsColumnNo(ast);
}
/**
* Searches in given sub-tree (including given node) for the token
* which represents first symbol for this sub-tree in file.
*
* @param ast a root of sub-tree in which the search should be performed.
* @return a token which occurs first in the file.
* @noinspection WeakerAccess
*/
public static DetailAST getFirstToken(DetailAST ast) {
DetailAST first = ast;
DetailAST child = ast.getFirstChild();
while (child != null) {
final DetailAST toTest = getFirstToken(child);
if (toTest.getColumnNo() < first.getColumnNo()) {
first = toTest;
}
child = child.getNextSibling();
}
return first;
}
/**
* Get the start of the line for the given expression.
*
* @param ast the expression to find the start of the line for
*
* @return the start of the line for the given expression
*/
protected final int getLineStart(DetailAST ast) {
return getLineStart(ast.getLineNo());
}
/**
* Get the start of the line for the given line number.
*
* @param lineNo the line number to find the start for
*
* @return the start of the line for the given expression
*/
protected final int getLineStart(int lineNo) {
return getLineStart(indentCheck.getLine(lineNo - 1));
}
/**
* Get the start of the specified line.
*
* @param line the specified line number
*
* @return the start of the specified line
*/
private int getLineStart(String line) {
int index = 0;
while (Character.isWhitespace(line.charAt(index))) {
index++;
}
return CommonUtil.lengthExpandedTabs(
line, index, indentCheck.getIndentationTabWidth());
}
/**
* Checks that indentation should be increased after first line in checkLinesIndent().
*
* @return true if indentation should be increased after
* first line in checkLinesIndent()
* false otherwise
*/
protected boolean shouldIncreaseIndent() {
return true;
}
/**
* Check the indentation for a set of lines.
*
* @param astSet the set of abstract syntax tree to check
* @param indentLevel the indentation level
* @param firstLineMatches whether or not the first line has to match
* @param firstLine first line of whole expression
* @param allowNesting whether or not subtree nesting is allowed
*/
private void checkLinesIndent(DetailAstSet astSet,
IndentLevel indentLevel,
boolean firstLineMatches,
int firstLine,
boolean allowNesting) {
if (!astSet.isEmpty()) {
// check first line
final DetailAST startLineAst = astSet.firstLine();
final int endLine = astSet.lastLine();
int startCol = expandedTabsColumnNo(astSet.firstLine());
final int realStartCol =
getLineStart(indentCheck.getLine(startLineAst.getLineNo() - 1));
if (firstLineMatches && !allowNesting) {
startCol = realStartCol;
}
if (realStartCol == startCol) {
checkLineIndent(startLineAst, indentLevel,
firstLineMatches);
}
// if first line starts the line, following lines are indented
// one level; but if the first line of this expression is
// nested with the previous expression (which is assumed if it
// doesn't start the line) then don't indent more, the first
// indentation is absorbed by the nesting
IndentLevel theLevel = indentLevel;
if (firstLineMatches
|| firstLine > mainAst.getLineNo() && shouldIncreaseIndent()) {
theLevel = new IndentLevel(indentLevel, getBasicOffset());
}
// check following lines
for (int i = startLineAst.getLineNo() + 1; i <= endLine; i++) {
final Integer col = astSet.getStartColumn(i);
// startCol could be null if this line didn't have an
// expression that was required to be checked (it could be
// checked by a child expression)
if (col != null) {
checkLineIndent(astSet.getAst(i), theLevel, false);
}
}
}
}
/**
* Check the indentation for a single line.
*
* @param ast the abstract syntax tree to check
* @param indentLevel the indentation level
* @param mustMatch whether or not the indentation level must match
*/
private void checkLineIndent(DetailAST ast,
IndentLevel indentLevel, boolean mustMatch) {
final String line = indentCheck.getLine(ast.getLineNo() - 1);
final int start = getLineStart(line);
final int columnNumber = expandedTabsColumnNo(ast);
// if must match is set, it is a violation if the line start is not
// at the correct indention level; otherwise, it is an only an
// violation if this statement starts the line and it is less than
// the correct indentation level
if (mustMatch && !indentLevel.isAcceptable(start)
|| !mustMatch && columnNumber == start && indentLevel.isGreaterThan(start)) {
logChildError(ast, start, indentLevel);
}
}
/**
* Checks indentation on wrapped lines between and including
* {@code firstNode} and {@code lastNode}.
*
* @param firstNode First node to start examining.
* @param lastNode Last node to examine inclusively.
*/
protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) {
indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode);
}
/**
* Checks indentation on wrapped lines between and including
* {@code firstNode} and {@code lastNode}.
*
* @param firstNode First node to start examining.
* @param lastNode Last node to examine inclusively.
* @param wrappedIndentLevel Indentation all wrapped lines should use.
* @param startIndent Indentation first line before wrapped lines used.
* @param ignoreFirstLine Test if first line's indentation should be checked or not.
*/
protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode,
int wrappedIndentLevel, int startIndent, boolean ignoreFirstLine) {
indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode,
wrappedIndentLevel, startIndent,
LineWrappingHandler.LineWrappingOptions.ofBoolean(ignoreFirstLine));
}
/**
* Check the indent level of the children of the specified parent
* expression.
*
* @param parentNode the parent whose children we are checking
* @param tokenTypes the token types to check
* @param startIndent the starting indent level
* @param firstLineMatches whether or not the first line needs to match
* @param allowNesting whether or not nested children are allowed
*/
protected final void checkChildren(DetailAST parentNode,
int[] tokenTypes,
IndentLevel startIndent,
boolean firstLineMatches,
boolean allowNesting) {
Arrays.sort(tokenTypes);
for (DetailAST child = parentNode.getFirstChild();
child != null;
child = child.getNextSibling()) {
if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0) {
checkExpressionSubtree(child, startIndent,
firstLineMatches, allowNesting);
}
}
}
/**
* Check the indentation level for an expression subtree.
*
* @param tree the expression subtree to check
* @param indentLevel the indentation level
* @param firstLineMatches whether or not the first line has to match
* @param allowNesting whether or not subtree nesting is allowed
*/
protected final void checkExpressionSubtree(
DetailAST tree,
IndentLevel indentLevel,
boolean firstLineMatches,
boolean allowNesting
) {
final DetailAstSet subtreeAst = new DetailAstSet(indentCheck);
final int firstLine = getFirstLine(tree);
if (firstLineMatches && !allowNesting) {
final DetailAST firstAst = getFirstAstNode(tree);
subtreeAst.addAst(firstAst);
}
findSubtreeAst(subtreeAst, tree, allowNesting);
checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
}
/**
* Get the first line number for given expression.
*
* @param tree the expression to find the first line for
* @return the first line of expression
*/
protected static int getFirstLine(DetailAST tree) {
return getFirstAstNode(tree).getLineNo();
}
/**
* Get the first ast for given expression.
*
* @param ast the abstract syntax tree for which the starting ast is to be found
*
* @return the first ast of the expression
*/
protected static DetailAST getFirstAstNode(DetailAST ast) {
return getFirstAst(ast, ast);
}
/**
* Get the first ast for given expression.
*
* @param ast the current ast that may have minimum line number
* @param tree the expression to find the first line for
*
* @return the first ast of the expression
*/
private static DetailAST getFirstAst(DetailAST ast, DetailAST tree) {
DetailAST realStart = ast;
if (tree.getLineNo() < realStart.getLineNo()
|| tree.getLineNo() == realStart.getLineNo()
&& tree.getColumnNo() < realStart.getColumnNo()
) {
realStart = tree;
}
// check children
for (DetailAST node = tree.getFirstChild();
node != null;
node = node.getNextSibling()) {
realStart = getFirstAst(realStart, node);
}
return realStart;
}
/**
* Get the column number for the start of a given expression, expanding
* tabs out into spaces in the process.
*
* @param ast the expression to find the start of
*
* @return the column number for the start of the expression
*/
protected final int expandedTabsColumnNo(DetailAST ast) {
final String line =
indentCheck.getLine(ast.getLineNo() - 1);
return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
indentCheck.getIndentationTabWidth());
}
/**
* Find the set of abstract syntax tree for a given subtree.
*
* @param astSet the set of ast to add
* @param tree the subtree to examine
* @param allowNesting whether or not to allow nested subtrees
*/
protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
boolean allowNesting) {
if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) {
final int lineNum = tree.getLineNo();
final Integer colNum = astSet.getStartColumn(lineNum);
final int thisLineColumn = expandedTabsColumnNo(tree);
if (colNum == null || thisLineColumn < colNum) {
astSet.addAst(tree);
}
// check children
for (DetailAST node = tree.getFirstChild();
node != null;
node = node.getNextSibling()) {
findSubtreeAst(astSet, node, allowNesting);
}
}
}
/**
* Check the indentation level of modifiers.
*/
protected void checkModifiers() {
final DetailAST modifiers =
mainAst.findFirstToken(TokenTypes.MODIFIERS);
for (DetailAST modifier = modifiers.getFirstChild();
modifier != null;
modifier = modifier.getNextSibling()) {
if (isOnStartOfLine(modifier)
&& !getIndent().isAcceptable(expandedTabsColumnNo(modifier))) {
logError(modifier, "modifier",
expandedTabsColumnNo(modifier));
}
}
}
/**
* Accessor for the IndentCheck attribute.
*
* @return the IndentCheck attribute
*/
protected final IndentationCheck getIndentCheck() {
return indentCheck;
}
/**
* Accessor for the MainAst attribute.
*
* @return the MainAst attribute
*/
protected final DetailAST getMainAst() {
return mainAst;
}
/**
* Accessor for the Parent attribute.
*
* @return the Parent attribute
*/
protected final AbstractExpressionHandler getParent() {
return parent;
}
/**
* A shortcut for {@code IndentationCheck} property.
*
* @return value of basicOffset property of {@code IndentationCheck}
*/
protected final int getBasicOffset() {
return indentCheck.getBasicOffset();
}
/**
* A shortcut for {@code IndentationCheck} property.
*
* @return value of braceAdjustment property
* of {@code IndentationCheck}
*/
protected final int getBraceAdjustment() {
return indentCheck.getBraceAdjustment();
}
/**
* Check the indentation of the right parenthesis.
*
* @param rparen parenthesis to check
* @param lparen left parenthesis associated with aRparen
*/
protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
if (rparen != null) {
// the rcurly can either be at the correct indentation,
// or not first on the line
final int rparenLevel = expandedTabsColumnNo(rparen);
// or has <lparen level> + 1 indentation
final int lparenLevel = expandedTabsColumnNo(lparen);
if (rparenLevel != lparenLevel + 1
&& !getIndent().isAcceptable(rparenLevel)
&& isOnStartOfLine(rparen)) {
logError(rparen, "rparen", rparenLevel);
}
}
}
/**
* Check the indentation of the left parenthesis.
*
* @param lparen parenthesis to check
*/
protected final void checkLeftParen(final DetailAST lparen) {
// the rcurly can either be at the correct indentation, or on the
// same line as the lcurly
if (lparen != null
&& !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
&& isOnStartOfLine(lparen)) {
logError(lparen, "lparen", expandedTabsColumnNo(lparen));
}
}
}