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