1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.indentation;
21
22 import java.util.Arrays;
23
24 import com.puppycrawl.tools.checkstyle.api.DetailAST;
25 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
26 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
27 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
28 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
29
30 /**
31 * Abstract base class for all handlers.
32 *
33 */
34 public abstract class AbstractExpressionHandler {
35
36 /**
37 * The instance of {@code IndentationCheck} using this handler.
38 */
39 private final IndentationCheck indentCheck;
40
41 /** The AST which is handled by this handler. */
42 private final DetailAST mainAst;
43
44 /** Name used during output to user. */
45 private final String typeName;
46
47 /** Containing AST handler. */
48 private final AbstractExpressionHandler parent;
49
50 /** Indentation amount for this handler. */
51 private IndentLevel indent;
52
53 /**
54 * Construct an instance of this handler with the given indentation check,
55 * name, abstract syntax tree, and parent handler.
56 *
57 * @param indentCheck the indentation check
58 * @param typeName the name of the handler
59 * @param expr the abstract syntax tree
60 * @param parent the parent handler
61 */
62 protected AbstractExpressionHandler(IndentationCheck indentCheck, String typeName,
63 DetailAST expr, AbstractExpressionHandler parent) {
64 this.indentCheck = indentCheck;
65 this.typeName = typeName;
66 mainAst = expr;
67 this.parent = parent;
68 }
69
70 /**
71 * Check the indentation of the expression we are handling.
72 */
73 public abstract void checkIndentation();
74
75 /**
76 * Get the indentation amount for this handler. For performance reasons,
77 * this value is cached. The first time this method is called, the
78 * indentation amount is computed and stored. On further calls, the stored
79 * value is returned.
80 *
81 * @return the expected indentation amount
82 * @noinspection WeakerAccess
83 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
84 */
85 public final IndentLevel getIndent() {
86 if (indent == null) {
87 indent = getIndentImpl();
88 }
89 return indent;
90 }
91
92 /**
93 * Compute the indentation amount for this handler.
94 *
95 * @return the expected indentation amount
96 */
97 protected IndentLevel getIndentImpl() {
98 return parent.getSuggestedChildIndent(this);
99 }
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
106 * type)
107 *
108 * @return suggested indentation for child
109 * @noinspection WeakerAccess
110 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
111 */
112 public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
113 return new IndentLevel(getIndent(), getBasicOffset());
114 }
115
116 /**
117 * Log an indentation error.
118 *
119 * @param ast the expression that caused the error
120 * @param subtypeName the type of the expression
121 * @param actualIndent the actual indent level of the expression
122 */
123 protected final void logError(DetailAST ast, String subtypeName,
124 int actualIndent) {
125 logError(ast, subtypeName, actualIndent, getIndent());
126 }
127
128 /**
129 * Log an indentation error.
130 *
131 * @param ast the expression that caused the error
132 * @param subtypeName the type of the expression
133 * @param actualIndent the actual indent level of the expression
134 * @param expectedIndent the expected indent level of the expression
135 */
136 protected final void logError(DetailAST ast, String subtypeName,
137 int actualIndent, IndentLevel expectedIndent) {
138 final String typeStr;
139
140 if (subtypeName.isEmpty()) {
141 typeStr = "";
142 }
143 else {
144 typeStr = " " + subtypeName;
145 }
146 String messageKey = IndentationCheck.MSG_ERROR;
147 if (expectedIndent.isMultiLevel()) {
148 messageKey = IndentationCheck.MSG_ERROR_MULTI;
149 }
150 indentCheck.indentationLog(ast, messageKey,
151 typeName + typeStr, actualIndent, expectedIndent);
152 }
153
154 /**
155 * Log child indentation error.
156 *
157 * @param ast the abstract syntax tree that causes the error
158 * @param actualIndent the actual indent level of the expression
159 * @param expectedIndent the expected indent level of the expression
160 */
161 private void logChildError(DetailAST ast,
162 int actualIndent,
163 IndentLevel expectedIndent) {
164 String messageKey = IndentationCheck.MSG_CHILD_ERROR;
165 if (expectedIndent.isMultiLevel()) {
166 messageKey = IndentationCheck.MSG_CHILD_ERROR_MULTI;
167 }
168 indentCheck.indentationLog(ast, messageKey,
169 typeName, actualIndent, expectedIndent);
170 }
171
172 /**
173 * Determines if the given expression is at the start of a line.
174 *
175 * @param ast the expression to check
176 *
177 * @return true if it is, false otherwise
178 */
179 protected final boolean isOnStartOfLine(DetailAST ast) {
180 return getLineStart(ast) == expandedTabsColumnNo(ast);
181 }
182
183 /**
184 * Searches in given subtree (including given node) for the token
185 * which represents first symbol for this subtree in file.
186 *
187 * @param ast a root of subtree in which the search should be performed.
188 * @return a token which occurs first in the file.
189 * @noinspection WeakerAccess
190 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
191 */
192 public static DetailAST getFirstToken(DetailAST ast) {
193 DetailAST first = ast;
194 DetailAST child = ast.getFirstChild();
195
196 while (child != null) {
197 final DetailAST toTest = getFirstToken(child);
198 if (toTest.getColumnNo() < first.getColumnNo()) {
199 first = toTest;
200 }
201 child = child.getNextSibling();
202 }
203
204 return first;
205 }
206
207 /**
208 * Get the start of the line for the given expression.
209 *
210 * @param ast the expression to find the start of the line for
211 *
212 * @return the start of the line for the given expression
213 */
214 protected final int getLineStart(DetailAST ast) {
215 return getLineStart(ast.getLineNo());
216 }
217
218 /**
219 * Get the start of the line for the given line number.
220 *
221 * @param lineNo the line number to find the start for
222 *
223 * @return the start of the line for the given expression
224 */
225 protected final int getLineStart(int lineNo) {
226 return getLineStart(indentCheck.getLine(lineNo - 1));
227 }
228
229 /**
230 * Get the start of the specified line.
231 *
232 * @param line the specified line number
233 *
234 * @return the start of the specified line
235 */
236 private int getLineStart(String line) {
237 int index = 0;
238 while (Character.isWhitespace(line.charAt(index))) {
239 index++;
240 }
241 return CommonUtil.lengthExpandedTabs(
242 line, index, indentCheck.getIndentationTabWidth());
243 }
244
245 /**
246 * Checks that indentation should be increased after first line in checkLinesIndent().
247 *
248 * @return true if indentation should be increased after
249 * first line in checkLinesIndent()
250 * false otherwise
251 */
252 protected boolean shouldIncreaseIndent() {
253 boolean result = true;
254 if (TokenUtil.isOfType(mainAst, TokenTypes.LITERAL_CATCH)) {
255 final DetailAST parameterAst = mainAst.findFirstToken(TokenTypes.PARAMETER_DEF);
256 result = !AnnotationUtil.containsAnnotation(parameterAst);
257 }
258 return result;
259 }
260
261 /**
262 * Check the indentation for a set of lines.
263 *
264 * @param astSet the set of abstract syntax tree to check
265 * @param indentLevel the indentation level
266 * @param firstLineMatches whether or not the first line has to match
267 * @param firstLine first line of whole expression
268 * @param allowNesting whether or not subtree nesting is allowed
269 */
270 private void checkLinesIndent(DetailAstSet astSet,
271 IndentLevel indentLevel,
272 boolean firstLineMatches,
273 int firstLine,
274 boolean allowNesting) {
275 if (!astSet.isEmpty()) {
276 // check first line
277 final DetailAST startLineAst = astSet.firstLine();
278 int startCol = expandedTabsColumnNo(startLineAst);
279
280 final int realStartCol =
281 getLineStart(indentCheck.getLine(startLineAst.getLineNo() - 1));
282
283 if (firstLineMatches && !allowNesting) {
284 startCol = realStartCol;
285 }
286
287 if (realStartCol == startCol) {
288 checkLineIndent(startLineAst, indentLevel,
289 firstLineMatches);
290 }
291
292 checkRemainingLines(firstLineMatches, indentLevel, firstLine, astSet);
293
294 }
295 }
296
297 /**
298 * Check the indentation of remaining lines present in the astSet.
299 *
300 * @param firstLineMatches whether or not the first line has to match
301 * @param indentLevel the indentation level
302 * @param firstLine first line of whole expression
303 * @param astSet the set of abstract syntax tree to check
304 */
305 private void checkRemainingLines(boolean firstLineMatches,
306 IndentLevel indentLevel,
307 int firstLine,
308 DetailAstSet astSet) {
309 // if first line starts the line, following lines are indented
310 // one level; but if the first line of this expression is
311 // nested with the previous expression (which is assumed if it
312 // doesn't start the line) then don't indent more, the first
313 // indentation is absorbed by the nesting
314 final DetailAST startLineAst = astSet.firstLine();
315 final int endLine = astSet.lastLine();
316 IndentLevel level = indentLevel;
317
318 if (shouldIncreaseIndent()
319 && startLineAst.getType() != TokenTypes.ANNOTATION
320 && (firstLineMatches || firstLine > mainAst.getLineNo())) {
321 level = new IndentLevel(indentLevel,
322 indentCheck.getLineWrappingIndentation());
323 }
324
325 // check following lines
326 for (int index = startLineAst.getLineNo() + 1; index <= endLine; index++) {
327 final Integer col = astSet.getStartColumn(index);
328 // startCol could be null if this line didn't have an
329 // expression that was required to be checked (it could be
330 // checked by a child expression)
331
332 if (col != null) {
333 final DetailAST ast = astSet.getAst(index);
334 final boolean textBlockEndAligned =
335 ast.getType() == TokenTypes.TEXT_BLOCK_LITERAL_END
336 && isOnStartOfLine(ast.getParent())
337 && indentLevel.isAcceptable(expandedTabsColumnNo(ast));
338 if (!textBlockEndAligned) {
339 checkLineIndent(ast, level, false);
340 }
341 }
342 }
343 }
344
345 /**
346 * Check the indentation for a single-line.
347 *
348 * @param ast the abstract syntax tree to check
349 * @param indentLevel the indentation level
350 * @param mustMatch whether or not the indentation level must match
351 */
352 private void checkLineIndent(DetailAST ast,
353 IndentLevel indentLevel, boolean mustMatch) {
354 final String line = indentCheck.getLine(ast.getLineNo() - 1);
355 final int start = getLineStart(line);
356 final int columnNumber = expandedTabsColumnNo(ast);
357 // if must match is set, it is a violation if the line start is not
358 // at the correct indention level; otherwise, it is an only a
359 // violation if this statement starts the line and it is less than
360 // the correct indentation level
361 if (mustMatch && !indentLevel.isAcceptable(start)
362 || !mustMatch && columnNumber == start && indentLevel.isGreaterThan(start)) {
363 logChildError(ast, start, indentLevel);
364 }
365 }
366
367 /**
368 * Checks indentation on wrapped lines between and including
369 * {@code firstNode} and {@code lastNode}.
370 *
371 * @param firstNode First node to start examining.
372 * @param lastNode Last node to examine inclusively.
373 */
374 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) {
375 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode);
376 }
377
378 /**
379 * Checks indentation on wrapped lines between and including
380 * {@code firstNode} and {@code lastNode}.
381 *
382 * @param firstNode First node to start examining.
383 * @param lastNode Last node to examine inclusively.
384 * @param wrappedIndentLevel Indentation all wrapped lines should use.
385 * @param startIndent Indentation first line before wrapped lines used.
386 * @param ignoreFirstLine Test if first line's indentation should be checked or not.
387 */
388 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode,
389 int wrappedIndentLevel, int startIndent,
390 LineWrappingHandler.LineWrappingOptions ignoreFirstLine) {
391 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode,
392 wrappedIndentLevel, startIndent, ignoreFirstLine);
393 }
394
395 /**
396 * Check the indent level of the children of the specified parent
397 * expression.
398 *
399 * @param parentNode the parent whose children we are checking
400 * @param tokenTypes the token types to check
401 * @param startIndent the starting indent level
402 * @param firstLineMatches whether or not the first line needs to match
403 * @param allowNesting whether or not nested children are allowed
404 */
405 protected final void checkChildren(DetailAST parentNode,
406 int[] tokenTypes,
407 IndentLevel startIndent,
408 boolean firstLineMatches,
409 boolean allowNesting) {
410 Arrays.sort(tokenTypes);
411 for (DetailAST child = parentNode.getFirstChild();
412 child != null;
413 child = child.getNextSibling()) {
414 if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0
415 && shouldCheckIndentationForChild(child)) {
416 checkExpressionSubtree(child, startIndent,
417 firstLineMatches, allowNesting);
418 }
419 }
420 }
421
422 /**
423 * Decide whether to check indentation for a specific child.
424 *
425 * @param child child AST node
426 * @return true if indentation should be checked
427 */
428 protected boolean shouldCheckIndentationForChild(DetailAST child) {
429 return true;
430 }
431
432 /**
433 * Check the indentation level for an expression subtree.
434 *
435 * @param tree the expression subtree to check
436 * @param indentLevel the indentation level
437 * @param firstLineMatches whether or not the first line has to match
438 * @param allowNesting whether or not subtree nesting is allowed
439 */
440 protected final void checkExpressionSubtree(
441 DetailAST tree,
442 IndentLevel indentLevel,
443 boolean firstLineMatches,
444 boolean allowNesting
445 ) {
446 final DetailAstSet subtreeAst = new DetailAstSet(indentCheck);
447 final int firstLine = getFirstLine(tree);
448 if (firstLineMatches && !allowNesting) {
449 final DetailAST firstAst = getFirstAstNode(tree);
450 subtreeAst.addAst(firstAst);
451 }
452 findSubtreeAst(subtreeAst, tree, allowNesting);
453
454 checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
455 }
456
457 /**
458 * Get the first line number for given expression.
459 *
460 * @param tree the expression to find the first line for
461 * @return the first line of expression
462 */
463 protected static int getFirstLine(DetailAST tree) {
464 return getFirstAstNode(tree).getLineNo();
465 }
466
467 /**
468 * Get the first ast for given expression.
469 *
470 * @param ast the abstract syntax tree for which the starting ast is to be found
471 *
472 * @return the first ast of the expression
473 */
474 protected static DetailAST getFirstAstNode(DetailAST ast) {
475
476 DetailAST curNode = ast;
477 DetailAST realStart = ast;
478 while (curNode != null) {
479 if (curNode.getLineNo() < realStart.getLineNo()
480 || curNode.getLineNo() == realStart.getLineNo()
481 && Math.min(curNode.getColumnNo(), realStart.getColumnNo())
482 == curNode.getColumnNo()) {
483 realStart = curNode;
484 }
485 DetailAST toVisit = curNode.getFirstChild();
486 while (curNode != ast && toVisit == null) {
487 toVisit = curNode.getNextSibling();
488 curNode = curNode.getParent();
489 }
490 curNode = toVisit;
491 }
492 return realStart;
493 }
494
495 /**
496 * Get the column number for the start of a given expression, expanding
497 * tabs out into spaces in the process.
498 *
499 * @param ast the expression to find the start of
500 *
501 * @return the column number for the start of the expression
502 */
503 protected final int expandedTabsColumnNo(DetailAST ast) {
504 final String line =
505 indentCheck.getLine(ast.getLineNo() - 1);
506
507 return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
508 indentCheck.getIndentationTabWidth());
509 }
510
511 /**
512 * Find the set of abstract syntax tree for a given subtree.
513 *
514 * @param astSet the set of ast to add
515 * @param tree the subtree to examine
516 * @param allowNesting whether or not to allow nested subtrees
517 */
518 protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
519 boolean allowNesting) {
520 if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) {
521 final int lineNum = tree.getLineNo();
522 final Integer colNum = astSet.getStartColumn(lineNum);
523
524 final int thisLineColumn = expandedTabsColumnNo(tree);
525 if (colNum == null || thisLineColumn < colNum) {
526 astSet.addAst(tree);
527 }
528
529 // check children
530 for (DetailAST node = tree.getFirstChild();
531 node != null;
532 node = node.getNextSibling()) {
533 findSubtreeAst(astSet, node, allowNesting);
534 }
535 }
536 }
537
538 /**
539 * Accessor for the IndentCheck attribute.
540 *
541 * @return the IndentCheck attribute
542 */
543 protected final IndentationCheck getIndentCheck() {
544 return indentCheck;
545 }
546
547 /**
548 * Accessor for the MainAst attribute.
549 *
550 * @return the MainAst attribute
551 */
552 protected final DetailAST getMainAst() {
553 return mainAst;
554 }
555
556 /**
557 * Accessor for the Parent attribute.
558 *
559 * @return the Parent attribute
560 */
561 protected final AbstractExpressionHandler getParent() {
562 return parent;
563 }
564
565 /**
566 * A shortcut for {@code IndentationCheck} property.
567 *
568 * @return value of basicOffset property of {@code IndentationCheck}
569 */
570 protected final int getBasicOffset() {
571 return indentCheck.getBasicOffset();
572 }
573
574 /**
575 * A shortcut for {@code IndentationCheck} property.
576 *
577 * @return value of braceAdjustment property
578 * of {@code IndentationCheck}
579 */
580 protected final int getBraceAdjustment() {
581 return indentCheck.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 }