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