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