//////////////////////////////////////////////////////////////////////////////// // 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.Collection; import java.util.Iterator; import java.util.NavigableMap; import java.util.TreeMap; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; /** * This class checks line-wrapping into definitions and expressions. The * line-wrapping indentation should be not less then value of the * lineWrappingIndentation parameter. * * @author maxvetrenko * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a> */ public class LineWrappingHandler { /** * The current instance of {@code IndentationCheck} class using this * handler. This field used to get access to private fields of * IndentationCheck instance. */ private final IndentationCheck indentCheck; /** * Sets values of class field, finds last node and calculates indentation level. * * @param instance * instance of IndentationCheck. */ public LineWrappingHandler(IndentationCheck instance) { indentCheck = instance; } /** * Checks line wrapping into expressions and definitions using property * 'lineWrappingIndentation'. * * @param firstNode First node to start examining. * @param lastNode Last node to examine inclusively. */ public void checkIndentation(DetailAST firstNode, DetailAST lastNode) { checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation()); } /** * Checks line wrapping into expressions and definitions. * * @param firstNode First node to start examining. * @param lastNode Last node to examine inclusively. * @param indentLevel Indentation all wrapped lines should use. */ public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) { checkIndentation(firstNode, lastNode, indentLevel, -1, true); } /** * Checks line wrapping into expressions and definitions. * * @param firstNode First node to start examining. * @param lastNode Last node to examine inclusively. * @param indentLevel 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. */ public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel, int startIndent, boolean ignoreFirstLine) { final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode, lastNode); final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); if (firstLineNode.getType() == TokenTypes.AT) { DetailAST node = firstLineNode.getParent(); while (node != null) { if (node.getType() == TokenTypes.ANNOTATION) { final DetailAST atNode = node.getFirstChild(); final NavigableMap<Integer, DetailAST> annotationLines = firstNodesOnLines.subMap( node.getLineNo(), true, getNextNodeLine(firstNodesOnLines, node), true ); checkAnnotationIndentation(atNode, annotationLines, indentLevel); } node = node.getNextSibling(); } } if (ignoreFirstLine) { // First node should be removed because it was already checked before. firstNodesOnLines.remove(firstNodesOnLines.firstKey()); } final int firstNodeIndent; if (startIndent == -1) { firstNodeIndent = getLineStart(firstLineNode); } else { firstNodeIndent = startIndent; } final int currentIndent = firstNodeIndent + indentLevel; for (DetailAST node : firstNodesOnLines.values()) { final int currentType = node.getType(); if (currentType == TokenTypes.RPAREN) { logWarningMessage(node, firstNodeIndent); } else if (currentType != TokenTypes.RCURLY && currentType != TokenTypes.ARRAY_INIT) { logWarningMessage(node, currentIndent); } } } /** * Gets the next node line from the firstNodesOnLines map unless there is no next line, in * which case, it returns the last line. * * @param firstNodesOnLines NavigableMap of lines and their first nodes. * @param node the node for which to find the next node line * @return the line number of the next line in the map */ private static Integer getNextNodeLine( NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) { Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo()); if (nextNodeLine == null) { nextNodeLine = firstNodesOnLines.lastKey(); } return nextNodeLine; } /** * Finds first nodes on line and puts them into Map. * * @param firstNode First node to start examining. * @param lastNode Last node to examine inclusively. * @return NavigableMap which contains lines numbers as a key and first * nodes on lines as a values. */ private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode, DetailAST lastNode) { final NavigableMap<Integer, DetailAST> result = new TreeMap<>(); result.put(firstNode.getLineNo(), firstNode); DetailAST curNode = firstNode.getFirstChild(); while (curNode != lastNode) { if (curNode.getType() == TokenTypes.OBJBLOCK || curNode.getType() == TokenTypes.SLIST) { curNode = curNode.getLastChild(); } final DetailAST firstTokenOnLine = result.get(curNode.getLineNo()); if (firstTokenOnLine == null || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) { result.put(curNode.getLineNo(), curNode); } curNode = getNextCurNode(curNode); } return result; } /** * Returns next curNode node. * * @param curNode current node. * @return next curNode node. */ private static DetailAST getNextCurNode(DetailAST curNode) { DetailAST nodeToVisit = curNode.getFirstChild(); DetailAST currentNode = curNode; while (nodeToVisit == null) { nodeToVisit = currentNode.getNextSibling(); if (nodeToVisit == null) { currentNode = currentNode.getParent(); } } return nodeToVisit; } /** * Checks line wrapping into annotations. * * @param atNode at-clause node. * @param firstNodesOnLines map which contains * first nodes as values and line numbers as keys. * @param indentLevel line wrapping indentation. */ private void checkAnnotationIndentation(DetailAST atNode, NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { final int firstNodeIndent = getLineStart(atNode); final int currentIndent = firstNodeIndent + indentLevel; final Collection<DetailAST> values = firstNodesOnLines.values(); final DetailAST lastAnnotationNode = atNode.getParent().getLastChild(); final int lastAnnotationLine = lastAnnotationNode.getLineNo(); final Iterator<DetailAST> itr = values.iterator(); while (firstNodesOnLines.size() > 1) { final DetailAST node = itr.next(); final DetailAST parentNode = node.getParent(); final boolean isCurrentNodeCloseAnnotationAloneInLine = node.getLineNo() == lastAnnotationLine && isEndOfScope(lastAnnotationNode, node); if (isCurrentNodeCloseAnnotationAloneInLine || node.getType() == TokenTypes.AT && (parentNode.getParent().getType() == TokenTypes.MODIFIERS || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)) { logWarningMessage(node, firstNodeIndent); } else { logWarningMessage(node, currentIndent); } itr.remove(); } } /** * Checks line for end of scope. Handles occurrences of close braces and close parenthesis on * the same line. * * @param lastAnnotationNode the last node of the annotation * @param node the node indicating where to begin checking * @return true if all the nodes up to the last annotation node are end of scope nodes * false otherwise */ private boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) { DetailAST checkNode = node; boolean endOfScope = true; while (endOfScope && !checkNode.equals(lastAnnotationNode)) { switch (checkNode.getType()) { case TokenTypes.RCURLY: case TokenTypes.RBRACK: while (checkNode.getNextSibling() == null) { checkNode = checkNode.getParent(); } checkNode = checkNode.getNextSibling(); break; default: endOfScope = false; } } return endOfScope; } /** * 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 */ private int expandedTabsColumnNo(DetailAST ast) { final String line = indentCheck.getLine(ast.getLineNo() - 1); return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(), indentCheck.getIndentationTabWidth()); } /** * 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 */ private int getLineStart(DetailAST ast) { final String line = indentCheck.getLine(ast.getLineNo() - 1); return getLineStart(line); } /** * 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()); } /** * Logs warning message if indentation is incorrect. * * @param currentNode * current node which probably invoked an error. * @param currentIndent * correct indentation. */ private void logWarningMessage(DetailAST currentNode, int currentIndent) { if (indentCheck.isForceStrictCondition()) { if (expandedTabsColumnNo(currentNode) != currentIndent) { indentCheck.indentationLog(currentNode.getLineNo(), IndentationCheck.MSG_ERROR, currentNode.getText(), expandedTabsColumnNo(currentNode), currentIndent); } } else { if (expandedTabsColumnNo(currentNode) < currentIndent) { indentCheck.indentationLog(currentNode.getLineNo(), IndentationCheck.MSG_ERROR, currentNode.getText(), expandedTabsColumnNo(currentNode), currentIndent); } } } }