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 DetailAST node = firstLineNode.getParent(); 097 while (node != null) { 098 if (node.getType() == TokenTypes.ANNOTATION) { 099 final DetailAST atNode = node.getFirstChild(); 100 final NavigableMap<Integer, DetailAST> annotationLines = 101 firstNodesOnLines.subMap( 102 node.getLineNo(), 103 true, 104 getNextNodeLine(firstNodesOnLines, node), 105 true 106 ); 107 checkAnnotationIndentation(atNode, annotationLines, indentLevel); 108 } 109 node = node.getNextSibling(); 110 } 111 } 112 113 if (ignoreFirstLine) { 114 // First node should be removed because it was already checked before. 115 firstNodesOnLines.remove(firstNodesOnLines.firstKey()); 116 } 117 118 final int firstNodeIndent; 119 if (startIndent == -1) { 120 firstNodeIndent = getLineStart(firstLineNode); 121 } 122 else { 123 firstNodeIndent = startIndent; 124 } 125 final int currentIndent = firstNodeIndent + indentLevel; 126 127 for (DetailAST node : firstNodesOnLines.values()) { 128 final int currentType = node.getType(); 129 130 if (currentType == TokenTypes.RPAREN) { 131 logWarningMessage(node, firstNodeIndent); 132 } 133 else if (currentType != TokenTypes.RCURLY && currentType != TokenTypes.ARRAY_INIT) { 134 logWarningMessage(node, currentIndent); 135 } 136 } 137 } 138 139 /** 140 * Gets the next node line from the firstNodesOnLines map unless there is no next line, in 141 * which case, it returns the last line. 142 * 143 * @param firstNodesOnLines NavigableMap of lines and their first nodes. 144 * @param node the node for which to find the next node line 145 * @return the line number of the next line in the map 146 */ 147 private static Integer getNextNodeLine( 148 NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) { 149 Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo()); 150 if (nextNodeLine == null) { 151 nextNodeLine = firstNodesOnLines.lastKey(); 152 } 153 return nextNodeLine; 154 } 155 156 /** 157 * Finds first nodes on line and puts them into Map. 158 * 159 * @param firstNode First node to start examining. 160 * @param lastNode Last node to examine inclusively. 161 * @return NavigableMap which contains lines numbers as a key and first 162 * nodes on lines as a values. 163 */ 164 private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode, 165 DetailAST lastNode) { 166 final NavigableMap<Integer, DetailAST> result = new TreeMap<>(); 167 168 result.put(firstNode.getLineNo(), firstNode); 169 DetailAST curNode = firstNode.getFirstChild(); 170 171 while (curNode != lastNode) { 172 173 if (curNode.getType() == TokenTypes.OBJBLOCK 174 || curNode.getType() == TokenTypes.SLIST) { 175 curNode = curNode.getLastChild(); 176 } 177 178 final DetailAST firstTokenOnLine = result.get(curNode.getLineNo()); 179 180 if (firstTokenOnLine == null 181 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) { 182 result.put(curNode.getLineNo(), curNode); 183 } 184 curNode = getNextCurNode(curNode); 185 } 186 return result; 187 } 188 189 /** 190 * Returns next curNode node. 191 * 192 * @param curNode current node. 193 * @return next curNode node. 194 */ 195 private static DetailAST getNextCurNode(DetailAST curNode) { 196 DetailAST nodeToVisit = curNode.getFirstChild(); 197 DetailAST currentNode = curNode; 198 199 while (nodeToVisit == null) { 200 nodeToVisit = currentNode.getNextSibling(); 201 if (nodeToVisit == null) { 202 currentNode = currentNode.getParent(); 203 } 204 } 205 return nodeToVisit; 206 } 207 208 /** 209 * Checks line wrapping into annotations. 210 * 211 * @param atNode at-clause node. 212 * @param firstNodesOnLines map which contains 213 * first nodes as values and line numbers as keys. 214 * @param indentLevel line wrapping indentation. 215 */ 216 private void checkAnnotationIndentation(DetailAST atNode, 217 NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { 218 final int firstNodeIndent = getLineStart(atNode); 219 final int currentIndent = firstNodeIndent + indentLevel; 220 final Collection<DetailAST> values = firstNodesOnLines.values(); 221 final DetailAST lastAnnotationNode = atNode.getParent().getLastChild(); 222 final int lastAnnotationLine = lastAnnotationNode.getLineNo(); 223 224 final Iterator<DetailAST> itr = values.iterator(); 225 while (firstNodesOnLines.size() > 1) { 226 final DetailAST node = itr.next(); 227 228 final DetailAST parentNode = node.getParent(); 229 final boolean isCurrentNodeCloseAnnotationAloneInLine = 230 node.getLineNo() == lastAnnotationLine 231 && isEndOfScope(lastAnnotationNode, node); 232 if (isCurrentNodeCloseAnnotationAloneInLine 233 || node.getType() == TokenTypes.AT 234 && (parentNode.getParent().getType() == TokenTypes.MODIFIERS 235 || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)) { 236 logWarningMessage(node, firstNodeIndent); 237 } 238 else { 239 logWarningMessage(node, currentIndent); 240 } 241 itr.remove(); 242 } 243 } 244 245 /** 246 * Checks line for end of scope. Handles occurrences of close braces and close parenthesis on 247 * the same line. 248 * 249 * @param lastAnnotationNode the last node of the annotation 250 * @param node the node indicating where to begin checking 251 * @return true if all the nodes up to the last annotation node are end of scope nodes 252 * false otherwise 253 */ 254 private boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) { 255 DetailAST checkNode = node; 256 boolean endOfScope = true; 257 while (endOfScope && !checkNode.equals(lastAnnotationNode)) { 258 switch (checkNode.getType()) { 259 case TokenTypes.RCURLY: 260 case TokenTypes.RBRACK: 261 while (checkNode.getNextSibling() == null) { 262 checkNode = checkNode.getParent(); 263 } 264 checkNode = checkNode.getNextSibling(); 265 break; 266 default: 267 endOfScope = false; 268 269 } 270 271 } 272 return endOfScope; 273 } 274 275 /** 276 * Get the column number for the start of a given expression, expanding 277 * tabs out into spaces in the process. 278 * 279 * @param ast the expression to find the start of 280 * 281 * @return the column number for the start of the expression 282 */ 283 private int expandedTabsColumnNo(DetailAST ast) { 284 final String line = 285 indentCheck.getLine(ast.getLineNo() - 1); 286 287 return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(), 288 indentCheck.getIndentationTabWidth()); 289 } 290 291 /** 292 * Get the start of the line for the given expression. 293 * 294 * @param ast the expression to find the start of the line for 295 * 296 * @return the start of the line for the given expression 297 */ 298 private int getLineStart(DetailAST ast) { 299 final String line = indentCheck.getLine(ast.getLineNo() - 1); 300 return getLineStart(line); 301 } 302 303 /** 304 * Get the start of the specified line. 305 * 306 * @param line the specified line number 307 * @return the start of the specified line 308 */ 309 private int getLineStart(String line) { 310 int index = 0; 311 while (Character.isWhitespace(line.charAt(index))) { 312 index++; 313 } 314 return CommonUtils.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth()); 315 } 316 317 /** 318 * Logs warning message if indentation is incorrect. 319 * 320 * @param currentNode 321 * current node which probably invoked an error. 322 * @param currentIndent 323 * correct indentation. 324 */ 325 private void logWarningMessage(DetailAST currentNode, int currentIndent) { 326 if (indentCheck.isForceStrictCondition()) { 327 if (expandedTabsColumnNo(currentNode) != currentIndent) { 328 indentCheck.indentationLog(currentNode.getLineNo(), 329 IndentationCheck.MSG_ERROR, currentNode.getText(), 330 expandedTabsColumnNo(currentNode), currentIndent); 331 } 332 } 333 else { 334 if (expandedTabsColumnNo(currentNode) < currentIndent) { 335 indentCheck.indentationLog(currentNode.getLineNo(), 336 IndentationCheck.MSG_ERROR, currentNode.getText(), 337 expandedTabsColumnNo(currentNode), currentIndent); 338 } 339 } 340 } 341}