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