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