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