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