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