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