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 indentation context providing config values, source lines, and
38       * violation reporting without a direct reference to the check.
39       */
40      private final IndentationContext context;
41  
42      /** The AST which is handled by this handler. */
43      private final DetailAST mainAst;
44  
45      /** Name used during output to user. */
46      private final String typeName;
47  
48      /** Containing AST handler. */
49      private final AbstractExpressionHandler parent;
50  
51      /** Indentation amount for this handler. */
52      private IndentLevel indent;
53  
54      /**
55       * Construct an instance of this handler with the given indentation context,
56       * name, abstract syntax tree, and parent handler.
57       *
58       * @param context   the indentation context
59       * @param typeName  the name of the handler
60       * @param expr      the abstract syntax tree
61       * @param parent    the parent handler
62       */
63      protected AbstractExpressionHandler(IndentationContext context, String typeName,
64              DetailAST expr, AbstractExpressionHandler parent) {
65          this.context = context;
66          this.typeName = typeName;
67          mainAst = expr;
68          this.parent = parent;
69      }
70  
71      /**
72       * Check the indentation of the expression we are handling.
73       */
74      public abstract void checkIndentation();
75  
76      /**
77       * Get the indentation amount for this handler. For performance reasons,
78       * this value is cached. The first time this method is called, the
79       * indentation amount is computed and stored. On further calls, the stored
80       * value is returned.
81       *
82       * @return the expected indentation amount
83       * @noinspection WeakerAccess
84       * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
85       */
86      public final IndentLevel getIndent() {
87          if (indent == null) {
88              indent = getIndentImpl();
89          }
90          return indent;
91      }
92  
93      /**
94       * Compute the indentation amount for this handler.
95       *
96       * @return the expected indentation amount
97       */
98      protected IndentLevel getIndentImpl() {
99          return parent.getSuggestedChildIndent(this);
100     }
101 
102     /**
103      * Indentation level suggested for a child element. Children don't have
104      * to respect this, but most do.
105      *
106      * @param child  child AST (so suggestion level can differ based on child
107      *                  type)
108      *
109      * @return suggested indentation for child
110      * @noinspection WeakerAccess
111      * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
112      */
113     public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
114         return new IndentLevel(getIndent(), getBasicOffset());
115     }
116 
117     /**
118      * Log an indentation error.
119      *
120      * @param ast           the expression that caused the error
121      * @param subtypeName   the type of the expression
122      * @param actualIndent  the actual indent level of the expression
123      */
124     protected final void logError(DetailAST ast, String subtypeName,
125                                   int actualIndent) {
126         logError(ast, subtypeName, actualIndent, getIndent());
127     }
128 
129     /**
130      * Log an indentation error.
131      *
132      * @param ast            the expression that caused the error
133      * @param subtypeName    the type of the expression
134      * @param actualIndent   the actual indent level of the expression
135      * @param expectedIndent the expected indent level of the expression
136      */
137     protected final void logError(DetailAST ast, String subtypeName,
138                                   int actualIndent, IndentLevel expectedIndent) {
139         final String typeStr;
140 
141         if (subtypeName.isEmpty()) {
142             typeStr = "";
143         }
144         else {
145             typeStr = " " + subtypeName;
146         }
147         String messageKey = IndentationContext.MSG_ERROR;
148         if (expectedIndent.isMultiLevel()) {
149             messageKey = IndentationContext.MSG_ERROR_MULTI;
150         }
151         context.indentationLog(ast, messageKey,
152             typeName + typeStr, actualIndent, expectedIndent);
153     }
154 
155     /**
156      * Log child indentation error.
157      *
158      * @param ast            the abstract syntax tree that causes the error
159      * @param actualIndent   the actual indent level of the expression
160      * @param expectedIndent the expected indent level of the expression
161      */
162     protected final void logChildError(DetailAST ast,
163                                        int actualIndent,
164                                        IndentLevel expectedIndent) {
165         String messageKey = IndentationContext.MSG_CHILD_ERROR;
166         if (expectedIndent.isMultiLevel()) {
167             messageKey = IndentationContext.MSG_CHILD_ERROR_MULTI;
168         }
169         context.indentationLog(ast, messageKey,
170             typeName, actualIndent, expectedIndent);
171     }
172 
173     /**
174      * Determines if the given expression is at the start of a line.
175      *
176      * @param ast   the expression to check
177      *
178      * @return true if it is, false otherwise
179      */
180     protected final boolean isOnStartOfLine(DetailAST ast) {
181         return getLineStart(ast) == expandedTabsColumnNo(ast);
182     }
183 
184     /**
185      * Searches in given subtree (including given node) for the token
186      * which represents first symbol for this subtree in file.
187      *
188      * @param ast a root of subtree in which the search should be performed.
189      * @return a token which occurs first in the file.
190      * @noinspection WeakerAccess
191      * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
192      */
193     public static DetailAST getFirstToken(DetailAST ast) {
194         DetailAST first = ast;
195         DetailAST child = ast.getFirstChild();
196 
197         while (child != null) {
198             final DetailAST toTest = getFirstToken(child);
199             if (toTest.getColumnNo() < first.getColumnNo()) {
200                 first = toTest;
201             }
202             child = child.getNextSibling();
203         }
204 
205         return first;
206     }
207 
208     /**
209      * Get the start of the line for the given expression.
210      *
211      * @param ast   the expression to find the start of the line for
212      *
213      * @return the start of the line for the given expression
214      */
215     protected final int getLineStart(DetailAST ast) {
216         return getLineStart(ast.getLineNo());
217     }
218 
219     /**
220      * Get the start of the line for the given line number.
221      *
222      * @param lineNo   the line number to find the start for
223      *
224      * @return the start of the line for the given expression
225      */
226     protected final int getLineStart(int lineNo) {
227         return getLineStart(context.getLine(lineNo - 1));
228     }
229 
230     /**
231      * Get the start of the specified line.
232      *
233      * @param line   the specified line number
234      *
235      * @return the start of the specified line
236      */
237     private int getLineStart(String line) {
238         int index = 0;
239         while (Character.isWhitespace(line.charAt(index))) {
240             index++;
241         }
242         return CommonUtil.lengthExpandedTabs(
243             line, index, context.getIndentationTabWidth());
244     }
245 
246     /**
247      * Checks that indentation should be increased after first line in checkLinesIndent().
248      *
249      * @return true if indentation should be increased after
250      *              first line in checkLinesIndent()
251      *         false otherwise
252      */
253     protected boolean shouldIncreaseIndent() {
254         boolean result = true;
255         if (TokenUtil.isOfType(mainAst, TokenTypes.LITERAL_CATCH)) {
256             final DetailAST parameterAst = mainAst.findFirstToken(TokenTypes.PARAMETER_DEF);
257             result = !AnnotationUtil.containsAnnotation(parameterAst);
258         }
259         return result;
260     }
261 
262     /**
263      * Check the indentation for a set of lines.
264      *
265      * @param astSet             the set of abstract syntax tree to check
266      * @param indentLevel        the indentation level
267      * @param firstLineMatches   whether or not the first line has to match
268      * @param firstLine          first line of whole expression
269      * @param allowNesting       whether or not subtree nesting is allowed
270      */
271     private void checkLinesIndent(DetailAstSet astSet,
272                                   IndentLevel indentLevel,
273                                   boolean firstLineMatches,
274                                   int firstLine,
275                                   boolean allowNesting) {
276         if (!astSet.isEmpty()) {
277             // check first line
278             final DetailAST startLineAst = astSet.firstLine();
279             int startCol = expandedTabsColumnNo(startLineAst);
280 
281             final int realStartCol =
282                 getLineStart(context.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             checkRemainingLines(firstLineMatches, indentLevel, firstLine, astSet);
294 
295         }
296     }
297 
298     /**
299      * Check the indentation of remaining lines present in the astSet.
300      *
301      * @param firstLineMatches   whether or not the first line has to match
302      * @param indentLevel        the indentation level
303      * @param firstLine          first line of whole expression
304      * @param astSet             the set of abstract syntax tree to check
305      */
306     private void checkRemainingLines(boolean firstLineMatches,
307                                      IndentLevel indentLevel,
308                                      int firstLine,
309                                      DetailAstSet astSet) {
310         // if first line starts the line, following lines are indented
311         // one level; but if the first line of this expression is
312         // nested with the previous expression (which is assumed if it
313         // doesn't start the line) then don't indent more, the first
314         // indentation is absorbed by the nesting
315         final DetailAST startLineAst = astSet.firstLine();
316         final int endLine = astSet.lastLine();
317         IndentLevel level = indentLevel;
318 
319         if (shouldIncreaseIndent()
320                 && startLineAst.getType() != TokenTypes.ANNOTATION
321                 && (firstLineMatches || firstLine > mainAst.getLineNo())) {
322             level = new IndentLevel(indentLevel,
323                     context.getLineWrappingIndentation());
324         }
325 
326         // check following lines
327         for (int index = startLineAst.getLineNo() + 1; index <= endLine; index++) {
328             final Integer col = astSet.getStartColumn(index);
329             // startCol could be null if this line didn't have an
330             // expression that was required to be checked (it could be
331             // checked by a child expression)
332 
333             if (col != null) {
334                 final DetailAST ast = astSet.getAst(index);
335                 final boolean textBlockEndAligned =
336                     ast.getType() == TokenTypes.TEXT_BLOCK_LITERAL_END
337                         && isOnStartOfLine(ast.getParent())
338                         && indentLevel.isAcceptable(expandedTabsColumnNo(ast));
339                 if (!textBlockEndAligned) {
340                     checkLineIndent(ast, level, false);
341                 }
342             }
343         }
344     }
345 
346     /**
347      * Check the indentation for a single-line.
348      *
349      * @param ast           the abstract syntax tree to check
350      * @param indentLevel   the indentation level
351      * @param mustMatch     whether or not the indentation level must match
352      */
353     private void checkLineIndent(DetailAST ast,
354         IndentLevel indentLevel, boolean mustMatch) {
355         final String line = context.getLine(ast.getLineNo() - 1);
356         final int start = getLineStart(line);
357         final int columnNumber = expandedTabsColumnNo(ast);
358         // if must match is set, it is a violation if the line start is not
359         // at the correct indention level; otherwise, it is an only a
360         // violation if this statement starts the line and it is less than
361         // the correct indentation level
362         if (mustMatch && !indentLevel.isAcceptable(start)
363                 || !mustMatch && columnNumber == start && indentLevel.isGreaterThan(start)) {
364             logChildError(ast, start, indentLevel);
365         }
366     }
367 
368     /**
369      * Checks indentation on wrapped lines between and including
370      * {@code firstNode} and {@code lastNode}.
371      *
372      * @param firstNode First node to start examining.
373      * @param lastNode Last node to examine inclusively.
374      */
375     protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) {
376         context.getLineWrappingHandler().checkIndentation(firstNode, lastNode);
377     }
378 
379     /**
380      * Checks indentation on wrapped lines between and including
381      * {@code firstNode} and {@code lastNode}.
382      *
383      * @param firstNode First node to start examining.
384      * @param lastNode Last node to examine inclusively.
385      * @param wrappedIndentLevel Indentation all wrapped lines should use.
386      * @param startIndent Indentation first line before wrapped lines used.
387      * @param ignoreFirstLine Test if first line's indentation should be checked or not.
388      */
389     protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode,
390             int wrappedIndentLevel, int startIndent,
391             LineWrappingHandler.LineWrappingOptions ignoreFirstLine) {
392         context.getLineWrappingHandler().checkIndentation(firstNode, lastNode,
393                 wrappedIndentLevel, startIndent, ignoreFirstLine);
394     }
395 
396     /**
397      * Check the indent level of the children of the specified parent
398      * expression.
399      *
400      * @param parentNode         the parent whose children we are checking
401      * @param tokenTypes         the token types to check
402      * @param startIndent        the starting indent level
403      * @param firstLineMatches   whether or not the first line needs to match
404      * @param allowNesting       whether or not nested children are allowed
405      */
406     protected final void checkChildren(DetailAST parentNode,
407                                        int[] tokenTypes,
408                                        IndentLevel startIndent,
409                                        boolean firstLineMatches,
410                                        boolean allowNesting) {
411         Arrays.sort(tokenTypes);
412         for (DetailAST child = parentNode.getFirstChild();
413                 child != null;
414                 child = child.getNextSibling()) {
415             if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0
416                     && shouldCheckIndentationForChild(child)) {
417                 checkExpressionSubtree(child, startIndent,
418                     firstLineMatches, allowNesting);
419             }
420         }
421     }
422 
423     /**
424      * Decide whether to check indentation for a specific child.
425      *
426      * @param child child AST node
427      * @return true if indentation should be checked
428      */
429     protected boolean shouldCheckIndentationForChild(DetailAST child) {
430         return true;
431     }
432 
433     /**
434      * Check the indentation level for an expression subtree.
435      *
436      * @param tree               the expression subtree to check
437      * @param indentLevel        the indentation level
438      * @param firstLineMatches   whether or not the first line has to match
439      * @param allowNesting       whether or not subtree nesting is allowed
440      */
441     protected final void checkExpressionSubtree(
442         DetailAST tree,
443         IndentLevel indentLevel,
444         boolean firstLineMatches,
445         boolean allowNesting
446     ) {
447         final DetailAstSet subtreeAst = new DetailAstSet(context);
448         final int firstLine = getFirstLine(tree);
449         if (firstLineMatches && !allowNesting) {
450             final DetailAST firstAst = getFirstAstNode(tree);
451             subtreeAst.addAst(firstAst);
452         }
453         findSubtreeAst(subtreeAst, tree, allowNesting);
454 
455         checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
456     }
457 
458     /**
459      * Get the first line number for given expression.
460      *
461      * @param tree      the expression to find the first line for
462      * @return          the first line of expression
463      */
464     protected static int getFirstLine(DetailAST tree) {
465         return getFirstAstNode(tree).getLineNo();
466     }
467 
468     /**
469      * Get the first ast for given expression.
470      *
471      * @param ast         the abstract syntax tree for which the starting ast is to be found
472      *
473      * @return            the first ast of the expression
474      */
475     protected static DetailAST getFirstAstNode(DetailAST ast) {
476 
477         DetailAST curNode = ast;
478         DetailAST realStart = ast;
479         while (curNode != null) {
480             if (curNode.getLineNo() < realStart.getLineNo()
481                     || curNode.getLineNo() == realStart.getLineNo()
482                     && Math.min(curNode.getColumnNo(), realStart.getColumnNo())
483                     == curNode.getColumnNo()) {
484                 realStart = curNode;
485             }
486             DetailAST toVisit = curNode.getFirstChild();
487             while (curNode != ast && toVisit == null) {
488                 toVisit = curNode.getNextSibling();
489                 curNode = curNode.getParent();
490             }
491             curNode = toVisit;
492         }
493         return realStart;
494     }
495 
496     /**
497      * Get the column number for the start of a given expression, expanding
498      * tabs out into spaces in the process.
499      *
500      * @param ast   the expression to find the start of
501      *
502      * @return the column number for the start of the expression
503      */
504     protected final int expandedTabsColumnNo(DetailAST ast) {
505         final String line =
506             context.getLine(ast.getLineNo() - 1);
507 
508         return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
509             context.getIndentationTabWidth());
510     }
511 
512     /**
513      * Find the set of abstract syntax tree for a given subtree.
514      *
515      * @param astSet         the set of ast to add
516      * @param tree           the subtree to examine
517      * @param allowNesting   whether or not to allow nested subtrees
518      */
519     protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
520         boolean allowNesting) {
521         if (!context.getHandlerFactory().isHandledType(tree.getType())) {
522             final int lineNum = tree.getLineNo();
523             final Integer colNum = astSet.getStartColumn(lineNum);
524 
525             final int thisLineColumn = expandedTabsColumnNo(tree);
526             if (colNum == null || thisLineColumn < colNum) {
527                 astSet.addAst(tree);
528             }
529 
530             // check children
531             for (DetailAST node = tree.getFirstChild();
532                 node != null;
533                 node = node.getNextSibling()) {
534                 findSubtreeAst(astSet, node, allowNesting);
535             }
536         }
537     }
538 
539     /**
540      * Accessor for the indentation context.
541      *
542      * @return the indentation context
543      */
544     protected final IndentationContext getContext() {
545         return context;
546     }
547 
548     /**
549      * Accessor for the MainAst attribute.
550      *
551      * @return the MainAst attribute
552      */
553     protected final DetailAST getMainAst() {
554         return mainAst;
555     }
556 
557     /**
558      * Accessor for the Parent attribute.
559      *
560      * @return the Parent attribute
561      */
562     protected final AbstractExpressionHandler getParent() {
563         return parent;
564     }
565 
566     /**
567      * A shortcut for {@code basicOffset} property.
568      *
569      * @return value of basicOffset property
570      */
571     protected final int getBasicOffset() {
572         return context.getBasicOffset();
573     }
574 
575     /**
576      * A shortcut for {@code braceAdjustment} property.
577      *
578      * @return value of braceAdjustment property
579      */
580     protected final int getBraceAdjustment() {
581         return context.getBraceAdjustment();
582     }
583 
584     /**
585      * Check the indentation of the right parenthesis.
586      *
587      * @param lparen left parenthesis associated with aRparen
588      * @param rparen parenthesis to check
589      */
590     protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
591         if (rparen != null) {
592             // the rcurly can either be at the correct indentation,
593             // or not first on the line
594             final int rparenLevel = expandedTabsColumnNo(rparen);
595             // or has <lparen level> + 1 indentation
596             final int lparenLevel = expandedTabsColumnNo(lparen);
597 
598             if (rparenLevel != lparenLevel + 1
599                     && !getIndent().isAcceptable(rparenLevel)
600                     && isOnStartOfLine(rparen)) {
601                 logError(rparen, "rparen", rparenLevel);
602             }
603         }
604     }
605 
606     /**
607      * Check the indentation of the left parenthesis.
608      *
609      * @param lparen parenthesis to check
610      */
611     protected final void checkLeftParen(final DetailAST lparen) {
612         // the rcurly can either be at the correct indentation, or on the
613         // same line as the lcurly
614         if (lparen != null
615                 && !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
616                 && isOnStartOfLine(lparen)) {
617             logError(lparen, "lparen", expandedTabsColumnNo(lparen));
618         }
619     }
620 
621 }