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