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                 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                     && shouldCheckIndentationForChild(child)) {
409                 checkExpressionSubtree(child, startIndent,
410                     firstLineMatches, allowNesting);
411             }
412         }
413     }
414 
415     /**
416      * Decide whether to check indentation for a specific child.
417      *
418      * @param child child AST node
419      * @return true if indentation should be checked
420      */
421     protected boolean shouldCheckIndentationForChild(DetailAST child) {
422         return true;
423     }
424 
425     /**
426      * Check the indentation level for an expression subtree.
427      *
428      * @param tree               the expression subtree to check
429      * @param indentLevel        the indentation level
430      * @param firstLineMatches   whether or not the first line has to match
431      * @param allowNesting       whether or not subtree nesting is allowed
432      */
433     protected final void checkExpressionSubtree(
434         DetailAST tree,
435         IndentLevel indentLevel,
436         boolean firstLineMatches,
437         boolean allowNesting
438     ) {
439         final DetailAstSet subtreeAst = new DetailAstSet(indentCheck);
440         final int firstLine = getFirstLine(tree);
441         if (firstLineMatches && !allowNesting) {
442             final DetailAST firstAst = getFirstAstNode(tree);
443             subtreeAst.addAst(firstAst);
444         }
445         findSubtreeAst(subtreeAst, tree, allowNesting);
446 
447         checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
448     }
449 
450     /**
451      * Get the first line number for given expression.
452      *
453      * @param tree      the expression to find the first line for
454      * @return          the first line of expression
455      */
456     protected static int getFirstLine(DetailAST tree) {
457         return getFirstAstNode(tree).getLineNo();
458     }
459 
460     /**
461      * Get the first ast for given expression.
462      *
463      * @param ast         the abstract syntax tree for which the starting ast is to be found
464      *
465      * @return            the first ast of the expression
466      */
467     protected static DetailAST getFirstAstNode(DetailAST ast) {
468 
469         DetailAST curNode = ast;
470         DetailAST realStart = ast;
471         while (curNode != null) {
472             if (curNode.getLineNo() < realStart.getLineNo()
473                     || curNode.getLineNo() == realStart.getLineNo()
474                     && Math.min(curNode.getColumnNo(), realStart.getColumnNo())
475                     == curNode.getColumnNo()) {
476                 realStart = curNode;
477             }
478             DetailAST toVisit = curNode.getFirstChild();
479             while (curNode != ast && toVisit == null) {
480                 toVisit = curNode.getNextSibling();
481                 curNode = curNode.getParent();
482             }
483             curNode = toVisit;
484         }
485         return realStart;
486     }
487 
488     /**
489      * Get the column number for the start of a given expression, expanding
490      * tabs out into spaces in the process.
491      *
492      * @param ast   the expression to find the start of
493      *
494      * @return the column number for the start of the expression
495      */
496     protected final int expandedTabsColumnNo(DetailAST ast) {
497         final String line =
498             indentCheck.getLine(ast.getLineNo() - 1);
499 
500         return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
501             indentCheck.getIndentationTabWidth());
502     }
503 
504     /**
505      * Find the set of abstract syntax tree for a given subtree.
506      *
507      * @param astSet         the set of ast to add
508      * @param tree           the subtree to examine
509      * @param allowNesting   whether or not to allow nested subtrees
510      */
511     protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
512         boolean allowNesting) {
513         if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) {
514             final int lineNum = tree.getLineNo();
515             final Integer colNum = astSet.getStartColumn(lineNum);
516 
517             final int thisLineColumn = expandedTabsColumnNo(tree);
518             if (colNum == null || thisLineColumn < colNum) {
519                 astSet.addAst(tree);
520             }
521 
522             // check children
523             for (DetailAST node = tree.getFirstChild();
524                 node != null;
525                 node = node.getNextSibling()) {
526                 findSubtreeAst(astSet, node, allowNesting);
527             }
528         }
529     }
530 
531     /**
532      * Check the indentation level of modifiers.
533      */
534     protected void checkModifiers() {
535         final DetailAST modifiers =
536             mainAst.findFirstToken(TokenTypes.MODIFIERS);
537         for (DetailAST modifier = modifiers.getFirstChild();
538              modifier != null;
539              modifier = modifier.getNextSibling()) {
540             if (isOnStartOfLine(modifier)
541                 && !getIndent().isAcceptable(expandedTabsColumnNo(modifier))) {
542                 logError(modifier, "modifier",
543                     expandedTabsColumnNo(modifier));
544             }
545         }
546     }
547 
548     /**
549      * Accessor for the IndentCheck attribute.
550      *
551      * @return the IndentCheck attribute
552      */
553     protected final IndentationCheck getIndentCheck() {
554         return indentCheck;
555     }
556 
557     /**
558      * Accessor for the MainAst attribute.
559      *
560      * @return the MainAst attribute
561      */
562     protected final DetailAST getMainAst() {
563         return mainAst;
564     }
565 
566     /**
567      * Accessor for the Parent attribute.
568      *
569      * @return the Parent attribute
570      */
571     protected final AbstractExpressionHandler getParent() {
572         return parent;
573     }
574 
575     /**
576      * A shortcut for {@code IndentationCheck} property.
577      *
578      * @return value of basicOffset property of {@code IndentationCheck}
579      */
580     protected final int getBasicOffset() {
581         return indentCheck.getBasicOffset();
582     }
583 
584     /**
585      * A shortcut for {@code IndentationCheck} property.
586      *
587      * @return value of braceAdjustment property
588      *         of {@code IndentationCheck}
589      */
590     protected final int getBraceAdjustment() {
591         return indentCheck.getBraceAdjustment();
592     }
593 
594     /**
595      * Check the indentation of the right parenthesis.
596      *
597      * @param lparen left parenthesis associated with aRparen
598      * @param rparen parenthesis to check
599      */
600     protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
601         if (rparen != null) {
602             // the rcurly can either be at the correct indentation,
603             // or not first on the line
604             final int rparenLevel = expandedTabsColumnNo(rparen);
605             // or has <lparen level> + 1 indentation
606             final int lparenLevel = expandedTabsColumnNo(lparen);
607 
608             if (rparenLevel != lparenLevel + 1
609                     && !getIndent().isAcceptable(rparenLevel)
610                     && isOnStartOfLine(rparen)) {
611                 logError(rparen, "rparen", rparenLevel);
612             }
613         }
614     }
615 
616     /**
617      * Check the indentation of the left parenthesis.
618      *
619      * @param lparen parenthesis to check
620      */
621     protected final void checkLeftParen(final DetailAST lparen) {
622         // the rcurly can either be at the correct indentation, or on the
623         // same line as the lcurly
624         if (lparen != null
625                 && !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
626                 && isOnStartOfLine(lparen)) {
627             logError(lparen, "lparen", expandedTabsColumnNo(lparen));
628         }
629     }
630 
631 }