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