View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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   *
58   * @since 3.4
59   */
60  @StatelessCheck
61  public class FallThroughCheck extends AbstractCheck {
62  
63      /**
64       * A key is pointing to the warning message text in "messages.properties"
65       * file.
66       */
67      public static final String MSG_FALL_THROUGH = "fall.through";
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
74  
75      /** Control whether the last case group must be checked. */
76      private boolean checkLastCaseGroup;
77  
78      /**
79       * Define the RegExp to match the relief comment that suppresses
80       * the warning about a fall through.
81       */
82      private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
83  
84      @Override
85      public int[] getDefaultTokens() {
86          return getRequiredTokens();
87      }
88  
89      @Override
90      public int[] getRequiredTokens() {
91          return new int[] {TokenTypes.CASE_GROUP};
92      }
93  
94      @Override
95      public int[] getAcceptableTokens() {
96          return getRequiredTokens();
97      }
98  
99      @Override
100     public boolean isCommentNodesRequired() {
101         return true;
102     }
103 
104     /**
105      * Setter to define the RegExp to match the relief comment that suppresses
106      * the warning about a fall through.
107      *
108      * @param pattern
109      *            The regular expression pattern.
110      * @since 4.0
111      */
112     public void setReliefPattern(Pattern pattern) {
113         reliefPattern = pattern;
114     }
115 
116     /**
117      * Setter to control whether the last case group must be checked.
118      *
119      * @param value new value of the property.
120      * @since 4.0
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, new HashSet<>())
134                     && !hasFallThroughComment(ast)) {
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      * When analyzing fall-through cases in switch statements, a Set of String labels
149      * is used to keep track of the labels encountered in the enclosing switch statements.
150      *
151      * @param ast root of given subtree
152      * @param useBreak should we consider break as terminator
153      * @param useContinue should we consider continue as terminator
154      * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
155      * @return true if the subtree is terminated.
156      */
157     private boolean isTerminated(final DetailAST ast, boolean useBreak,
158                                  boolean useContinue, Set<String> labelsForCurrentSwitchScope) {
159 
160         return switch (ast.getType()) {
161             case TokenTypes.LITERAL_RETURN, TokenTypes.LITERAL_YIELD,
162                     TokenTypes.LITERAL_THROW -> true;
163             case TokenTypes.LITERAL_BREAK -> useBreak
164                     || hasLabel(ast, labelsForCurrentSwitchScope);
165             case TokenTypes.LITERAL_CONTINUE -> useContinue
166                     || hasLabel(ast, labelsForCurrentSwitchScope);
167             case TokenTypes.SLIST -> checkSlist(ast, useBreak, useContinue,
168                     labelsForCurrentSwitchScope);
169             case TokenTypes.LITERAL_IF -> checkIf(ast, useBreak, useContinue,
170                     labelsForCurrentSwitchScope);
171             case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO ->
172                 checkLoop(ast, labelsForCurrentSwitchScope);
173             case TokenTypes.LITERAL_TRY -> checkTry(ast, useBreak, useContinue,
174                     labelsForCurrentSwitchScope);
175             case TokenTypes.LITERAL_SWITCH -> checkSwitch(ast, useContinue,
176                     labelsForCurrentSwitchScope);
177             case TokenTypes.LITERAL_SYNCHRONIZED ->
178                 checkSynchronized(ast, useBreak, useContinue,
179                     labelsForCurrentSwitchScope);
180             case TokenTypes.LABELED_STAT -> {
181                 labelsForCurrentSwitchScope.add(ast.getFirstChild().getText());
182                 yield isTerminated(ast.getLastChild(), useBreak, useContinue,
183                         labelsForCurrentSwitchScope);
184             }
185             default -> false;
186         };
187     }
188 
189     /**
190      * Checks if given break or continue ast has outer label.
191      *
192      * @param statement break or continue node
193      * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
194      * @return true if local label used
195      */
196     private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) {
197         return Optional.ofNullable(statement)
198                 .map(DetailAST::getFirstChild)
199                 .filter(child -> child.getType() == TokenTypes.IDENT)
200                 .map(DetailAST::getText)
201                 .filter(label -> !labelsForCurrentSwitchScope.contains(label))
202                 .isPresent();
203     }
204 
205     /**
206      * Checks if a given SLIST terminated by return, throw or,
207      * if allowed break, continue.
208      *
209      * @param slistAst SLIST to check
210      * @param useBreak should we consider break as terminator
211      * @param useContinue should we consider continue as terminator
212      * @param labels label names
213      * @return true if SLIST is terminated.
214      */
215     private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
216                                boolean useContinue, Set<String> labels) {
217         DetailAST lastStmt = slistAst.getLastChild();
218 
219         if (lastStmt.getType() == TokenTypes.RCURLY) {
220             lastStmt = lastStmt.getPreviousSibling();
221         }
222 
223         while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT,
224                 TokenTypes.BLOCK_COMMENT_BEGIN)) {
225             lastStmt = lastStmt.getPreviousSibling();
226         }
227 
228         return lastStmt != null
229             && isTerminated(lastStmt, useBreak, useContinue, labels);
230     }
231 
232     /**
233      * Checks if a given IF terminated by return, throw or,
234      * if allowed break, continue.
235      *
236      * @param ast IF to check
237      * @param useBreak should we consider break as terminator
238      * @param useContinue should we consider continue as terminator
239      * @param labels label names
240      * @return true if IF is terminated.
241      */
242     private boolean checkIf(final DetailAST ast, boolean useBreak,
243                             boolean useContinue, Set<String> labels) {
244         final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN));
245 
246         final DetailAST elseStmt = getNextNonCommentAst(thenStmt);
247 
248         return elseStmt != null
249                 && isTerminated(thenStmt, useBreak, useContinue, labels)
250                 && isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels);
251     }
252 
253     /**
254      * This method will skip the comment content while finding the next ast of current ast.
255      *
256      * @param ast current ast
257      * @return next ast after skipping comment
258      */
259     private static DetailAST getNextNonCommentAst(DetailAST ast) {
260         DetailAST nextSibling = ast.getNextSibling();
261         while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT,
262                 TokenTypes.BLOCK_COMMENT_BEGIN)) {
263             nextSibling = nextSibling.getNextSibling();
264         }
265         return nextSibling;
266     }
267 
268     /**
269      * Checks if a given loop terminated by return, throw or,
270      * if allowed break, continue.
271      *
272      * @param ast loop to check
273      * @param labels label names
274      * @return true if loop is terminated.
275      */
276     private boolean checkLoop(final DetailAST ast, Set<String> labels) {
277         final DetailAST loopBody;
278         if (ast.getType() == TokenTypes.LITERAL_DO) {
279             final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
280             loopBody = lparen.getPreviousSibling();
281         }
282         else {
283             final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
284             loopBody = rparen.getNextSibling();
285         }
286         return isTerminated(loopBody, false, false, labels);
287     }
288 
289     /**
290      * Checks if a given try/catch/finally block terminated by return, throw or,
291      * if allowed break, continue.
292      *
293      * @param ast loop to check
294      * @param useBreak should we consider break as terminator
295      * @param useContinue should we consider continue as terminator
296      * @param labels label names
297      * @return true if try/catch/finally block is terminated
298      */
299     private boolean checkTry(final DetailAST ast, boolean useBreak,
300                              boolean useContinue, Set<String> labels) {
301         final DetailAST finalStmt = ast.getLastChild();
302         boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
303                 && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
304                 useBreak, useContinue, labels);
305 
306         if (!isTerminated) {
307             DetailAST firstChild = ast.getFirstChild();
308 
309             if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
310                 firstChild = firstChild.getNextSibling();
311             }
312 
313             isTerminated = isTerminated(firstChild,
314                     useBreak, useContinue, labels);
315 
316             DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
317             while (catchStmt != null
318                     && isTerminated
319                     && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
320                 final DetailAST catchBody =
321                         catchStmt.findFirstToken(TokenTypes.SLIST);
322                 isTerminated = isTerminated(catchBody, useBreak, useContinue, labels);
323                 catchStmt = catchStmt.getNextSibling();
324             }
325         }
326         return isTerminated;
327     }
328 
329     /**
330      * Checks if a given switch terminated by return, throw or,
331      * if allowed break, continue.
332      *
333      * @param literalSwitchAst loop to check
334      * @param useContinue should we consider continue as terminator
335      * @param labels label names
336      * @return true if switch is terminated
337      */
338     private boolean checkSwitch(DetailAST literalSwitchAst,
339                                 boolean useContinue, Set<String> labels) {
340         DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
341         boolean isTerminated = caseGroup != null;
342         while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
343             final DetailAST caseBody =
344                 caseGroup.findFirstToken(TokenTypes.SLIST);
345             isTerminated = caseBody != null
346                     && isTerminated(caseBody, false, useContinue, labels);
347             caseGroup = caseGroup.getNextSibling();
348         }
349         return isTerminated;
350     }
351 
352     /**
353      * Checks if a given synchronized block terminated by return, throw or,
354      * if allowed break, continue.
355      *
356      * @param synchronizedAst synchronized block to check.
357      * @param useBreak should we consider break as terminator
358      * @param useContinue should we consider continue as terminator
359      * @param labels label names
360      * @return true if synchronized block is terminated
361      */
362     private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
363                                       boolean useContinue, Set<String> labels) {
364         return isTerminated(
365             synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels);
366     }
367 
368     /**
369      * Determines if the fall through case between {@code currentCase} and
370      * {@code nextCase} is relieved by an appropriate comment.
371      *
372      * <p>Handles</p>
373      * <pre>
374      * case 1:
375      * /&#42; FALLTHRU &#42;/ case 2:
376      *
377      * switch(i) {
378      * default:
379      * /&#42; FALLTHRU &#42;/}
380      *
381      * case 1:
382      * // FALLTHRU
383      * case 2:
384      *
385      * switch(i) {
386      * default:
387      * // FALLTHRU
388      * </pre>
389      *
390      * @param currentCase AST of the case that falls through to the next case.
391      * @return True if a relief comment was found
392      */
393     private boolean hasFallThroughComment(DetailAST currentCase) {
394         final DetailAST nextSibling = currentCase.getNextSibling();
395         final DetailAST ast;
396         if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
397             ast = nextSibling.getFirstChild();
398         }
399         else {
400             ast = currentCase;
401         }
402         return hasReliefComment(ast);
403     }
404 
405     /**
406      * Check if there is any fall through comment.
407      *
408      * @param ast ast to check
409      * @return true if relief comment found
410      */
411     private boolean hasReliefComment(DetailAST ast) {
412         final DetailAST nonCommentAst = getNextNonCommentAst(ast);
413         boolean result = false;
414         if (nonCommentAst != null) {
415             final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo();
416             result = Stream.iterate(nonCommentAst.getPreviousSibling(),
417                             Objects::nonNull,
418                             DetailAST::getPreviousSibling)
419                     .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber)
420                     .map(DetailAST::getFirstChild)
421                     .filter(Objects::nonNull)
422                     .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find());
423         }
424         return result;
425     }
426 
427 }