001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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.Optional;
023import java.util.regex.Pattern;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
030
031/**
032 * <p>
033 * Checks for fall-through in {@code switch} statements.
034 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a
035 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement.
036 * </p>
037 * <p>
038 * The check honors special comments to suppress the warning.
039 * By default, the texts
040 * "fallthru", "fall thru", "fall-thru",
041 * "fallthrough", "fall through", "fall-through"
042 * "fallsthrough", "falls through", "falls-through" (case-sensitive).
043 * The comment containing these words must be all on one line,
044 * and must be on the last non-empty line before the {@code case} triggering
045 * the warning or on the same line before the {@code case}(ugly, but possible).
046 * </p>
047 * <p>
048 * Note: The check assumes that there is no unreachable code in the {@code case}.
049 * </p>
050 * <ul>
051 * <li>
052 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked.
053 * Type is {@code boolean}.
054 * Default value is {@code false}.
055 * </li>
056 * <li>
057 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses
058 * the warning about a fall through.
059 * Type is {@code java.util.regex.Pattern}.
060 * Default value is {@code "falls?[ -]?thr(u|ough)"}.
061 * </li>
062 * </ul>
063 * <p>
064 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
065 * </p>
066 * <p>
067 * Violation Message Keys:
068 * </p>
069 * <ul>
070 * <li>
071 * {@code fall.through}
072 * </li>
073 * <li>
074 * {@code fall.through.last}
075 * </li>
076 * </ul>
077 *
078 * @since 3.4
079 */
080@StatelessCheck
081public class FallThroughCheck extends AbstractCheck {
082
083    /**
084     * A key is pointing to the warning message text in "messages.properties"
085     * file.
086     */
087    public static final String MSG_FALL_THROUGH = "fall.through";
088
089    /**
090     * A key is pointing to the warning message text in "messages.properties"
091     * file.
092     */
093    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
094
095    /** Control whether the last case group must be checked. */
096    private boolean checkLastCaseGroup;
097
098    /**
099     * Define the RegExp to match the relief comment that suppresses
100     * the warning about a fall through.
101     */
102    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
103
104    @Override
105    public int[] getDefaultTokens() {
106        return getRequiredTokens();
107    }
108
109    @Override
110    public int[] getRequiredTokens() {
111        return new int[] {TokenTypes.CASE_GROUP};
112    }
113
114    @Override
115    public int[] getAcceptableTokens() {
116        return getRequiredTokens();
117    }
118
119    @Override
120    public boolean isCommentNodesRequired() {
121        return true;
122    }
123
124    /**
125     * Setter to define the RegExp to match the relief comment that suppresses
126     * the warning about a fall through.
127     *
128     * @param pattern
129     *            The regular expression pattern.
130     * @since 4.0
131     */
132    public void setReliefPattern(Pattern pattern) {
133        reliefPattern = pattern;
134    }
135
136    /**
137     * Setter to control whether the last case group must be checked.
138     *
139     * @param value new value of the property.
140     * @since 4.0
141     */
142    public void setCheckLastCaseGroup(boolean value) {
143        checkLastCaseGroup = value;
144    }
145
146    @Override
147    public void visitToken(DetailAST ast) {
148        final DetailAST nextGroup = ast.getNextSibling();
149        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
150        if (!isLastGroup || checkLastCaseGroup) {
151            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
152
153            if (slist != null && !isTerminated(slist, true, true)
154                && !hasFallThroughComment(ast)) {
155                if (isLastGroup) {
156                    log(ast, MSG_FALL_THROUGH_LAST);
157                }
158                else {
159                    log(nextGroup, MSG_FALL_THROUGH);
160                }
161            }
162        }
163    }
164
165    /**
166     * Checks if a given subtree terminated by return, throw or,
167     * if allowed break, continue.
168     *
169     * @param ast root of given subtree
170     * @param useBreak should we consider break as terminator
171     * @param useContinue should we consider continue as terminator
172     * @return true if the subtree is terminated.
173     */
174    private boolean isTerminated(final DetailAST ast, boolean useBreak,
175                                 boolean useContinue) {
176        final boolean terminated;
177
178        switch (ast.getType()) {
179            case TokenTypes.LITERAL_RETURN:
180            case TokenTypes.LITERAL_YIELD:
181            case TokenTypes.LITERAL_THROW:
182                terminated = true;
183                break;
184            case TokenTypes.LITERAL_BREAK:
185                terminated = useBreak;
186                break;
187            case TokenTypes.LITERAL_CONTINUE:
188                terminated = useContinue;
189                break;
190            case TokenTypes.SLIST:
191                terminated = checkSlist(ast, useBreak, useContinue);
192                break;
193            case TokenTypes.LITERAL_IF:
194                terminated = checkIf(ast, useBreak, useContinue);
195                break;
196            case TokenTypes.LITERAL_FOR:
197            case TokenTypes.LITERAL_WHILE:
198            case TokenTypes.LITERAL_DO:
199                terminated = checkLoop(ast);
200                break;
201            case TokenTypes.LITERAL_TRY:
202                terminated = checkTry(ast, useBreak, useContinue);
203                break;
204            case TokenTypes.LITERAL_SWITCH:
205                terminated = checkSwitch(ast, useContinue);
206                break;
207            case TokenTypes.LITERAL_SYNCHRONIZED:
208                terminated = checkSynchronized(ast, useBreak, useContinue);
209                break;
210            default:
211                terminated = false;
212        }
213        return terminated;
214    }
215
216    /**
217     * Checks if a given SLIST terminated by return, throw or,
218     * if allowed break, continue.
219     *
220     * @param slistAst SLIST to check
221     * @param useBreak should we consider break as terminator
222     * @param useContinue should we consider continue as terminator
223     * @return true if SLIST is terminated.
224     */
225    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
226                               boolean useContinue) {
227        DetailAST lastStmt = slistAst.getLastChild();
228
229        if (lastStmt.getType() == TokenTypes.RCURLY) {
230            lastStmt = lastStmt.getPreviousSibling();
231        }
232
233        while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT,
234                TokenTypes.BLOCK_COMMENT_BEGIN)) {
235            lastStmt = lastStmt.getPreviousSibling();
236        }
237
238        return lastStmt != null
239            && isTerminated(lastStmt, useBreak, useContinue);
240    }
241
242    /**
243     * Checks if a given IF terminated by return, throw or,
244     * if allowed break, continue.
245     *
246     * @param ast IF to check
247     * @param useBreak should we consider break as terminator
248     * @param useContinue should we consider continue as terminator
249     * @return true if IF is terminated.
250     */
251    private boolean checkIf(final DetailAST ast, boolean useBreak,
252                            boolean useContinue) {
253        final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN));
254
255        final DetailAST elseStmt = getNextNonCommentAst(thenStmt);
256
257        return elseStmt != null
258                && isTerminated(thenStmt, useBreak, useContinue)
259                && isTerminated(elseStmt.getLastChild(), useBreak, useContinue);
260    }
261
262    /**
263     * This method will skip the comment content while finding the next ast of current ast.
264     *
265     * @param ast current ast
266     * @return next ast after skipping comment
267     */
268    private static DetailAST getNextNonCommentAst(DetailAST ast) {
269        DetailAST nextSibling = ast.getNextSibling();
270        while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT,
271                TokenTypes.BLOCK_COMMENT_BEGIN)) {
272            nextSibling = nextSibling.getNextSibling();
273        }
274        return nextSibling;
275    }
276
277    /**
278     * Checks if a given loop terminated by return, throw or,
279     * if allowed break, continue.
280     *
281     * @param ast loop to check
282     * @return true if loop is terminated.
283     */
284    private boolean checkLoop(final DetailAST ast) {
285        final DetailAST loopBody;
286        if (ast.getType() == TokenTypes.LITERAL_DO) {
287            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
288            loopBody = lparen.getPreviousSibling();
289        }
290        else {
291            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
292            loopBody = rparen.getNextSibling();
293        }
294        return isTerminated(loopBody, false, false);
295    }
296
297    /**
298     * Checks if a given try/catch/finally block terminated by return, throw or,
299     * if allowed break, continue.
300     *
301     * @param ast loop to check
302     * @param useBreak should we consider break as terminator
303     * @param useContinue should we consider continue as terminator
304     * @return true if try/catch/finally block is terminated
305     */
306    private boolean checkTry(final DetailAST ast, boolean useBreak,
307                             boolean useContinue) {
308        final DetailAST finalStmt = ast.getLastChild();
309        boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
310                && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
311
312        if (!isTerminated) {
313            DetailAST firstChild = ast.getFirstChild();
314
315            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
316                firstChild = firstChild.getNextSibling();
317            }
318
319            isTerminated = isTerminated(firstChild,
320                    useBreak, useContinue);
321
322            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
323            while (catchStmt != null
324                    && isTerminated
325                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
326                final DetailAST catchBody =
327                        catchStmt.findFirstToken(TokenTypes.SLIST);
328                isTerminated = isTerminated(catchBody, useBreak, useContinue);
329                catchStmt = catchStmt.getNextSibling();
330            }
331        }
332        return isTerminated;
333    }
334
335    /**
336     * Checks if a given switch terminated by return, throw or,
337     * if allowed break, continue.
338     *
339     * @param literalSwitchAst loop to check
340     * @param useContinue should we consider continue as terminator
341     * @return true if switch is terminated
342     */
343    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
344        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
345        boolean isTerminated = caseGroup != null;
346        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
347            final DetailAST caseBody =
348                caseGroup.findFirstToken(TokenTypes.SLIST);
349            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
350            caseGroup = caseGroup.getNextSibling();
351        }
352        return isTerminated;
353    }
354
355    /**
356     * Checks if a given synchronized block terminated by return, throw or,
357     * if allowed break, continue.
358     *
359     * @param synchronizedAst synchronized block to check.
360     * @param useBreak should we consider break as terminator
361     * @param useContinue should we consider continue as terminator
362     * @return true if synchronized block is terminated
363     */
364    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
365                                      boolean useContinue) {
366        return isTerminated(
367            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
368    }
369
370    /**
371     * Determines if the fall through case between {@code currentCase} and
372     * {@code nextCase} is relieved by an appropriate comment.
373     *
374     * <p>Handles</p>
375     * <pre>
376     * case 1:
377     * /&#42; FALLTHRU &#42;/ case 2:
378     *
379     * switch(i) {
380     * default:
381     * /&#42; FALLTHRU &#42;/}
382     *
383     * case 1:
384     * // FALLTHRU
385     * case 2:
386     *
387     * switch(i) {
388     * default:
389     * // FALLTHRU
390     * </pre>
391     *
392     * @param currentCase AST of the case that falls through to the next case.
393     * @return True if a relief comment was found
394     */
395    private boolean hasFallThroughComment(DetailAST currentCase) {
396        final DetailAST nextSibling = currentCase.getNextSibling();
397        final DetailAST ast;
398        if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
399            ast = nextSibling.getFirstChild();
400        }
401        else {
402            ast = currentCase;
403        }
404        return hasReliefComment(ast);
405    }
406
407    /**
408     * Check if there is any fall through comment.
409     *
410     * @param ast ast to check
411     * @return true if relief comment found
412     */
413    private boolean hasReliefComment(DetailAST ast) {
414        return Optional.ofNullable(getNextNonCommentAst(ast))
415                .map(DetailAST::getPreviousSibling)
416                .map(previous -> previous.getFirstChild().getText())
417                .map(text -> reliefPattern.matcher(text).find())
418                .orElse(Boolean.FALSE);
419    }
420
421}