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