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.Arrays; 023 024import com.puppycrawl.tools.checkstyle.api.DetailAST; 025import com.puppycrawl.tools.checkstyle.api.TokenTypes; 026import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 027 028/** 029 * Abstract base class for all handlers. 030 * 031 */ 032public abstract class AbstractExpressionHandler { 033 034 /** 035 * The instance of {@code IndentationCheck} using this handler. 036 */ 037 private final IndentationCheck indentCheck; 038 039 /** The AST which is handled by this handler. */ 040 private final DetailAST mainAst; 041 042 /** Name used during output to user. */ 043 private final String typeName; 044 045 /** Containing AST handler. */ 046 private final AbstractExpressionHandler parent; 047 048 /** Indentation amount for this handler. */ 049 private IndentLevel indent; 050 051 /** 052 * Construct an instance of this handler with the given indentation check, 053 * name, abstract syntax tree, and parent handler. 054 * 055 * @param indentCheck the indentation check 056 * @param typeName the name of the handler 057 * @param expr the abstract syntax tree 058 * @param parent the parent handler 059 */ 060 protected AbstractExpressionHandler(IndentationCheck indentCheck, String typeName, 061 DetailAST expr, AbstractExpressionHandler parent) { 062 this.indentCheck = indentCheck; 063 this.typeName = typeName; 064 mainAst = expr; 065 this.parent = parent; 066 } 067 068 /** 069 * Check the indentation of the expression we are handling. 070 */ 071 public abstract void checkIndentation(); 072 073 /** 074 * Get the indentation amount for this handler. For performance reasons, 075 * this value is cached. The first time this method is called, the 076 * indentation amount is computed and stored. On further calls, the stored 077 * value is returned. 078 * 079 * @return the expected indentation amount 080 * @noinspection WeakerAccess 081 */ 082 public final IndentLevel getIndent() { 083 if (indent == null) { 084 indent = getIndentImpl(); 085 } 086 return indent; 087 } 088 089 /** 090 * Compute the indentation amount for this handler. 091 * 092 * @return the expected indentation amount 093 */ 094 protected IndentLevel getIndentImpl() { 095 return parent.getSuggestedChildIndent(this); 096 } 097 098 /** 099 * Indentation level suggested for a child element. Children don't have 100 * to respect this, but most do. 101 * 102 * @param child child AST (so suggestion level can differ based on child 103 * type) 104 * 105 * @return suggested indentation for child 106 * @noinspection WeakerAccess 107 */ 108 public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) { 109 return new IndentLevel(getIndent(), getBasicOffset()); 110 } 111 112 /** 113 * Log an indentation error. 114 * 115 * @param ast the expression that caused the error 116 * @param subtypeName the type of the expression 117 * @param actualIndent the actual indent level of the expression 118 */ 119 protected final void logError(DetailAST ast, String subtypeName, 120 int actualIndent) { 121 logError(ast, subtypeName, actualIndent, getIndent()); 122 } 123 124 /** 125 * Log an indentation error. 126 * 127 * @param ast the expression that caused the error 128 * @param subtypeName the type of the expression 129 * @param actualIndent the actual indent level of the expression 130 * @param expectedIndent the expected indent level of the expression 131 */ 132 protected final void logError(DetailAST ast, String subtypeName, 133 int actualIndent, IndentLevel expectedIndent) { 134 final String typeStr; 135 136 if (subtypeName.isEmpty()) { 137 typeStr = ""; 138 } 139 else { 140 typeStr = " " + subtypeName; 141 } 142 String messageKey = IndentationCheck.MSG_ERROR; 143 if (expectedIndent.isMultiLevel()) { 144 messageKey = IndentationCheck.MSG_ERROR_MULTI; 145 } 146 indentCheck.indentationLog(ast.getLineNo(), messageKey, 147 typeName + typeStr, actualIndent, expectedIndent); 148 } 149 150 /** 151 * Log child indentation error. 152 * 153 * @param line the expression that caused the error 154 * @param actualIndent the actual indent level of the expression 155 * @param expectedIndent the expected indent level of the expression 156 */ 157 private void logChildError(int line, 158 int actualIndent, 159 IndentLevel expectedIndent) { 160 String messageKey = IndentationCheck.MSG_CHILD_ERROR; 161 if (expectedIndent.isMultiLevel()) { 162 messageKey = IndentationCheck.MSG_CHILD_ERROR_MULTI; 163 } 164 indentCheck.indentationLog(line, messageKey, 165 typeName, actualIndent, expectedIndent); 166 } 167 168 /** 169 * Determines if the given expression is at the start of a line. 170 * 171 * @param ast the expression to check 172 * 173 * @return true if it is, false otherwise 174 */ 175 protected final boolean isOnStartOfLine(DetailAST ast) { 176 return getLineStart(ast) == expandedTabsColumnNo(ast); 177 } 178 179 /** 180 * Searches in given sub-tree (including given node) for the token 181 * which represents first symbol for this sub-tree in file. 182 * 183 * @param ast a root of sub-tree in which the search should be performed. 184 * @return a token which occurs first in the file. 185 * @noinspection WeakerAccess 186 */ 187 public static DetailAST getFirstToken(DetailAST ast) { 188 DetailAST first = ast; 189 DetailAST child = ast.getFirstChild(); 190 191 while (child != null) { 192 final DetailAST toTest = getFirstToken(child); 193 if (toTest.getColumnNo() < first.getColumnNo()) { 194 first = toTest; 195 } 196 child = child.getNextSibling(); 197 } 198 199 return first; 200 } 201 202 /** 203 * Get the start of the line for the given expression. 204 * 205 * @param ast the expression to find the start of the line for 206 * 207 * @return the start of the line for the given expression 208 */ 209 protected final int getLineStart(DetailAST ast) { 210 return getLineStart(ast.getLineNo()); 211 } 212 213 /** 214 * Get the start of the line for the given line number. 215 * 216 * @param lineNo the line number to find the start for 217 * 218 * @return the start of the line for the given expression 219 */ 220 protected final int getLineStart(int lineNo) { 221 return getLineStart(indentCheck.getLine(lineNo - 1)); 222 } 223 224 /** 225 * Get the start of the specified line. 226 * 227 * @param line the specified line number 228 * 229 * @return the start of the specified line 230 */ 231 private int getLineStart(String line) { 232 int index = 0; 233 while (Character.isWhitespace(line.charAt(index))) { 234 index++; 235 } 236 return CommonUtil.lengthExpandedTabs( 237 line, index, indentCheck.getIndentationTabWidth()); 238 } 239 240 /** 241 * Checks that indentation should be increased after first line in checkLinesIndent(). 242 * 243 * @return true if indentation should be increased after 244 * first line in checkLinesIndent() 245 * false otherwise 246 */ 247 protected boolean shouldIncreaseIndent() { 248 return true; 249 } 250 251 /** 252 * Check the indentation for a set of lines. 253 * 254 * @param lines the set of lines to check 255 * @param indentLevel the indentation level 256 * @param firstLineMatches whether or not the first line has to match 257 * @param firstLine first line of whole expression 258 */ 259 private void checkLinesIndent(LineSet lines, 260 IndentLevel indentLevel, 261 boolean firstLineMatches, 262 int firstLine) { 263 if (!lines.isEmpty()) { 264 // check first line 265 final int startLine = lines.firstLine(); 266 final int endLine = lines.lastLine(); 267 final int startCol = lines.firstLineCol(); 268 269 final int realStartCol = 270 getLineStart(indentCheck.getLine(startLine - 1)); 271 272 if (realStartCol == startCol) { 273 checkLineIndent(startLine, startCol, indentLevel, 274 firstLineMatches); 275 } 276 277 // if first line starts the line, following lines are indented 278 // one level; but if the first line of this expression is 279 // nested with the previous expression (which is assumed if it 280 // doesn't start the line) then don't indent more, the first 281 // indentation is absorbed by the nesting 282 283 IndentLevel theLevel = indentLevel; 284 if (firstLineMatches 285 || firstLine > mainAst.getLineNo() && shouldIncreaseIndent()) { 286 theLevel = new IndentLevel(indentLevel, getBasicOffset()); 287 } 288 289 // check following lines 290 for (int i = startLine + 1; i <= endLine; i++) { 291 final Integer col = lines.getStartColumn(i); 292 // startCol could be null if this line didn't have an 293 // expression that was required to be checked (it could be 294 // checked by a child expression) 295 296 if (col != null) { 297 checkLineIndent(i, col, theLevel, false); 298 } 299 } 300 } 301 } 302 303 /** 304 * Check the indentation for a single line. 305 * 306 * @param lineNum the number of the line to check 307 * @param colNum the column number we are starting at 308 * @param indentLevel the indentation level 309 * @param mustMatch whether or not the indentation level must match 310 */ 311 private void checkLineIndent(int lineNum, int colNum, 312 IndentLevel indentLevel, boolean mustMatch) { 313 final String line = indentCheck.getLine(lineNum - 1); 314 final int start = getLineStart(line); 315 // if must match is set, it is a violation if the line start is not 316 // at the correct indention level; otherwise, it is an only an 317 // violation if this statement starts the line and it is less than 318 // the correct indentation level 319 if (mustMatch && !indentLevel.isAcceptable(start) 320 || !mustMatch && colNum == start && indentLevel.isGreaterThan(start)) { 321 logChildError(lineNum, start, indentLevel); 322 } 323 } 324 325 /** 326 * Checks indentation on wrapped lines between and including 327 * {@code firstNode} and {@code lastNode}. 328 * 329 * @param firstNode First node to start examining. 330 * @param lastNode Last node to examine inclusively. 331 */ 332 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) { 333 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode); 334 } 335 336 /** 337 * Checks indentation on wrapped lines between and including 338 * {@code firstNode} and {@code lastNode}. 339 * 340 * @param firstNode First node to start examining. 341 * @param lastNode Last node to examine inclusively. 342 * @param wrappedIndentLevel Indentation all wrapped lines should use. 343 * @param startIndent Indentation first line before wrapped lines used. 344 * @param ignoreFirstLine Test if first line's indentation should be checked or not. 345 */ 346 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode, 347 int wrappedIndentLevel, int startIndent, boolean ignoreFirstLine) { 348 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode, 349 wrappedIndentLevel, startIndent, 350 LineWrappingHandler.LineWrappingOptions.ofBoolean(ignoreFirstLine)); 351 } 352 353 /** 354 * Check the indent level of the children of the specified parent 355 * expression. 356 * 357 * @param parentNode the parent whose children we are checking 358 * @param tokenTypes the token types to check 359 * @param startIndent the starting indent level 360 * @param firstLineMatches whether or not the first line needs to match 361 * @param allowNesting whether or not nested children are allowed 362 */ 363 protected final void checkChildren(DetailAST parentNode, 364 int[] tokenTypes, 365 IndentLevel startIndent, 366 boolean firstLineMatches, 367 boolean allowNesting) { 368 Arrays.sort(tokenTypes); 369 for (DetailAST child = parentNode.getFirstChild(); 370 child != null; 371 child = child.getNextSibling()) { 372 if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0) { 373 checkExpressionSubtree(child, startIndent, 374 firstLineMatches, allowNesting); 375 } 376 } 377 } 378 379 /** 380 * Check the indentation level for an expression subtree. 381 * 382 * @param tree the expression subtree to check 383 * @param indentLevel the indentation level 384 * @param firstLineMatches whether or not the first line has to match 385 * @param allowNesting whether or not subtree nesting is allowed 386 */ 387 protected final void checkExpressionSubtree( 388 DetailAST tree, 389 IndentLevel indentLevel, 390 boolean firstLineMatches, 391 boolean allowNesting 392 ) { 393 final LineSet subtreeLines = new LineSet(); 394 final int firstLine = getFirstLine(Integer.MAX_VALUE, tree); 395 if (firstLineMatches && !allowNesting) { 396 subtreeLines.addLineAndCol(firstLine, 397 getLineStart(indentCheck.getLine(firstLine - 1))); 398 } 399 findSubtreeLines(subtreeLines, tree, allowNesting); 400 401 checkLinesIndent(subtreeLines, indentLevel, firstLineMatches, firstLine); 402 } 403 404 /** 405 * Get the first line for a given expression. 406 * 407 * @param startLine the line we are starting from 408 * @param tree the expression to find the first line for 409 * 410 * @return the first line of the expression 411 */ 412 protected static int getFirstLine(int startLine, DetailAST tree) { 413 int realStart = startLine; 414 final int currLine = tree.getLineNo(); 415 if (currLine < realStart) { 416 realStart = currLine; 417 } 418 419 // check children 420 for (DetailAST node = tree.getFirstChild(); 421 node != null; 422 node = node.getNextSibling()) { 423 realStart = getFirstLine(realStart, node); 424 } 425 426 return realStart; 427 } 428 429 /** 430 * Get the column number for the start of a given expression, expanding 431 * tabs out into spaces in the process. 432 * 433 * @param ast the expression to find the start of 434 * 435 * @return the column number for the start of the expression 436 */ 437 protected final int expandedTabsColumnNo(DetailAST ast) { 438 final String line = 439 indentCheck.getLine(ast.getLineNo() - 1); 440 441 return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(), 442 indentCheck.getIndentationTabWidth()); 443 } 444 445 /** 446 * Find the set of lines for a given subtree. 447 * 448 * @param lines the set of lines to add to 449 * @param tree the subtree to examine 450 * @param allowNesting whether or not to allow nested subtrees 451 */ 452 protected final void findSubtreeLines(LineSet lines, DetailAST tree, 453 boolean allowNesting) { 454 if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) { 455 final int lineNum = tree.getLineNo(); 456 final Integer colNum = lines.getStartColumn(lineNum); 457 458 final int thisLineColumn = expandedTabsColumnNo(tree); 459 if (colNum == null || thisLineColumn < colNum) { 460 lines.addLineAndCol(lineNum, thisLineColumn); 461 } 462 463 // check children 464 for (DetailAST node = tree.getFirstChild(); 465 node != null; 466 node = node.getNextSibling()) { 467 findSubtreeLines(lines, node, allowNesting); 468 } 469 } 470 } 471 472 /** 473 * Check the indentation level of modifiers. 474 */ 475 protected void checkModifiers() { 476 final DetailAST modifiers = 477 mainAst.findFirstToken(TokenTypes.MODIFIERS); 478 for (DetailAST modifier = modifiers.getFirstChild(); 479 modifier != null; 480 modifier = modifier.getNextSibling()) { 481 if (isOnStartOfLine(modifier) 482 && !getIndent().isAcceptable(expandedTabsColumnNo(modifier))) { 483 logError(modifier, "modifier", 484 expandedTabsColumnNo(modifier)); 485 } 486 } 487 } 488 489 /** 490 * Accessor for the IndentCheck attribute. 491 * 492 * @return the IndentCheck attribute 493 */ 494 protected final IndentationCheck getIndentCheck() { 495 return indentCheck; 496 } 497 498 /** 499 * Accessor for the MainAst attribute. 500 * 501 * @return the MainAst attribute 502 */ 503 protected final DetailAST getMainAst() { 504 return mainAst; 505 } 506 507 /** 508 * Accessor for the Parent attribute. 509 * 510 * @return the Parent attribute 511 */ 512 protected final AbstractExpressionHandler getParent() { 513 return parent; 514 } 515 516 /** 517 * A shortcut for {@code IndentationCheck} property. 518 * 519 * @return value of basicOffset property of {@code IndentationCheck} 520 */ 521 protected final int getBasicOffset() { 522 return indentCheck.getBasicOffset(); 523 } 524 525 /** 526 * A shortcut for {@code IndentationCheck} property. 527 * 528 * @return value of braceAdjustment property 529 * of {@code IndentationCheck} 530 */ 531 protected final int getBraceAdjustment() { 532 return indentCheck.getBraceAdjustment(); 533 } 534 535 /** 536 * Check the indentation of the right parenthesis. 537 * 538 * @param rparen parenthesis to check 539 * @param lparen left parenthesis associated with aRparen 540 */ 541 protected final void checkRightParen(DetailAST lparen, DetailAST rparen) { 542 if (rparen != null) { 543 // the rcurly can either be at the correct indentation, 544 // or not first on the line 545 final int rparenLevel = expandedTabsColumnNo(rparen); 546 // or has <lparen level> + 1 indentation 547 final int lparenLevel = expandedTabsColumnNo(lparen); 548 549 if (rparenLevel != lparenLevel + 1 550 && !getIndent().isAcceptable(rparenLevel) 551 && isOnStartOfLine(rparen)) { 552 logError(rparen, "rparen", rparenLevel); 553 } 554 } 555 } 556 557 /** 558 * Check the indentation of the left parenthesis. 559 * 560 * @param lparen parenthesis to check 561 */ 562 protected final void checkLeftParen(final DetailAST lparen) { 563 // the rcurly can either be at the correct indentation, or on the 564 // same line as the lcurly 565 if (lparen != null 566 && !getIndent().isAcceptable(expandedTabsColumnNo(lparen)) 567 && isOnStartOfLine(lparen)) { 568 logError(lparen, "lparen", expandedTabsColumnNo(lparen)); 569 } 570 } 571 572}