View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.coding;
21  
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import com.puppycrawl.tools.checkstyle.StatelessCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
30  
31  /**
32   * Checks for fall through in switch statements
33   * Finds locations where a case <b>contains</b> Java code -
34   * but lacks a break, return, throw or continue statement.
35   *
36   * <p>
37   * The check honors special comments to suppress warnings about
38   * the fall through. By default the comments "fallthru",
39   * "fall through", "falls through" and "fallthrough" are recognized.
40   * </p>
41   * <p>
42   * The following fragment of code will NOT trigger the check,
43   * because of the comment "fallthru" and absence of any Java code
44   * in case 5.
45   * </p>
46   * <pre>
47   * case 3:
48   *     x = 2;
49   *     // fallthru
50   * case 4:
51   * case 5:
52   * case 6:
53   *     break;
54   * </pre>
55   * <p>
56   * The recognized relief comment can be configured with the property
57   * {@code reliefPattern}. Default value of this regular expression
58   * is "fallthru|fall through|fallthrough|falls through".
59   * </p>
60   * <p>
61   * An example of how to configure the check is:
62   * </p>
63   * <pre>
64   * &lt;module name="FallThrough"&gt;
65   *     &lt;property name=&quot;reliefPattern&quot;
66   *                  value=&quot;Fall Through&quot;/&gt;
67   * &lt;/module&gt;
68   * </pre>
69   *
70   * @author o_sukhodolsky
71   */
72  @StatelessCheck
73  public class FallThroughCheck extends AbstractCheck {
74  
75      /**
76       * A key is pointing to the warning message text in "messages.properties"
77       * file.
78       */
79      public static final String MSG_FALL_THROUGH = "fall.through";
80  
81      /**
82       * A key is pointing to the warning message text in "messages.properties"
83       * file.
84       */
85      public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
86  
87      /** Do we need to check last case group. */
88      private boolean checkLastCaseGroup;
89  
90      /** Relief regexp to allow fall through to the next case branch. */
91      private Pattern reliefPattern = Pattern.compile("fallthru|falls? ?through");
92  
93      @Override
94      public int[] getDefaultTokens() {
95          return getRequiredTokens();
96      }
97  
98      @Override
99      public int[] getRequiredTokens() {
100         return new int[] {TokenTypes.CASE_GROUP};
101     }
102 
103     @Override
104     public int[] getAcceptableTokens() {
105         return getRequiredTokens();
106     }
107 
108     /**
109      * Set the relief pattern.
110      *
111      * @param pattern
112      *            The regular expression pattern.
113      */
114     public void setReliefPattern(Pattern pattern) {
115         reliefPattern = pattern;
116     }
117 
118     /**
119      * Configures whether we need to check last case group or not.
120      * @param value new value of the property.
121      */
122     public void setCheckLastCaseGroup(boolean value) {
123         checkLastCaseGroup = value;
124     }
125 
126     @Override
127     public void visitToken(DetailAST ast) {
128         final DetailAST nextGroup = ast.getNextSibling();
129         final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
130         if (!isLastGroup || checkLastCaseGroup) {
131             final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
132 
133             if (slist != null && !isTerminated(slist, true, true)
134                 && !hasFallThroughComment(ast, nextGroup)) {
135                 if (isLastGroup) {
136                     log(ast, MSG_FALL_THROUGH_LAST);
137                 }
138                 else {
139                     log(nextGroup, MSG_FALL_THROUGH);
140                 }
141             }
142         }
143     }
144 
145     /**
146      * Checks if a given subtree terminated by return, throw or,
147      * if allowed break, continue.
148      * @param ast root of given subtree
149      * @param useBreak should we consider break as terminator.
150      * @param useContinue should we consider continue as terminator.
151      * @return true if the subtree is terminated.
152      */
153     private boolean isTerminated(final DetailAST ast, boolean useBreak,
154                                  boolean useContinue) {
155         final boolean terminated;
156 
157         switch (ast.getType()) {
158             case TokenTypes.LITERAL_RETURN:
159             case TokenTypes.LITERAL_THROW:
160                 terminated = true;
161                 break;
162             case TokenTypes.LITERAL_BREAK:
163                 terminated = useBreak;
164                 break;
165             case TokenTypes.LITERAL_CONTINUE:
166                 terminated = useContinue;
167                 break;
168             case TokenTypes.SLIST:
169                 terminated = checkSlist(ast, useBreak, useContinue);
170                 break;
171             case TokenTypes.LITERAL_IF:
172                 terminated = checkIf(ast, useBreak, useContinue);
173                 break;
174             case TokenTypes.LITERAL_FOR:
175             case TokenTypes.LITERAL_WHILE:
176             case TokenTypes.LITERAL_DO:
177                 terminated = checkLoop(ast);
178                 break;
179             case TokenTypes.LITERAL_TRY:
180                 terminated = checkTry(ast, useBreak, useContinue);
181                 break;
182             case TokenTypes.LITERAL_SWITCH:
183                 terminated = checkSwitch(ast, useContinue);
184                 break;
185             case TokenTypes.LITERAL_SYNCHRONIZED:
186                 terminated = checkSynchronized(ast, useBreak, useContinue);
187                 break;
188             default:
189                 terminated = false;
190         }
191         return terminated;
192     }
193 
194     /**
195      * Checks if a given SLIST terminated by return, throw or,
196      * if allowed break, continue.
197      * @param slistAst SLIST to check
198      * @param useBreak should we consider break as terminator.
199      * @param useContinue should we consider continue as terminator.
200      * @return true if SLIST is terminated.
201      */
202     private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
203                                boolean useContinue) {
204         DetailAST lastStmt = slistAst.getLastChild();
205 
206         if (lastStmt.getType() == TokenTypes.RCURLY) {
207             lastStmt = lastStmt.getPreviousSibling();
208         }
209 
210         return lastStmt != null
211             && isTerminated(lastStmt, useBreak, useContinue);
212     }
213 
214     /**
215      * Checks if a given IF terminated by return, throw or,
216      * if allowed break, continue.
217      * @param ast IF to check
218      * @param useBreak should we consider break as terminator.
219      * @param useContinue should we consider continue as terminator.
220      * @return true if IF is terminated.
221      */
222     private boolean checkIf(final DetailAST ast, boolean useBreak,
223                             boolean useContinue) {
224         final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
225                 .getNextSibling();
226         final DetailAST elseStmt = thenStmt.getNextSibling();
227         boolean isTerminated = isTerminated(thenStmt, useBreak, useContinue);
228 
229         if (isTerminated && elseStmt != null) {
230             isTerminated = isTerminated(elseStmt.getFirstChild(),
231                 useBreak, useContinue);
232         }
233         else if (elseStmt == null) {
234             isTerminated = false;
235         }
236         return isTerminated;
237     }
238 
239     /**
240      * Checks if a given loop terminated by return, throw or,
241      * if allowed break, continue.
242      * @param ast loop to check
243      * @return true if loop is terminated.
244      */
245     private boolean checkLoop(final DetailAST ast) {
246         final DetailAST loopBody;
247         if (ast.getType() == TokenTypes.LITERAL_DO) {
248             final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
249             loopBody = lparen.getPreviousSibling();
250         }
251         else {
252             final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
253             loopBody = rparen.getNextSibling();
254         }
255         return isTerminated(loopBody, false, false);
256     }
257 
258     /**
259      * Checks if a given try/catch/finally block terminated by return, throw or,
260      * if allowed break, continue.
261      * @param ast loop to check
262      * @param useBreak should we consider break as terminator.
263      * @param useContinue should we consider continue as terminator.
264      * @return true if try/catch/finally block is terminated.
265      */
266     private boolean checkTry(final DetailAST ast, boolean useBreak,
267                              boolean useContinue) {
268         final DetailAST finalStmt = ast.getLastChild();
269         boolean isTerminated = false;
270         if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
271             isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
272                                 useBreak, useContinue);
273         }
274 
275         if (!isTerminated) {
276             DetailAST firstChild = ast.getFirstChild();
277 
278             if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
279                 firstChild = firstChild.getNextSibling();
280             }
281 
282             isTerminated = isTerminated(firstChild,
283                     useBreak, useContinue);
284 
285             DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
286             while (catchStmt != null
287                     && isTerminated
288                     && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
289                 final DetailAST catchBody =
290                         catchStmt.findFirstToken(TokenTypes.SLIST);
291                 isTerminated = isTerminated(catchBody, useBreak, useContinue);
292                 catchStmt = catchStmt.getNextSibling();
293             }
294         }
295         return isTerminated;
296     }
297 
298     /**
299      * Checks if a given switch terminated by return, throw or,
300      * if allowed break, continue.
301      * @param literalSwitchAst loop to check
302      * @param useContinue should we consider continue as terminator.
303      * @return true if switch is terminated.
304      */
305     private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
306         DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
307         boolean isTerminated = caseGroup != null;
308         while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
309             final DetailAST caseBody =
310                 caseGroup.findFirstToken(TokenTypes.SLIST);
311             isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
312             caseGroup = caseGroup.getNextSibling();
313         }
314         return isTerminated;
315     }
316 
317     /**
318      * Checks if a given synchronized block terminated by return, throw or,
319      * if allowed break, continue.
320      * @param synchronizedAst synchronized block to check.
321      * @param useBreak should we consider break as terminator.
322      * @param useContinue should we consider continue as terminator.
323      * @return true if synchronized block is terminated.
324      */
325     private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
326                                       boolean useContinue) {
327         return isTerminated(
328             synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
329     }
330 
331     /**
332      * Determines if the fall through case between {@code currentCase} and
333      * {@code nextCase} is relieved by a appropriate comment.
334      *
335      * @param currentCase AST of the case that falls through to the next case.
336      * @param nextCase AST of the next case.
337      * @return True if a relief comment was found
338      */
339     private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
340         boolean allThroughComment = false;
341         final int endLineNo = nextCase.getLineNo();
342         final int endColNo = nextCase.getColumnNo();
343 
344         // Remember: The lines number returned from the AST is 1-based, but
345         // the lines number in this array are 0-based. So you will often
346         // see a "lineNo-1" etc.
347         final String[] lines = getLines();
348 
349         // Handle:
350         //    case 1:
351         //    /+ FALLTHRU +/ case 2:
352         //    ....
353         // and
354         //    switch(i) {
355         //    default:
356         //    /+ FALLTHRU +/}
357         //
358         final String linePart = lines[endLineNo - 1].substring(0, endColNo);
359         if (matchesComment(reliefPattern, linePart, endLineNo)) {
360             allThroughComment = true;
361         }
362         else {
363             // Handle:
364             //    case 1:
365             //    .....
366             //    // FALLTHRU
367             //    case 2:
368             //    ....
369             // and
370             //    switch(i) {
371             //    default:
372             //    // FALLTHRU
373             //    }
374             final int startLineNo = currentCase.getLineNo();
375             for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
376                 if (!CommonUtils.isBlank(lines[i])) {
377                     allThroughComment = matchesComment(reliefPattern, lines[i], i + 1);
378                     break;
379                 }
380             }
381         }
382         return allThroughComment;
383     }
384 
385     /**
386      * Does a regular expression match on the given line and checks that a
387      * possible match is within a comment.
388      * @param pattern The regular expression pattern to use.
389      * @param line The line of test to do the match on.
390      * @param lineNo The line number in the file.
391      * @return True if a match was found inside a comment.
392      */
393     private boolean matchesComment(Pattern pattern, String line, int lineNo) {
394         final Matcher matcher = pattern.matcher(line);
395         boolean matches = false;
396 
397         if (matcher.find()) {
398             // -1 because it returns the char position beyond the match
399             matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(),
400                     lineNo, matcher.end() - 1);
401         }
402         return matches;
403     }
404 }