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