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