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                 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                     && Math.min(curNode.getColumnNo(), realStart.getColumnNo())
464                     == curNode.getColumnNo()) {
465                 realStart = curNode;
466             }
467             DetailAST toVisit = curNode.getFirstChild();
468             while (curNode != ast && toVisit == null) {
469                 toVisit = curNode.getNextSibling();
470                 curNode = curNode.getParent();
471             }
472             curNode = toVisit;
473         }
474         return realStart;
475     }
476 
477     /**
478      * Get the column number for the start of a given expression, expanding
479      * tabs out into spaces in the process.
480      *
481      * @param ast   the expression to find the start of
482      *
483      * @return the column number for the start of the expression
484      */
485     protected final int expandedTabsColumnNo(DetailAST ast) {
486         final String line =
487             indentCheck.getLine(ast.getLineNo() - 1);
488 
489         return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
490             indentCheck.getIndentationTabWidth());
491     }
492 
493     /**
494      * Find the set of abstract syntax tree for a given subtree.
495      *
496      * @param astSet         the set of ast to add
497      * @param tree           the subtree to examine
498      * @param allowNesting   whether or not to allow nested subtrees
499      */
500     protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
501         boolean allowNesting) {
502         if (!indentCheck.getHandlerFactory().isHandledType(tree.getType())) {
503             final int lineNum = tree.getLineNo();
504             final Integer colNum = astSet.getStartColumn(lineNum);
505 
506             final int thisLineColumn = expandedTabsColumnNo(tree);
507             if (colNum == null || thisLineColumn < colNum) {
508                 astSet.addAst(tree);
509             }
510 
511             // check children
512             for (DetailAST node = tree.getFirstChild();
513                 node != null;
514                 node = node.getNextSibling()) {
515                 findSubtreeAst(astSet, node, allowNesting);
516             }
517         }
518     }
519 
520     /**
521      * Check the indentation level of modifiers.
522      */
523     protected void checkModifiers() {
524         final DetailAST modifiers =
525             mainAst.findFirstToken(TokenTypes.MODIFIERS);
526         for (DetailAST modifier = modifiers.getFirstChild();
527              modifier != null;
528              modifier = modifier.getNextSibling()) {
529             if (isOnStartOfLine(modifier)
530                 && !getIndent().isAcceptable(expandedTabsColumnNo(modifier))) {
531                 logError(modifier, "modifier",
532                     expandedTabsColumnNo(modifier));
533             }
534         }
535     }
536 
537     /**
538      * Accessor for the IndentCheck attribute.
539      *
540      * @return the IndentCheck attribute
541      */
542     protected final IndentationCheck getIndentCheck() {
543         return indentCheck;
544     }
545 
546     /**
547      * Accessor for the MainAst attribute.
548      *
549      * @return the MainAst attribute
550      */
551     protected final DetailAST getMainAst() {
552         return mainAst;
553     }
554 
555     /**
556      * Accessor for the Parent attribute.
557      *
558      * @return the Parent attribute
559      */
560     protected final AbstractExpressionHandler getParent() {
561         return parent;
562     }
563 
564     /**
565      * A shortcut for {@code IndentationCheck} property.
566      *
567      * @return value of basicOffset property of {@code IndentationCheck}
568      */
569     protected final int getBasicOffset() {
570         return indentCheck.getBasicOffset();
571     }
572 
573     /**
574      * A shortcut for {@code IndentationCheck} property.
575      *
576      * @return value of braceAdjustment property
577      *         of {@code IndentationCheck}
578      */
579     protected final int getBraceAdjustment() {
580         return indentCheck.getBraceAdjustment();
581     }
582 
583     /**
584      * Check the indentation of the right parenthesis.
585      *
586      * @param lparen left parenthesis associated with aRparen
587      * @param rparen parenthesis to check
588      */
589     protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
590         if (rparen != null) {
591             // the rcurly can either be at the correct indentation,
592             // or not first on the line
593             final int rparenLevel = expandedTabsColumnNo(rparen);
594             // or has <lparen level> + 1 indentation
595             final int lparenLevel = expandedTabsColumnNo(lparen);
596 
597             if (rparenLevel != lparenLevel + 1
598                     && !getIndent().isAcceptable(rparenLevel)
599                     && isOnStartOfLine(rparen)) {
600                 logError(rparen, "rparen", rparenLevel);
601             }
602         }
603     }
604 
605     /**
606      * Check the indentation of the left parenthesis.
607      *
608      * @param lparen parenthesis to check
609      */
610     protected final void checkLeftParen(final DetailAST lparen) {
611         // the rcurly can either be at the correct indentation, or on the
612         // same line as the lcurly
613         if (lparen != null
614                 && !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
615                 && isOnStartOfLine(lparen)) {
616             logError(lparen, "lparen", expandedTabsColumnNo(lparen));
617         }
618     }
619 
620 }