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 *
058 * @since 3.4
059 */
060@StatelessCheck
061public class FallThroughCheck extends AbstractCheck {
062
063    /**
064     * A key is pointing to the warning message text in "messages.properties"
065     * file.
066     */
067    public static final String MSG_FALL_THROUGH = "fall.through";
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
074
075    /** Control whether the last case group must be checked. */
076    private boolean checkLastCaseGroup;
077
078    /**
079     * Define the RegExp to match the relief comment that suppresses
080     * the warning about a fall through.
081     */
082    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
083
084    @Override
085    public int[] getDefaultTokens() {
086        return getRequiredTokens();
087    }
088
089    @Override
090    public int[] getRequiredTokens() {
091        return new int[] {TokenTypes.CASE_GROUP};
092    }
093
094    @Override
095    public int[] getAcceptableTokens() {
096        return getRequiredTokens();
097    }
098
099    @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}