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