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 indentation context providing config values, source lines, and
38 * violation reporting without a direct reference to the check.
39 */
40 private final IndentationContext context;
41
42 /** The AST which is handled by this handler. */
43 private final DetailAST mainAst;
44
45 /** Name used during output to user. */
46 private final String typeName;
47
48 /** Containing AST handler. */
49 private final AbstractExpressionHandler parent;
50
51 /** Indentation amount for this handler. */
52 private IndentLevel indent;
53
54 /**
55 * Construct an instance of this handler with the given indentation context,
56 * name, abstract syntax tree, and parent handler.
57 *
58 * @param context the indentation context
59 * @param typeName the name of the handler
60 * @param expr the abstract syntax tree
61 * @param parent the parent handler
62 */
63 protected AbstractExpressionHandler(IndentationContext context, String typeName,
64 DetailAST expr, AbstractExpressionHandler parent) {
65 this.context = context;
66 this.typeName = typeName;
67 mainAst = expr;
68 this.parent = parent;
69 }
70
71 /**
72 * Check the indentation of the expression we are handling.
73 */
74 public abstract void checkIndentation();
75
76 /**
77 * Get the indentation amount for this handler. For performance reasons,
78 * this value is cached. The first time this method is called, the
79 * indentation amount is computed and stored. On further calls, the stored
80 * value is returned.
81 *
82 * @return the expected indentation amount
83 * @noinspection WeakerAccess
84 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
85 */
86 public final IndentLevel getIndent() {
87 if (indent == null) {
88 indent = getIndentImpl();
89 }
90 return indent;
91 }
92
93 /**
94 * Compute the indentation amount for this handler.
95 *
96 * @return the expected indentation amount
97 */
98 protected IndentLevel getIndentImpl() {
99 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 }