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