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 lineWrappingIndentation parameter. 035 * 036 */ 037public class LineWrappingHandler { 038 039 /** 040 * Enum to be used for test if first line's indentation should be checked or not. 041 */ 042 public enum LineWrappingOptions { 043 044 /** 045 * First line's indentation should NOT be checked. 046 */ 047 IGNORE_FIRST_LINE, 048 /** 049 * First line's indentation should be checked. 050 */ 051 NONE 052 053 } 054 055 /** 056 * The list of ignored token types for being checked by lineWrapping indentation 057 * inside {@code checkIndentation()} as these tokens are checked for lineWrapping 058 * inside their dedicated handlers. 059 * 060 * @see NewHandler#getIndentImpl() 061 * @see BlockParentHandler#curlyIndent() 062 * @see ArrayInitHandler#getIndentImpl() 063 * @see CaseHandler#getIndentImpl() 064 */ 065 private static final int[] IGNORED_LIST = { 066 TokenTypes.LCURLY, 067 TokenTypes.RCURLY, 068 TokenTypes.LITERAL_NEW, 069 TokenTypes.LITERAL_YIELD, 070 TokenTypes.ARRAY_INIT, 071 TokenTypes.LITERAL_DEFAULT, 072 TokenTypes.LITERAL_CASE, 073 }; 074 075 /** 076 * The indentation context. Set lazily via {@link #setContext} because the 077 * context and this handler are created together at {@code beginTree} time. 078 */ 079 private IndentationContext context; 080 081 /** Default constructor. Call {@link #setContext} before use. */ 082 public LineWrappingHandler() { 083 // context is injected via setContext to break construction cycle with 084 // IndentationContext, which needs a LineWrappingHandler reference. 085 } 086 087 /** 088 * Injects the indentation context. Must be called before any check method. 089 * 090 * @param indentationContext the indentation context to use 091 */ 092 /* package */ void setContext(IndentationContext indentationContext) { 093 context = indentationContext; 094 } 095 096 /** 097 * Checks line wrapping into expressions and definitions using property 098 * 'lineWrappingIndentation'. 099 * 100 * @param firstNode First node to start examining. 101 * @param lastNode Last node to examine inclusively. 102 */ 103 public void checkIndentation(DetailAST firstNode, DetailAST lastNode) { 104 checkIndentation(firstNode, lastNode, context.getLineWrappingIndentation()); 105 } 106 107 /** 108 * Checks line wrapping into expressions and definitions. 109 * 110 * @param firstNode First node to start examining. 111 * @param lastNode Last node to examine inclusively. 112 * @param indentLevel Indentation all wrapped lines should use. 113 */ 114 private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) { 115 checkIndentation(firstNode, lastNode, indentLevel, 116 -1, LineWrappingOptions.IGNORE_FIRST_LINE); 117 } 118 119 /** 120 * Checks line wrapping into expressions and definitions. 121 * 122 * @param firstNode First node to start examining. 123 * @param lastNode Last node to examine inclusively. 124 * @param indentLevel Indentation all wrapped lines should use. 125 * @param startIndent Indentation first line before wrapped lines used. 126 * @param ignoreFirstLine Test if first line's indentation should be checked or not. 127 */ 128 public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel, 129 int startIndent, LineWrappingOptions ignoreFirstLine) { 130 final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode, 131 lastNode); 132 133 final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); 134 if (firstLineNode.getType() == TokenTypes.AT) { 135 checkForAnnotationIndentation(firstNodesOnLines, indentLevel); 136 } 137 138 if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) { 139 // First node should be removed because it was already checked before. 140 firstNodesOnLines.remove(firstNodesOnLines.firstKey()); 141 } 142 143 final int firstNodeIndent; 144 if (startIndent == -1) { 145 firstNodeIndent = getLineStart(firstLineNode); 146 } 147 else { 148 firstNodeIndent = startIndent; 149 } 150 final int currentIndent = firstNodeIndent + indentLevel; 151 152 for (DetailAST node : firstNodesOnLines.values()) { 153 checkNodeIndentation(node, firstNodeIndent, currentIndent); 154 } 155 } 156 157 /** 158 * Checks whether the indentation of the node on the wrapped line is correct. 159 * 160 * @param node the node for which to check. 161 * @param firstNodeIndent indentation first line before wrapped lines used. 162 * @param currentIndent indentation all wrapped lines should use. 163 */ 164 private void checkNodeIndentation(DetailAST node, int firstNodeIndent, int currentIndent) { 165 if (!checkForNullParameterChild(node) && !checkForMethodLparenNewLine(node) 166 && shouldProcessTextBlockLiteral(node)) { 167 final int currentType = node.getType(); 168 if (currentType == TokenTypes.RPAREN) { 169 if (node.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF) { 170 logWarningMessage(node, currentIndent); 171 } 172 else { 173 logWarningMessage(node, firstNodeIndent); 174 } 175 } 176 else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) { 177 logWarningMessage(node, currentIndent); 178 } 179 } 180 } 181 182 /** 183 * Checks for annotation indentation. 184 * 185 * @param firstNodesOnLines the nodes which are present in the beginning of each line. 186 * @param indentLevel line wrapping indentation. 187 */ 188 public void checkForAnnotationIndentation( 189 NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) { 190 final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey()); 191 DetailAST node = firstLineNode.getParent(); 192 while (node != null) { 193 if (node.getType() == TokenTypes.ANNOTATION) { 194 final DetailAST atNode = node.getFirstChild(); 195 final NavigableMap<Integer, DetailAST> annotationLines = 196 firstNodesOnLines.subMap( 197 node.getLineNo(), 198 true, 199 getNextNodeLine(firstNodesOnLines, node), 200 true 201 ); 202 checkAnnotationIndentation(atNode, annotationLines, indentLevel); 203 } 204 node = node.getNextSibling(); 205 } 206 } 207 208 /** 209 * Checks whether parameter node has any child or not. 210 * 211 * @param node the node for which to check. 212 * @return true if parameter has no child. 213 */ 214 public static boolean checkForNullParameterChild(DetailAST node) { 215 return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS; 216 } 217 218 /** 219 * Checks whether the method lparen starts from a new line or not. 220 * 221 * @param node the node for which to check. 222 * @return true if method lparen starts from a new line. 223 */ 224 public static boolean checkForMethodLparenNewLine(DetailAST node) { 225 final int parentType = node.getParent().getType(); 226 return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN; 227 } 228 229 /** 230 * Gets the next node line from the firstNodesOnLines map unless there is no next line, in 231 * which case, it returns the last line. 232 * 233 * @param firstNodesOnLines NavigableMap of lines and their first nodes. 234 * @param node the node for which to find the next node line 235 * @return the line number of the next line in the map 236 */ 237 private static Integer getNextNodeLine( 238 NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) { 239 Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo()); 240 if (nextNodeLine == null) { 241 nextNodeLine = firstNodesOnLines.lastKey(); 242 } 243 return nextNodeLine; 244 } 245 246 /** 247 * Finds first nodes on line and puts them into Map. 248 * 249 * @param firstNode First node to start examining. 250 * @param lastNode Last node to examine inclusively. 251 * @return NavigableMap which contains lines numbers as a key and first 252 * nodes on lines as a values. 253 */ 254 private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode, 255 DetailAST lastNode) { 256 final NavigableMap<Integer, DetailAST> result = new TreeMap<>(); 257 258 result.put(firstNode.getLineNo(), firstNode); 259 DetailAST curNode = firstNode.getFirstChild(); 260 261 while (curNode != lastNode) { 262 if (curNode.getType() == TokenTypes.OBJBLOCK 263 || curNode.getType() == TokenTypes.SLIST) { 264 curNode = curNode.getLastChild(); 265 } 266 267 final DetailAST firstTokenOnLine = result.get(curNode.getLineNo()); 268 269 if (firstTokenOnLine == null 270 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) { 271 result.put(curNode.getLineNo(), curNode); 272 } 273 curNode = getNextCurNode(curNode); 274 } 275 return result; 276 } 277 278 /** 279 * Checks whether indentation of {@code TEXT_BLOCK_LITERAL_END} 280 * needs to be checked. Yes if it is first on start of the line. 281 * 282 * @param node the node 283 * @return true if node is line-starting node. 284 */ 285 private boolean shouldProcessTextBlockLiteral(DetailAST node) { 286 return node.getType() != TokenTypes.TEXT_BLOCK_LITERAL_END 287 || expandedTabsColumnNo(node) == getLineStart(node); 288 } 289 290 /** 291 * Returns next curNode node. 292 * 293 * @param curNode current node. 294 * @return next curNode node. 295 */ 296 private static DetailAST getNextCurNode(DetailAST curNode) { 297 DetailAST nodeToVisit = curNode.getFirstChild(); 298 DetailAST currentNode = curNode; 299 300 while (nodeToVisit == null) { 301 nodeToVisit = currentNode.getNextSibling(); 302 if (nodeToVisit == null) { 303 currentNode = currentNode.getParent(); 304 } 305 } 306 return nodeToVisit; 307 } 308 309 /** 310 * Checks line wrapping into annotations. 311 * 312 * @param atNode block tag node. 313 * @param firstNodesOnLines map which contains 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, TokenTypes.RBRACK -> { 364 while (checkNode.getNextSibling() == null) { 365 checkNode = checkNode.getParent(); 366 } 367 checkNode = checkNode.getNextSibling(); 368 } 369 default -> endOfScope = false; 370 } 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 context.getLine(ast.getLineNo() - 1); 405 406 return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(), 407 context.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 = context.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, context.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 (context.isForceStrictCondition()) { 446 if (expandedTabsColumnNo(currentNode) != currentIndent) { 447 context.indentationLog(currentNode, 448 IndentationContext.MSG_ERROR, currentNode.getText(), 449 expandedTabsColumnNo(currentNode), currentIndent); 450 } 451 } 452 else { 453 if (expandedTabsColumnNo(currentNode) < currentIndent) { 454 context.indentationLog(currentNode, 455 IndentationContext.MSG_ERROR, currentNode.getText(), 456 expandedTabsColumnNo(currentNode), currentIndent); 457 } 458 } 459 } 460 461}