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