001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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.CommonUtil; 030import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 031 032/** 033 * This class checks line-wrapping into definitions and expressions. The 034 * line-wrapping indentation should be not less than value of the 035 * lineWrappingIndentation parameter. 036 * 037 */ 038public class LineWrappingHandler { 039 040 /** 041 * Enum to be used for test if first line's indentation should be checked or not. 042 */ 043 public enum LineWrappingOptions { 044 045 /** 046 * First line's indentation should NOT be checked. 047 */ 048 IGNORE_FIRST_LINE, 049 /** 050 * First line's indentation should be checked. 051 */ 052 NONE; 053 054 /** 055 * Builds enum value from boolean. 056 * 057 * @param val value. 058 * @return enum instance. 059 * 060 * @noinspection BooleanParameter 061 */ 062 public static LineWrappingOptions ofBoolean(boolean val) { 063 LineWrappingOptions option = NONE; 064 if (val) { 065 option = IGNORE_FIRST_LINE; 066 } 067 return option; 068 } 069 070 } 071 072 /** 073 * The list of ignored token types for being checked by lineWrapping indentation 074 * inside {@code checkIndentation()} as these tokens are checked for lineWrapping 075 * inside their dedicated handlers. 076 * 077 * @see NewHandler#getIndentImpl() 078 * @see BlockParentHandler#curlyIndent() 079 * @see ArrayInitHandler#getIndentImpl() 080 */ 081 private static final int[] IGNORED_LIST = { 082 TokenTypes.RCURLY, 083 TokenTypes.LITERAL_NEW, 084 TokenTypes.ARRAY_INIT, 085 }; 086 087 /** 088 * The current instance of {@code IndentationCheck} class using this 089 * handler. This field used to get access to private fields of 090 * IndentationCheck instance. 091 */ 092 private final IndentationCheck indentCheck; 093 094 /** 095 * Sets values of class field, finds last node and calculates indentation level. 096 * 097 * @param instance 098 * instance of IndentationCheck. 099 */ 100 public LineWrappingHandler(IndentationCheck instance) { 101 indentCheck = instance; 102 } 103 104 /** 105 * Checks line wrapping into expressions and definitions using property 106 * 'lineWrappingIndentation'. 107 * 108 * @param firstNode First node to start examining. 109 * @param lastNode Last node to examine inclusively. 110 */ 111 public void checkIndentation(DetailAST firstNode, DetailAST lastNode) { 112 checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation()); 113 } 114 115 /** 116 * Checks line wrapping into expressions and definitions. 117 * 118 * @param firstNode First node to start examining. 119 * @param lastNode Last node to examine inclusively. 120 * @param indentLevel Indentation all wrapped lines should use. 121 */ 122 private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) { 123 checkIndentation(firstNode, lastNode, indentLevel, 124 -1, LineWrappingOptions.IGNORE_FIRST_LINE); 125 } 126 127 /** 128 * Checks line wrapping into expressions and definitions. 129 * 130 * @param firstNode First node to start examining. 131 * @param lastNode Last node to examine inclusively. 132 * @param indentLevel Indentation all wrapped lines should use. 133 * @param startIndent Indentation first line before wrapped lines used. 134 * @param ignoreFirstLine Test if first line's indentation should be checked or not. 135 */ 136 public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel, 137 int startIndent, LineWrappingOptions ignoreFirstLine) { 138 final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode, 139 lastNode); 140 141 final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); 142 if (firstLineNode.getType() == TokenTypes.AT) { 143 checkForAnnotationIndentation(firstNodesOnLines, indentLevel); 144 } 145 146 if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) { 147 // First node should be removed because it was already checked before. 148 firstNodesOnLines.remove(firstNodesOnLines.firstKey()); 149 } 150 151 final int firstNodeIndent; 152 if (startIndent == -1) { 153 firstNodeIndent = getLineStart(firstLineNode); 154 } 155 else { 156 firstNodeIndent = startIndent; 157 } 158 final int currentIndent = firstNodeIndent + indentLevel; 159 160 for (DetailAST node : firstNodesOnLines.values()) { 161 final int currentType = node.getType(); 162 if (checkForNullParameterChild(node) || checkForMethodLparenNewLine(node)) { 163 continue; 164 } 165 if (currentType == TokenTypes.RPAREN) { 166 logWarningMessage(node, firstNodeIndent); 167 } 168 else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) { 169 logWarningMessage(node, currentIndent); 170 } 171 } 172 } 173 174 /** 175 * Checks for annotation indentation. 176 * 177 * @param firstNodesOnLines the nodes which are present in the beginning of each line. 178 * @param indentLevel line wrapping indentation. 179 */ 180 public void checkForAnnotationIndentation( 181 NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { 182 final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); 183 DetailAST node = firstLineNode.getParent(); 184 while (node != null) { 185 if (node.getType() == TokenTypes.ANNOTATION) { 186 final DetailAST atNode = node.getFirstChild(); 187 final NavigableMap<Integer, DetailAST> annotationLines = 188 firstNodesOnLines.subMap( 189 node.getLineNo(), 190 true, 191 getNextNodeLine(firstNodesOnLines, node), 192 true 193 ); 194 checkAnnotationIndentation(atNode, annotationLines, indentLevel); 195 } 196 node = node.getNextSibling(); 197 } 198 } 199 200 /** 201 * Checks whether parameter node has any child or not. 202 * 203 * @param node the node for which to check. 204 * @return true if parameter has no child. 205 */ 206 public static boolean checkForNullParameterChild(DetailAST node) { 207 return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS; 208 } 209 210 /** 211 * Checks whether the method lparen starts from a new line or not. 212 * 213 * @param node the node for which to check. 214 * @return true if method lparen starts from a new line. 215 */ 216 public static boolean checkForMethodLparenNewLine(DetailAST node) { 217 final int parentType = node.getParent().getType(); 218 return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN; 219 } 220 221 /** 222 * Gets the next node line from the firstNodesOnLines map unless there is no next line, in 223 * which case, it returns the last line. 224 * 225 * @param firstNodesOnLines NavigableMap of lines and their first nodes. 226 * @param node the node for which to find the next node line 227 * @return the line number of the next line in the map 228 */ 229 private static Integer getNextNodeLine( 230 NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) { 231 Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo()); 232 if (nextNodeLine == null) { 233 nextNodeLine = firstNodesOnLines.lastKey(); 234 } 235 return nextNodeLine; 236 } 237 238 /** 239 * Finds first nodes on line and puts them into Map. 240 * 241 * @param firstNode First node to start examining. 242 * @param lastNode Last node to examine inclusively. 243 * @return NavigableMap which contains lines numbers as a key and first 244 * nodes on lines as a values. 245 */ 246 private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode, 247 DetailAST lastNode) { 248 final NavigableMap<Integer, DetailAST> result = new TreeMap<>(); 249 250 result.put(firstNode.getLineNo(), firstNode); 251 DetailAST curNode = firstNode.getFirstChild(); 252 253 while (curNode != lastNode) { 254 if (curNode.getType() == TokenTypes.OBJBLOCK 255 || curNode.getType() == TokenTypes.SLIST) { 256 curNode = curNode.getLastChild(); 257 } 258 259 final DetailAST firstTokenOnLine = result.get(curNode.getLineNo()); 260 261 if (firstTokenOnLine == null 262 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) { 263 result.put(curNode.getLineNo(), curNode); 264 } 265 curNode = getNextCurNode(curNode); 266 } 267 return result; 268 } 269 270 /** 271 * Returns next curNode node. 272 * 273 * @param curNode current node. 274 * @return next curNode node. 275 */ 276 private static DetailAST getNextCurNode(DetailAST curNode) { 277 DetailAST nodeToVisit = curNode.getFirstChild(); 278 DetailAST currentNode = curNode; 279 280 while (nodeToVisit == null) { 281 nodeToVisit = currentNode.getNextSibling(); 282 if (nodeToVisit == null) { 283 currentNode = currentNode.getParent(); 284 } 285 } 286 return nodeToVisit; 287 } 288 289 /** 290 * Checks line wrapping into annotations. 291 * 292 * @param atNode block tag node. 293 * @param firstNodesOnLines map which contains 294 * first nodes as values and line numbers as keys. 295 * @param indentLevel line wrapping indentation. 296 */ 297 private void checkAnnotationIndentation(DetailAST atNode, 298 NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { 299 final int firstNodeIndent = getLineStart(atNode); 300 final int currentIndent = firstNodeIndent + indentLevel; 301 final Collection<DetailAST> values = firstNodesOnLines.values(); 302 final DetailAST lastAnnotationNode = atNode.getParent().getLastChild(); 303 final int lastAnnotationLine = lastAnnotationNode.getLineNo(); 304 305 final Iterator<DetailAST> itr = values.iterator(); 306 while (firstNodesOnLines.size() > 1) { 307 final DetailAST node = itr.next(); 308 309 final DetailAST parentNode = node.getParent(); 310 final boolean isArrayInitPresentInAncestors = 311 isParentContainsTokenType(node, TokenTypes.ANNOTATION_ARRAY_INIT); 312 final boolean isCurrentNodeCloseAnnotationAloneInLine = 313 node.getLineNo() == lastAnnotationLine 314 && isEndOfScope(lastAnnotationNode, node); 315 if (!isArrayInitPresentInAncestors 316 && (isCurrentNodeCloseAnnotationAloneInLine 317 || node.getType() == TokenTypes.AT 318 && (parentNode.getParent().getType() == TokenTypes.MODIFIERS 319 || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS) 320 || TokenUtil.areOnSameLine(node, atNode))) { 321 logWarningMessage(node, firstNodeIndent); 322 } 323 else if (!isArrayInitPresentInAncestors) { 324 logWarningMessage(node, currentIndent); 325 } 326 itr.remove(); 327 } 328 } 329 330 /** 331 * Checks line for end of scope. Handles occurrences of close braces and close parenthesis on 332 * the same line. 333 * 334 * @param lastAnnotationNode the last node of the annotation 335 * @param node the node indicating where to begin checking 336 * @return true if all the nodes up to the last annotation node are end of scope nodes 337 * false otherwise 338 */ 339 private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) { 340 DetailAST checkNode = node; 341 boolean endOfScope = true; 342 while (endOfScope && !checkNode.equals(lastAnnotationNode)) { 343 switch (checkNode.getType()) { 344 case TokenTypes.RCURLY: 345 case TokenTypes.RBRACK: 346 while (checkNode.getNextSibling() == null) { 347 checkNode = checkNode.getParent(); 348 } 349 checkNode = checkNode.getNextSibling(); 350 break; 351 default: 352 endOfScope = false; 353 } 354 } 355 return endOfScope; 356 } 357 358 /** 359 * Checks that some parent of given node contains given token type. 360 * 361 * @param node node to check 362 * @param type type to look for 363 * @return true if there is a parent of given type 364 */ 365 private static boolean isParentContainsTokenType(final DetailAST node, int type) { 366 boolean returnValue = false; 367 for (DetailAST ast = node.getParent(); ast != null; ast = ast.getParent()) { 368 if (ast.getType() == type) { 369 returnValue = true; 370 break; 371 } 372 } 373 return returnValue; 374 } 375 376 /** 377 * Get the column number for the start of a given expression, expanding 378 * tabs out into spaces in the process. 379 * 380 * @param ast the expression to find the start of 381 * 382 * @return the column number for the start of the expression 383 */ 384 private int expandedTabsColumnNo(DetailAST ast) { 385 final String line = 386 indentCheck.getLine(ast.getLineNo() - 1); 387 388 return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(), 389 indentCheck.getIndentationTabWidth()); 390 } 391 392 /** 393 * Get the start of the line for the given expression. 394 * 395 * @param ast the expression to find the start of the line for 396 * 397 * @return the start of the line for the given expression 398 */ 399 private int getLineStart(DetailAST ast) { 400 final String line = indentCheck.getLine(ast.getLineNo() - 1); 401 return getLineStart(line); 402 } 403 404 /** 405 * Get the start of the specified line. 406 * 407 * @param line the specified line number 408 * @return the start of the specified line 409 */ 410 private int getLineStart(String line) { 411 int index = 0; 412 while (Character.isWhitespace(line.charAt(index))) { 413 index++; 414 } 415 return CommonUtil.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth()); 416 } 417 418 /** 419 * Logs warning message if indentation is incorrect. 420 * 421 * @param currentNode 422 * current node which probably invoked a violation. 423 * @param currentIndent 424 * correct indentation. 425 */ 426 private void logWarningMessage(DetailAST currentNode, int currentIndent) { 427 if (indentCheck.isForceStrictCondition()) { 428 if (expandedTabsColumnNo(currentNode) != currentIndent) { 429 indentCheck.indentationLog(currentNode, 430 IndentationCheck.MSG_ERROR, currentNode.getText(), 431 expandedTabsColumnNo(currentNode), currentIndent); 432 } 433 } 434 else { 435 if (expandedTabsColumnNo(currentNode) < currentIndent) { 436 indentCheck.indentationLog(currentNode, 437 IndentationCheck.MSG_ERROR, currentNode.getText(), 438 expandedTabsColumnNo(currentNode), currentIndent); 439 } 440 } 441 } 442 443}