001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.HashSet;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.Set;
026import java.util.regex.Pattern;
027import java.util.stream.Stream;
028
029import com.puppycrawl.tools.checkstyle.StatelessCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
034
035/**
036 * <div>
037 * Checks for fall-through in {@code switch} statements.
038 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a
039 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement.
040 * </div>
041 *
042 * <p>
043 * The check honors special comments to suppress the warning.
044 * By default, the texts
045 * "fallthru", "fall thru", "fall-thru",
046 * "fallthrough", "fall through", "fall-through"
047 * "fallsthrough", "falls through", "falls-through" (case-sensitive).
048 * The comment containing these words must be all on one line,
049 * and must be on the last non-empty line before the {@code case} triggering
050 * the warning or on the same line before the {@code case}(ugly, but possible).
051 * Any other comment may follow on the same line.
052 * </p>
053 *
054 * <p>
055 * Note: The check assumes that there is no unreachable code in the {@code case}.
056 * </p>
057 * <ul>
058 * <li>
059 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked.
060 * Type is {@code boolean}.
061 * Default value is {@code false}.
062 * </li>
063 * <li>
064 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses
065 * the warning about a fall through.
066 * Type is {@code java.util.regex.Pattern}.
067 * Default value is {@code "falls?[ -]?thr(u|ough)"}.
068 * </li>
069 * </ul>
070 *
071 * <p>
072 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
073 * </p>
074 *
075 * <p>
076 * Violation Message Keys:
077 * </p>
078 * <ul>
079 * <li>
080 * {@code fall.through}
081 * </li>
082 * <li>
083 * {@code fall.through.last}
084 * </li>
085 * </ul>
086 *
087 * @since 3.4
088 */
089@StatelessCheck
090public class FallThroughCheck extends AbstractCheck {
091
092    /**
093     * A key is pointing to the warning message text in "messages.properties"
094     * file.
095     */
096    public static final String MSG_FALL_THROUGH = "fall.through";
097
098    /**
099     * 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
189        return switch (ast.getType()) {
190            case TokenTypes.LITERAL_RETURN, TokenTypes.LITERAL_YIELD,
191                    TokenTypes.LITERAL_THROW -> true;
192            case TokenTypes.LITERAL_BREAK -> useBreak
193                    || hasLabel(ast, labelsForCurrentSwitchScope);
194            case TokenTypes.LITERAL_CONTINUE -> useContinue
195                    || hasLabel(ast, labelsForCurrentSwitchScope);
196            case TokenTypes.SLIST -> checkSlist(ast, useBreak, useContinue,
197                    labelsForCurrentSwitchScope);
198            case TokenTypes.LITERAL_IF -> checkIf(ast, useBreak, useContinue,
199                    labelsForCurrentSwitchScope);
200            case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO ->
201                checkLoop(ast, labelsForCurrentSwitchScope);
202            case TokenTypes.LITERAL_TRY -> checkTry(ast, useBreak, useContinue,
203                    labelsForCurrentSwitchScope);
204            case TokenTypes.LITERAL_SWITCH -> checkSwitch(ast, useContinue,
205                    labelsForCurrentSwitchScope);
206            case TokenTypes.LITERAL_SYNCHRONIZED ->
207                checkSynchronized(ast, useBreak, useContinue,
208                    labelsForCurrentSwitchScope);
209            case TokenTypes.LABELED_STAT -> {
210                labelsForCurrentSwitchScope.add(ast.getFirstChild().getText());
211                yield isTerminated(ast.getLastChild(), useBreak, useContinue,
212                        labelsForCurrentSwitchScope);
213            }
214            default -> false;
215        };
216    }
217
218    /**
219     * Checks if given break or continue ast has outer label.
220     *
221     * @param statement break or continue node
222     * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
223     * @return true if local label used
224     */
225    private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) {
226        return Optional.ofNullable(statement)
227                .map(DetailAST::getFirstChild)
228                .filter(child -> child.getType() == TokenTypes.IDENT)
229                .map(DetailAST::getText)
230                .filter(label -> !labelsForCurrentSwitchScope.contains(label))
231                .isPresent();
232    }
233
234    /**
235     * Checks if a given SLIST terminated by return, throw or,
236     * if allowed break, continue.
237     *
238     * @param slistAst SLIST to check
239     * @param useBreak should we consider break as terminator
240     * @param useContinue should we consider continue as terminator
241     * @param labels label names
242     * @return true if SLIST is terminated.
243     */
244    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
245                               boolean useContinue, Set<String> labels) {
246        DetailAST lastStmt = slistAst.getLastChild();
247
248        if (lastStmt.getType() == TokenTypes.RCURLY) {
249            lastStmt = lastStmt.getPreviousSibling();
250        }
251
252        while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT,
253                TokenTypes.BLOCK_COMMENT_BEGIN)) {
254            lastStmt = lastStmt.getPreviousSibling();
255        }
256
257        return lastStmt != null
258            && isTerminated(lastStmt, useBreak, useContinue, labels);
259    }
260
261    /**
262     * Checks if a given IF terminated by return, throw or,
263     * if allowed break, continue.
264     *
265     * @param ast IF to check
266     * @param useBreak should we consider break as terminator
267     * @param useContinue should we consider continue as terminator
268     * @param labels label names
269     * @return true if IF is terminated.
270     */
271    private boolean checkIf(final DetailAST ast, boolean useBreak,
272                            boolean useContinue, Set<String> labels) {
273        final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN));
274
275        final DetailAST elseStmt = getNextNonCommentAst(thenStmt);
276
277        return elseStmt != null
278                && isTerminated(thenStmt, useBreak, useContinue, labels)
279                && isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels);
280    }
281
282    /**
283     * This method will skip the comment content while finding the next ast of current ast.
284     *
285     * @param ast current ast
286     * @return next ast after skipping comment
287     */
288    private static DetailAST getNextNonCommentAst(DetailAST ast) {
289        DetailAST nextSibling = ast.getNextSibling();
290        while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT,
291                TokenTypes.BLOCK_COMMENT_BEGIN)) {
292            nextSibling = nextSibling.getNextSibling();
293        }
294        return nextSibling;
295    }
296
297    /**
298     * Checks if a given loop terminated by return, throw or,
299     * if allowed break, continue.
300     *
301     * @param ast loop to check
302     * @param labels label names
303     * @return true if loop is terminated.
304     */
305    private boolean checkLoop(final DetailAST ast, Set<String> labels) {
306        final DetailAST loopBody;
307        if (ast.getType() == TokenTypes.LITERAL_DO) {
308            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
309            loopBody = lparen.getPreviousSibling();
310        }
311        else {
312            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
313            loopBody = rparen.getNextSibling();
314        }
315        return isTerminated(loopBody, false, false, labels);
316    }
317
318    /**
319     * Checks if a given try/catch/finally block terminated by return, throw or,
320     * if allowed break, continue.
321     *
322     * @param ast loop to check
323     * @param useBreak should we consider break as terminator
324     * @param useContinue should we consider continue as terminator
325     * @param labels label names
326     * @return true if try/catch/finally block is terminated
327     */
328    private boolean checkTry(final DetailAST ast, boolean useBreak,
329                             boolean useContinue, Set<String> labels) {
330        final DetailAST finalStmt = ast.getLastChild();
331        boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
332                && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
333                useBreak, useContinue, labels);
334
335        if (!isTerminated) {
336            DetailAST firstChild = ast.getFirstChild();
337
338            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
339                firstChild = firstChild.getNextSibling();
340            }
341
342            isTerminated = isTerminated(firstChild,
343                    useBreak, useContinue, labels);
344
345            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
346            while (catchStmt != null
347                    && isTerminated
348                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
349                final DetailAST catchBody =
350                        catchStmt.findFirstToken(TokenTypes.SLIST);
351                isTerminated = isTerminated(catchBody, useBreak, useContinue, labels);
352                catchStmt = catchStmt.getNextSibling();
353            }
354        }
355        return isTerminated;
356    }
357
358    /**
359     * Checks if a given switch terminated by return, throw or,
360     * if allowed break, continue.
361     *
362     * @param literalSwitchAst loop to check
363     * @param useContinue should we consider continue as terminator
364     * @param labels label names
365     * @return true if switch is terminated
366     */
367    private boolean checkSwitch(DetailAST literalSwitchAst,
368                                boolean useContinue, Set<String> labels) {
369        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
370        boolean isTerminated = caseGroup != null;
371        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
372            final DetailAST caseBody =
373                caseGroup.findFirstToken(TokenTypes.SLIST);
374            isTerminated = caseBody != null
375                    && isTerminated(caseBody, false, useContinue, labels);
376            caseGroup = caseGroup.getNextSibling();
377        }
378        return isTerminated;
379    }
380
381    /**
382     * Checks if a given synchronized block terminated by return, throw or,
383     * if allowed break, continue.
384     *
385     * @param synchronizedAst synchronized block to check.
386     * @param useBreak should we consider break as terminator
387     * @param useContinue should we consider continue as terminator
388     * @param labels label names
389     * @return true if synchronized block is terminated
390     */
391    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
392                                      boolean useContinue, Set<String> labels) {
393        return isTerminated(
394            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels);
395    }
396
397    /**
398     * Determines if the fall through case between {@code currentCase} and
399     * {@code nextCase} is relieved by an appropriate comment.
400     *
401     * <p>Handles</p>
402     * <pre>
403     * case 1:
404     * /&#42; FALLTHRU &#42;/ case 2:
405     *
406     * switch(i) {
407     * default:
408     * /&#42; FALLTHRU &#42;/}
409     *
410     * case 1:
411     * // FALLTHRU
412     * case 2:
413     *
414     * switch(i) {
415     * default:
416     * // FALLTHRU
417     * </pre>
418     *
419     * @param currentCase AST of the case that falls through to the next case.
420     * @return True if a relief comment was found
421     */
422    private boolean hasFallThroughComment(DetailAST currentCase) {
423        final DetailAST nextSibling = currentCase.getNextSibling();
424        final DetailAST ast;
425        if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
426            ast = nextSibling.getFirstChild();
427        }
428        else {
429            ast = currentCase;
430        }
431        return hasReliefComment(ast);
432    }
433
434    /**
435     * Check if there is any fall through comment.
436     *
437     * @param ast ast to check
438     * @return true if relief comment found
439     */
440    private boolean hasReliefComment(DetailAST ast) {
441        final DetailAST nonCommentAst = getNextNonCommentAst(ast);
442        boolean result = false;
443        if (nonCommentAst != null) {
444            final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo();
445            result = Stream.iterate(nonCommentAst.getPreviousSibling(),
446                            Objects::nonNull,
447                            DetailAST::getPreviousSibling)
448                    .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber)
449                    .map(DetailAST::getFirstChild)
450                    .filter(Objects::nonNull)
451                    .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find());
452        }
453        return result;
454    }
455
456}