View Javadoc
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 }