001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.indentation; 021 022import java.util.Collection; 023import java.util.Iterator; 024import java.util.NavigableMap; 025import java.util.TreeMap; 026 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 030 031/** 032 * This class checks line-wrapping into definitions and expressions. The 033 * line-wrapping indentation should be not less then value of the 034 * lineWrappingIndentation parameter. 035 * 036 * @author maxvetrenko 037 * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a> 038 */ 039public class LineWrappingHandler { 040 041 /** 042 * The current instance of {@code IndentationCheck} class using this 043 * handler. This field used to get access to private fields of 044 * IndentationCheck instance. 045 */ 046 private final IndentationCheck indentCheck; 047 048 /** 049 * Sets values of class field, finds last node and calculates indentation level. 050 * 051 * @param instance 052 * instance of IndentationCheck. 053 */ 054 public LineWrappingHandler(IndentationCheck instance) { 055 indentCheck = instance; 056 } 057 058 /** 059 * Checks line wrapping into expressions and definitions using property 060 * 'lineWrappingIndentation'. 061 * 062 * @param firstNode First node to start examining. 063 * @param lastNode Last node to examine inclusively. 064 */ 065 public void checkIndentation(DetailAST firstNode, DetailAST lastNode) { 066 checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation()); 067 } 068 069 /** 070 * Checks line wrapping into expressions and definitions. 071 * 072 * @param firstNode First node to start examining. 073 * @param lastNode Last node to examine inclusively. 074 * @param indentLevel Indentation all wrapped lines should use. 075 */ 076 public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) { 077 checkIndentation(firstNode, lastNode, indentLevel, -1, true); 078 } 079 080 /** 081 * Checks line wrapping into expressions and definitions. 082 * 083 * @param firstNode First node to start examining. 084 * @param lastNode Last node to examine inclusively. 085 * @param indentLevel Indentation all wrapped lines should use. 086 * @param startIndent Indentation first line before wrapped lines used. 087 * @param ignoreFirstLine Test if first line's indentation should be checked or not. 088 */ 089 public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel, 090 int startIndent, boolean ignoreFirstLine) { 091 final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode, 092 lastNode); 093 094 final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); 095 if (firstLineNode.getType() == TokenTypes.AT) { 096 checkAnnotationIndentation(firstLineNode, firstNodesOnLines, indentLevel); 097 } 098 099 if (ignoreFirstLine) { 100 // First node should be removed because it was already checked before. 101 firstNodesOnLines.remove(firstNodesOnLines.firstKey()); 102 } 103 104 final int firstNodeIndent; 105 if (startIndent == -1) { 106 firstNodeIndent = getLineStart(firstLineNode); 107 } 108 else { 109 firstNodeIndent = startIndent; 110 } 111 final int currentIndent = firstNodeIndent + indentLevel; 112 113 for (DetailAST node : firstNodesOnLines.values()) { 114 final int currentType = node.getType(); 115 116 if (currentType == TokenTypes.RPAREN) { 117 logWarningMessage(node, firstNodeIndent); 118 } 119 else if (currentType != TokenTypes.RCURLY && currentType != TokenTypes.ARRAY_INIT) { 120 logWarningMessage(node, currentIndent); 121 } 122 } 123 } 124 125 /** 126 * Finds first nodes on line and puts them into Map. 127 * 128 * @param firstNode First node to start examining. 129 * @param lastNode Last node to examine inclusively. 130 * @return NavigableMap which contains lines numbers as a key and first 131 * nodes on lines as a values. 132 */ 133 private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode, 134 DetailAST lastNode) { 135 final NavigableMap<Integer, DetailAST> result = new TreeMap<>(); 136 137 result.put(firstNode.getLineNo(), firstNode); 138 DetailAST curNode = firstNode.getFirstChild(); 139 140 while (curNode != lastNode) { 141 142 if (curNode.getType() == TokenTypes.OBJBLOCK 143 || curNode.getType() == TokenTypes.SLIST) { 144 curNode = curNode.getLastChild(); 145 } 146 147 final DetailAST firstTokenOnLine = result.get(curNode.getLineNo()); 148 149 if (firstTokenOnLine == null 150 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) { 151 result.put(curNode.getLineNo(), curNode); 152 } 153 curNode = getNextCurNode(curNode); 154 } 155 return result; 156 } 157 158 /** 159 * Returns next curNode node. 160 * 161 * @param curNode current node. 162 * @return next curNode node. 163 */ 164 private static DetailAST getNextCurNode(DetailAST curNode) { 165 DetailAST nodeToVisit = curNode.getFirstChild(); 166 DetailAST currentNode = curNode; 167 168 while (nodeToVisit == null) { 169 nodeToVisit = currentNode.getNextSibling(); 170 if (nodeToVisit == null) { 171 currentNode = currentNode.getParent(); 172 } 173 } 174 return nodeToVisit; 175 } 176 177 /** 178 * Checks line wrapping into annotations. 179 * 180 * @param atNode at-clause node. 181 * @param firstNodesOnLines map which contains 182 * first nodes as values and line numbers as keys. 183 * @param indentLevel line wrapping indentation. 184 */ 185 private void checkAnnotationIndentation(DetailAST atNode, 186 NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { 187 final int firstNodeIndent = getLineStart(atNode); 188 final int currentIndent = firstNodeIndent + indentLevel; 189 final Collection<DetailAST> values = firstNodesOnLines.values(); 190 final DetailAST lastAnnotationNode = getLastAnnotationNode(atNode); 191 final int lastAnnotationLine = lastAnnotationNode.getLineNo(); 192 193 final Iterator<DetailAST> itr = values.iterator(); 194 while (firstNodesOnLines.size() > 1) { 195 final DetailAST node = itr.next(); 196 197 if (node.getLineNo() <= lastAnnotationLine) { 198 final DetailAST parentNode = node.getParent(); 199 final boolean isCurrentNodeCloseAnnotationAloneInLine = 200 node.getLineNo() == lastAnnotationLine 201 && node.equals(lastAnnotationNode); 202 if (isCurrentNodeCloseAnnotationAloneInLine 203 || node.getType() == TokenTypes.AT 204 && (parentNode.getParent().getType() == TokenTypes.MODIFIERS 205 || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)) { 206 logWarningMessage(node, firstNodeIndent); 207 } 208 else { 209 logWarningMessage(node, currentIndent); 210 } 211 itr.remove(); 212 } 213 else { 214 break; 215 } 216 } 217 } 218 219 /** 220 * Get the column number for the start of a given expression, expanding 221 * tabs out into spaces in the process. 222 * 223 * @param ast the expression to find the start of 224 * 225 * @return the column number for the start of the expression 226 */ 227 private int expandedTabsColumnNo(DetailAST ast) { 228 final String line = 229 indentCheck.getLine(ast.getLineNo() - 1); 230 231 return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(), 232 indentCheck.getIndentationTabWidth()); 233 } 234 235 /** 236 * Get the start of the line for the given expression. 237 * 238 * @param ast the expression to find the start of the line for 239 * 240 * @return the start of the line for the given expression 241 */ 242 private int getLineStart(DetailAST ast) { 243 final String line = indentCheck.getLine(ast.getLineNo() - 1); 244 return getLineStart(line); 245 } 246 247 /** 248 * Get the start of the specified line. 249 * 250 * @param line the specified line number 251 * 252 * @return the start of the specified line 253 */ 254 private int getLineStart(String line) { 255 int index = 0; 256 while (Character.isWhitespace(line.charAt(index))) { 257 index++; 258 } 259 return CommonUtils.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth()); 260 } 261 262 /** 263 * Finds and returns last annotation node. 264 * @param atNode first at-clause node. 265 * @return last annotation node. 266 */ 267 private static DetailAST getLastAnnotationNode(DetailAST atNode) { 268 DetailAST lastAnnotation = atNode.getParent(); 269 while (lastAnnotation.getNextSibling() != null 270 && lastAnnotation.getNextSibling().getType() == TokenTypes.ANNOTATION) { 271 lastAnnotation = lastAnnotation.getNextSibling(); 272 } 273 return lastAnnotation.getLastChild(); 274 } 275 276 /** 277 * Logs warning message if indentation is incorrect. 278 * 279 * @param currentNode 280 * current node which probably invoked an error. 281 * @param currentIndent 282 * correct indentation. 283 */ 284 private void logWarningMessage(DetailAST currentNode, int currentIndent) { 285 if (indentCheck.isForceStrictCondition()) { 286 if (expandedTabsColumnNo(currentNode) != currentIndent) { 287 indentCheck.indentationLog(currentNode.getLineNo(), 288 IndentationCheck.MSG_ERROR, currentNode.getText(), 289 expandedTabsColumnNo(currentNode), currentIndent); 290 } 291 } 292 else { 293 if (expandedTabsColumnNo(currentNode) < currentIndent) { 294 indentCheck.indentationLog(currentNode.getLineNo(), 295 IndentationCheck.MSG_ERROR, currentNode.getText(), 296 expandedTabsColumnNo(currentNode), currentIndent); 297 } 298 } 299 } 300}