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