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