//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 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.CommonUtils; /** * Abstract base class for all handlers. * * @author jrichard */ 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 */ 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 */ 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.getLineNo(), messageKey, typeName + typeStr, actualIndent, expectedIndent); } /** * Log child indentation error. * * @param line the expression that caused the error * @param actualIndent the actual indent level of the expression * @param expectedIndent the expected indent level of the expression */ private void logChildError(int line, int actualIndent, IndentLevel expectedIndent) { String messageKey = IndentationCheck.MSG_CHILD_ERROR; if (expectedIndent.isMultiLevel()) { messageKey = IndentationCheck.MSG_CHILD_ERROR_MULTI; } indentCheck.indentationLog(line, 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); } /** * Determines if two expressions are on the same line. * * @param ast1 the first expression * @param ast2 the second expression * * @return true if they are, false otherwise */ public static boolean areOnSameLine(DetailAST ast1, DetailAST ast2) { return ast1.getLineNo() == ast2.getLineNo(); } /** * 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. */ 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 CommonUtils.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 lines the set of lines 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 */ private void checkLinesIndent(LineSet lines, IndentLevel indentLevel, boolean firstLineMatches, int firstLine) { if (!lines.isEmpty()) { // check first line final int startLine = lines.firstLine(); final int endLine = lines.lastLine(); final int startCol = lines.firstLineCol(); final int realStartCol = getLineStart(indentCheck.getLine(startLine - 1)); if (realStartCol == startCol) { checkLineIndent(startLine, startCol, 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 = startLine + 1; i <= endLine; i++) { final Integer col = lines.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(i, col, theLevel, false); } } } } /** * Check the indentation for a single line. * * @param lineNum the number of the line to check * @param colNum the column number we are starting at * @param indentLevel the indentation level * @param mustMatch whether or not the indentation level must match */ private void checkLineIndent(int lineNum, int colNum, IndentLevel indentLevel, boolean mustMatch) { final String line = indentCheck.getLine(lineNum - 1); final int start = getLineStart(line); // if must match is set, it is an error if the line start is not // at the correct indention level; otherwise, it is an only an // error if this statement starts the line and it is less than // the correct indentation level if (mustMatch && !indentLevel.isAcceptable(start) || !mustMatch && colNum == start && indentLevel.isGreaterThan(start)) { logChildError(lineNum, 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, 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 LineSet subtreeLines = new LineSet(); final int firstLine = getFirstLine(Integer.MAX_VALUE, tree); if (firstLineMatches && !allowNesting) { subtreeLines.addLineAndCol(firstLine, getLineStart(indentCheck.getLine(firstLine - 1))); } findSubtreeLines(subtreeLines, tree, allowNesting); checkLinesIndent(subtreeLines, indentLevel, firstLineMatches, firstLine); } /** * Get the first line for a given expression. * * @param startLine the line we are starting from * @param tree the expression to find the first line for * * @return the first line of the expression */ protected final int getFirstLine(int startLine, DetailAST tree) { int realStart = startLine; final int currLine = tree.getLineNo(); if (currLine < realStart) { realStart = currLine; } // check children for (DetailAST node = tree.getFirstChild(); node != null; node = node.getNextSibling()) { realStart = getFirstLine(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 CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(), indentCheck.getIndentationTabWidth()); } /** * Find the set of lines for a given subtree. * * @param lines the set of lines to add to * @param tree the subtree to examine * @param allowNesting whether or not to allow nested subtrees */ protected final void findSubtreeLines(LineSet lines, DetailAST tree, boolean allowNesting) { if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) { final int lineNum = tree.getLineNo(); final Integer colNum = lines.getStartColumn(lineNum); final int thisLineColumn = expandedTabsColumnNo(tree); if (colNum == null || thisLineColumn < colNum) { lines.addLineAndCol(lineNum, thisLineColumn); } // check children for (DetailAST node = tree.getFirstChild(); node != null; node = node.getNextSibling()) { findSubtreeLines(lines, 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)); } } }