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