1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 checkLineIndent(astSet.getAst(index), level, false);
334 }
335 }
336 }
337
338 /**
339 * Check the indentation for a single-line.
340 *
341 * @param ast the abstract syntax tree to check
342 * @param indentLevel the indentation level
343 * @param mustMatch whether or not the indentation level must match
344 */
345 private void checkLineIndent(DetailAST ast,
346 IndentLevel indentLevel, boolean mustMatch) {
347 final String line = indentCheck.getLine(ast.getLineNo() - 1);
348 final int start = getLineStart(line);
349 final int columnNumber = expandedTabsColumnNo(ast);
350 // if must match is set, it is a violation if the line start is not
351 // at the correct indention level; otherwise, it is an only a
352 // violation if this statement starts the line and it is less than
353 // the correct indentation level
354 if (mustMatch && !indentLevel.isAcceptable(start)
355 || !mustMatch && columnNumber == start && indentLevel.isGreaterThan(start)) {
356 logChildError(ast, start, indentLevel);
357 }
358 }
359
360 /**
361 * Checks indentation on wrapped lines between and including
362 * {@code firstNode} and {@code lastNode}.
363 *
364 * @param firstNode First node to start examining.
365 * @param lastNode Last node to examine inclusively.
366 */
367 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) {
368 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode);
369 }
370
371 /**
372 * Checks indentation on wrapped lines between and including
373 * {@code firstNode} and {@code lastNode}.
374 *
375 * @param firstNode First node to start examining.
376 * @param lastNode Last node to examine inclusively.
377 * @param wrappedIndentLevel Indentation all wrapped lines should use.
378 * @param startIndent Indentation first line before wrapped lines used.
379 * @param ignoreFirstLine Test if first line's indentation should be checked or not.
380 */
381 protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode,
382 int wrappedIndentLevel, int startIndent, boolean ignoreFirstLine) {
383 indentCheck.getLineWrappingHandler().checkIndentation(firstNode, lastNode,
384 wrappedIndentLevel, startIndent,
385 LineWrappingHandler.LineWrappingOptions.ofBoolean(ignoreFirstLine));
386 }
387
388 /**
389 * Check the indent level of the children of the specified parent
390 * expression.
391 *
392 * @param parentNode the parent whose children we are checking
393 * @param tokenTypes the token types to check
394 * @param startIndent the starting indent level
395 * @param firstLineMatches whether or not the first line needs to match
396 * @param allowNesting whether or not nested children are allowed
397 */
398 protected final void checkChildren(DetailAST parentNode,
399 int[] tokenTypes,
400 IndentLevel startIndent,
401 boolean firstLineMatches,
402 boolean allowNesting) {
403 Arrays.sort(tokenTypes);
404 for (DetailAST child = parentNode.getFirstChild();
405 child != null;
406 child = child.getNextSibling()) {
407 if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0) {
408 checkExpressionSubtree(child, startIndent,
409 firstLineMatches, allowNesting);
410 }
411 }
412 }
413
414 /**
415 * Check the indentation level for an expression subtree.
416 *
417 * @param tree the expression subtree to check
418 * @param indentLevel the indentation level
419 * @param firstLineMatches whether or not the first line has to match
420 * @param allowNesting whether or not subtree nesting is allowed
421 */
422 protected final void checkExpressionSubtree(
423 DetailAST tree,
424 IndentLevel indentLevel,
425 boolean firstLineMatches,
426 boolean allowNesting
427 ) {
428 final DetailAstSet subtreeAst = new DetailAstSet(indentCheck);
429 final int firstLine = getFirstLine(tree);
430 if (firstLineMatches && !allowNesting) {
431 final DetailAST firstAst = getFirstAstNode(tree);
432 subtreeAst.addAst(firstAst);
433 }
434 findSubtreeAst(subtreeAst, tree, allowNesting);
435
436 checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
437 }
438
439 /**
440 * Get the first line number for given expression.
441 *
442 * @param tree the expression to find the first line for
443 * @return the first line of expression
444 */
445 protected static int getFirstLine(DetailAST tree) {
446 return getFirstAstNode(tree).getLineNo();
447 }
448
449 /**
450 * Get the first ast for given expression.
451 *
452 * @param ast the abstract syntax tree for which the starting ast is to be found
453 *
454 * @return the first ast of the expression
455 */
456 protected static DetailAST getFirstAstNode(DetailAST ast) {
457
458 DetailAST curNode = ast;
459 DetailAST realStart = ast;
460 while (curNode != null) {
461 if (curNode.getLineNo() < realStart.getLineNo()
462 || curNode.getLineNo() == realStart.getLineNo()
463 && curNode.getColumnNo() < realStart.getColumnNo()) {
464 realStart = curNode;
465 }
466 DetailAST toVisit = curNode.getFirstChild();
467 while (curNode != ast && toVisit == null) {
468 toVisit = curNode.getNextSibling();
469 curNode = curNode.getParent();
470 }
471 curNode = toVisit;
472 }
473 return realStart;
474 }
475
476 /**
477 * Get the column number for the start of a given expression, expanding
478 * tabs out into spaces in the process.
479 *
480 * @param ast the expression to find the start of
481 *
482 * @return the column number for the start of the expression
483 */
484 protected final int expandedTabsColumnNo(DetailAST ast) {
485 final String line =
486 indentCheck.getLine(ast.getLineNo() - 1);
487
488 return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
489 indentCheck.getIndentationTabWidth());
490 }
491
492 /**
493 * Find the set of abstract syntax tree for a given subtree.
494 *
495 * @param astSet the set of ast to add
496 * @param tree the subtree to examine
497 * @param allowNesting whether or not to allow nested subtrees
498 */
499 protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
500 boolean allowNesting) {
501 if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) {
502 final int lineNum = tree.getLineNo();
503 final Integer colNum = astSet.getStartColumn(lineNum);
504
505 final int thisLineColumn = expandedTabsColumnNo(tree);
506 if (colNum == null || thisLineColumn < colNum) {
507 astSet.addAst(tree);
508 }
509
510 // check children
511 for (DetailAST node = tree.getFirstChild();
512 node != null;
513 node = node.getNextSibling()) {
514 findSubtreeAst(astSet, node, allowNesting);
515 }
516 }
517 }
518
519 /**
520 * Check the indentation level of modifiers.
521 */
522 protected void checkModifiers() {
523 final DetailAST modifiers =
524 mainAst.findFirstToken(TokenTypes.MODIFIERS);
525 for (DetailAST modifier = modifiers.getFirstChild();
526 modifier != null;
527 modifier = modifier.getNextSibling()) {
528 if (isOnStartOfLine(modifier)
529 && !getIndent().isAcceptable(expandedTabsColumnNo(modifier))) {
530 logError(modifier, "modifier",
531 expandedTabsColumnNo(modifier));
532 }
533 }
534 }
535
536 /**
537 * Accessor for the IndentCheck attribute.
538 *
539 * @return the IndentCheck attribute
540 */
541 protected final IndentationCheck getIndentCheck() {
542 return indentCheck;
543 }
544
545 /**
546 * Accessor for the MainAst attribute.
547 *
548 * @return the MainAst attribute
549 */
550 protected final DetailAST getMainAst() {
551 return mainAst;
552 }
553
554 /**
555 * Accessor for the Parent attribute.
556 *
557 * @return the Parent attribute
558 */
559 protected final AbstractExpressionHandler getParent() {
560 return parent;
561 }
562
563 /**
564 * A shortcut for {@code IndentationCheck} property.
565 *
566 * @return value of basicOffset property of {@code IndentationCheck}
567 */
568 protected final int getBasicOffset() {
569 return indentCheck.getBasicOffset();
570 }
571
572 /**
573 * A shortcut for {@code IndentationCheck} property.
574 *
575 * @return value of braceAdjustment property
576 * of {@code IndentationCheck}
577 */
578 protected final int getBraceAdjustment() {
579 return indentCheck.getBraceAdjustment();
580 }
581
582 /**
583 * Check the indentation of the right parenthesis.
584 *
585 * @param lparen left parenthesis associated with aRparen
586 * @param rparen parenthesis to check
587 */
588 protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
589 if (rparen != null) {
590 // the rcurly can either be at the correct indentation,
591 // or not first on the line
592 final int rparenLevel = expandedTabsColumnNo(rparen);
593 // or has <lparen level> + 1 indentation
594 final int lparenLevel = expandedTabsColumnNo(lparen);
595
596 if (rparenLevel != lparenLevel + 1
597 && !getIndent().isAcceptable(rparenLevel)
598 && isOnStartOfLine(rparen)) {
599 logError(rparen, "rparen", rparenLevel);
600 }
601 }
602 }
603
604 /**
605 * Check the indentation of the left parenthesis.
606 *
607 * @param lparen parenthesis to check
608 */
609 protected final void checkLeftParen(final DetailAST lparen) {
610 // the rcurly can either be at the correct indentation, or on the
611 // same line as the lcurly
612 if (lparen != null
613 && !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
614 && isOnStartOfLine(lparen)) {
615 logError(lparen, "lparen", expandedTabsColumnNo(lparen));
616 }
617 }
618
619 }