001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.Objects;
023import java.util.regex.Pattern;
024import java.util.stream.Stream;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
031
032/**
033 * <div>
034 * Checks for fall-through in {@code switch} statements.
035 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a
036 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement.
037 * </div>
038 *
039 * <p>
040 * The check honors special comments to suppress the warning.
041 * By default, the texts
042 * "fallthru", "fall thru", "fall-thru",
043 * "fallthrough", "fall through", "fall-through"
044 * "fallsthrough", "falls through", "falls-through" (case-sensitive).
045 * The comment containing these words must be all on one line,
046 * and must be on the last non-empty line before the {@code case} triggering
047 * the warning or on the same line before the {@code case}(ugly, but possible).
048 * Any other comment may follow on the same line.
049 * </p>
050 *
051 * <p>
052 * Note: The check assumes that there is no unreachable code in the {@code case}.
053 * </p>
054 *
055 * @since 3.4
056 */
057@StatelessCheck
058public class FallThroughCheck extends AbstractCheck {
059
060    /**
061     * A key is pointing to the warning message text in "messages.properties"
062     * file.
063     */
064    public static final String MSG_FALL_THROUGH = "fall.through";
065
066    /**
067     * A key is pointing to the warning message text in "messages.properties"
068     * file.
069     */
070    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
071
072    /** Control whether the last case group must be checked. */
073    private boolean checkLastCaseGroup;
074
075    /**
076     * Define the RegExp to match the relief comment that suppresses
077     * the warning about a fall through.
078     */
079    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
080
081    @Override
082    public int[] getDefaultTokens() {
083        return getRequiredTokens();
084    }
085
086    @Override
087    public int[] getRequiredTokens() {
088        return new int[] {TokenTypes.CASE_GROUP};
089    }
090
091    @Override
092    public int[] getAcceptableTokens() {
093        return getRequiredTokens();
094    }
095
096    @Override
097    public boolean isCommentNodesRequired() {
098        return true;
099    }
100
101    /**
102     * Setter to define the RegExp to match the relief comment that suppresses
103     * the warning about a fall through.
104     *
105     * @param pattern
106     *            The regular expression pattern.
107     * @since 4.0
108     */
109    public void setReliefPattern(Pattern pattern) {
110        reliefPattern = pattern;
111    }
112
113    /**
114     * Setter to control whether the last case group must be checked.
115     *
116     * @param value new value of the property.
117     * @since 4.0
118     */
119    public void setCheckLastCaseGroup(boolean value) {
120        checkLastCaseGroup = value;
121    }
122
123    @Override
124    public void visitToken(DetailAST ast) {
125        final DetailAST nextGroup = ast.getNextSibling();
126        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
127        if (!isLastGroup || checkLastCaseGroup) {
128            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
129
130            if (slist != null && !CheckUtil.isTerminated(slist) && !hasFallThroughComment(ast)) {
131                if (isLastGroup) {
132                    log(ast, MSG_FALL_THROUGH_LAST);
133                }
134                else {
135                    log(nextGroup, MSG_FALL_THROUGH);
136                }
137            }
138        }
139    }
140
141    /**
142     * Determines if the fall through case between {@code currentCase} and
143     * {@code nextCase} is relieved by an appropriate comment.
144     *
145     * <p>Handles</p>
146     * <pre>
147     * case 1:
148     * /&#42; FALLTHRU &#42;/ case 2:
149     *
150     * switch(i) {
151     * default:
152     * /&#42; FALLTHRU &#42;/}
153     *
154     * case 1:
155     * // FALLTHRU
156     * case 2:
157     *
158     * switch(i) {
159     * default:
160     * // FALLTHRU
161     * </pre>
162     *
163     * @param currentCase AST of the case that falls through to the next case.
164     * @return True if a relief comment was found
165     */
166    private boolean hasFallThroughComment(DetailAST currentCase) {
167        final DetailAST nextSibling = currentCase.getNextSibling();
168        final DetailAST ast;
169        if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
170            ast = nextSibling.getFirstChild();
171        }
172        else {
173            ast = currentCase;
174        }
175        return hasReliefComment(ast);
176    }
177
178    /**
179     * Check if there is any fall through comment.
180     *
181     * @param ast ast to check
182     * @return true if relief comment found
183     */
184    private boolean hasReliefComment(DetailAST ast) {
185        final DetailAST nonCommentAst = CheckUtil.getNextNonCommentAst(ast);
186        boolean result = false;
187        if (nonCommentAst != null) {
188            final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo();
189            result = Stream.iterate(nonCommentAst.getPreviousSibling(),
190                            Objects::nonNull,
191                            DetailAST::getPreviousSibling)
192                    .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber)
193                    .map(DetailAST::getFirstChild)
194                    .filter(Objects::nonNull)
195                    .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find());
196        }
197        return result;
198    }
199
200}