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.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        final boolean terminated;
189
190        switch (ast.getType()) {
191            case TokenTypes.LITERAL_RETURN:
192            case TokenTypes.LITERAL_YIELD:
193            case TokenTypes.LITERAL_THROW:
194                terminated = true;
195                break;
196            case TokenTypes.LITERAL_BREAK:
197                terminated =
198                        useBreak || hasLabel(ast, labelsForCurrentSwitchScope);
199                break;
200            case TokenTypes.LITERAL_CONTINUE:
201                terminated =
202                        useContinue || hasLabel(ast, labelsForCurrentSwitchScope);
203                break;
204            case TokenTypes.SLIST:
205                terminated =
206                        checkSlist(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
207                break;
208            case TokenTypes.LITERAL_IF:
209                terminated =
210                        checkIf(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
211                break;
212            case TokenTypes.LITERAL_FOR:
213            case TokenTypes.LITERAL_WHILE:
214            case TokenTypes.LITERAL_DO:
215                terminated = checkLoop(ast, labelsForCurrentSwitchScope);
216                break;
217            case TokenTypes.LITERAL_TRY:
218                terminated =
219                        checkTry(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
220                break;
221            case TokenTypes.LITERAL_SWITCH:
222                terminated =
223                        checkSwitch(ast, useContinue, labelsForCurrentSwitchScope);
224                break;
225            case TokenTypes.LITERAL_SYNCHRONIZED:
226                terminated =
227                        checkSynchronized(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
228                break;
229            case TokenTypes.LABELED_STAT:
230                labelsForCurrentSwitchScope.add(ast.getFirstChild().getText());
231                terminated =
232                        isTerminated(ast.getLastChild(), useBreak, useContinue,
233                                labelsForCurrentSwitchScope);
234                break;
235            default:
236                terminated = false;
237        }
238        return terminated;
239    }
240
241    /**
242     * Checks if given break or continue ast has outer label.
243     *
244     * @param statement break or continue node
245     * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
246     * @return true if local label used
247     */
248    private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) {
249        return Optional.ofNullable(statement)
250                .map(DetailAST::getFirstChild)
251                .filter(child -> child.getType() == TokenTypes.IDENT)
252                .map(DetailAST::getText)
253                .filter(label -> !labelsForCurrentSwitchScope.contains(label))
254                .isPresent();
255    }
256
257    /**
258     * Checks if a given SLIST terminated by return, throw or,
259     * if allowed break, continue.
260     *
261     * @param slistAst SLIST to check
262     * @param useBreak should we consider break as terminator
263     * @param useContinue should we consider continue as terminator
264     * @param labels label names
265     * @return true if SLIST is terminated.
266     */
267    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
268                               boolean useContinue, Set<String> labels) {
269        DetailAST lastStmt = slistAst.getLastChild();
270
271        if (lastStmt.getType() == TokenTypes.RCURLY) {
272            lastStmt = lastStmt.getPreviousSibling();
273        }
274
275        while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT,
276                TokenTypes.BLOCK_COMMENT_BEGIN)) {
277            lastStmt = lastStmt.getPreviousSibling();
278        }
279
280        return lastStmt != null
281            && isTerminated(lastStmt, useBreak, useContinue, labels);
282    }
283
284    /**
285     * Checks if a given IF terminated by return, throw or,
286     * if allowed break, continue.
287     *
288     * @param ast IF to check
289     * @param useBreak should we consider break as terminator
290     * @param useContinue should we consider continue as terminator
291     * @param labels label names
292     * @return true if IF is terminated.
293     */
294    private boolean checkIf(final DetailAST ast, boolean useBreak,
295                            boolean useContinue, Set<String> labels) {
296        final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN));
297
298        final DetailAST elseStmt = getNextNonCommentAst(thenStmt);
299
300        return elseStmt != null
301                && isTerminated(thenStmt, useBreak, useContinue, labels)
302                && isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels);
303    }
304
305    /**
306     * This method will skip the comment content while finding the next ast of current ast.
307     *
308     * @param ast current ast
309     * @return next ast after skipping comment
310     */
311    private static DetailAST getNextNonCommentAst(DetailAST ast) {
312        DetailAST nextSibling = ast.getNextSibling();
313        while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT,
314                TokenTypes.BLOCK_COMMENT_BEGIN)) {
315            nextSibling = nextSibling.getNextSibling();
316        }
317        return nextSibling;
318    }
319
320    /**
321     * Checks if a given loop terminated by return, throw or,
322     * if allowed break, continue.
323     *
324     * @param ast loop to check
325     * @param labels label names
326     * @return true if loop is terminated.
327     */
328    private boolean checkLoop(final DetailAST ast, Set<String> labels) {
329        final DetailAST loopBody;
330        if (ast.getType() == TokenTypes.LITERAL_DO) {
331            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
332            loopBody = lparen.getPreviousSibling();
333        }
334        else {
335            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
336            loopBody = rparen.getNextSibling();
337        }
338        return isTerminated(loopBody, false, false, labels);
339    }
340
341    /**
342     * Checks if a given try/catch/finally block terminated by return, throw or,
343     * if allowed break, continue.
344     *
345     * @param ast loop to check
346     * @param useBreak should we consider break as terminator
347     * @param useContinue should we consider continue as terminator
348     * @param labels label names
349     * @return true if try/catch/finally block is terminated
350     */
351    private boolean checkTry(final DetailAST ast, boolean useBreak,
352                             boolean useContinue, Set<String> labels) {
353        final DetailAST finalStmt = ast.getLastChild();
354        boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
355                && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
356                useBreak, useContinue, labels);
357
358        if (!isTerminated) {
359            DetailAST firstChild = ast.getFirstChild();
360
361            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
362                firstChild = firstChild.getNextSibling();
363            }
364
365            isTerminated = isTerminated(firstChild,
366                    useBreak, useContinue, labels);
367
368            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
369            while (catchStmt != null
370                    && isTerminated
371                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
372                final DetailAST catchBody =
373                        catchStmt.findFirstToken(TokenTypes.SLIST);
374                isTerminated = isTerminated(catchBody, useBreak, useContinue, labels);
375                catchStmt = catchStmt.getNextSibling();
376            }
377        }
378        return isTerminated;
379    }
380
381    /**
382     * Checks if a given switch terminated by return, throw or,
383     * if allowed break, continue.
384     *
385     * @param literalSwitchAst loop to check
386     * @param useContinue should we consider continue as terminator
387     * @param labels label names
388     * @return true if switch is terminated
389     */
390    private boolean checkSwitch(DetailAST literalSwitchAst,
391                                boolean useContinue, Set<String> labels) {
392        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
393        boolean isTerminated = caseGroup != null;
394        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
395            final DetailAST caseBody =
396                caseGroup.findFirstToken(TokenTypes.SLIST);
397            isTerminated = caseBody != null
398                    && isTerminated(caseBody, false, useContinue, labels);
399            caseGroup = caseGroup.getNextSibling();
400        }
401        return isTerminated;
402    }
403
404    /**
405     * Checks if a given synchronized block terminated by return, throw or,
406     * if allowed break, continue.
407     *
408     * @param synchronizedAst synchronized block to check.
409     * @param useBreak should we consider break as terminator
410     * @param useContinue should we consider continue as terminator
411     * @param labels label names
412     * @return true if synchronized block is terminated
413     */
414    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
415                                      boolean useContinue, Set<String> labels) {
416        return isTerminated(
417            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels);
418    }
419
420    /**
421     * Determines if the fall through case between {@code currentCase} and
422     * {@code nextCase} is relieved by an appropriate comment.
423     *
424     * <p>Handles</p>
425     * <pre>
426     * case 1:
427     * /&#42; FALLTHRU &#42;/ case 2:
428     *
429     * switch(i) {
430     * default:
431     * /&#42; FALLTHRU &#42;/}
432     *
433     * case 1:
434     * // FALLTHRU
435     * case 2:
436     *
437     * switch(i) {
438     * default:
439     * // FALLTHRU
440     * </pre>
441     *
442     * @param currentCase AST of the case that falls through to the next case.
443     * @return True if a relief comment was found
444     */
445    private boolean hasFallThroughComment(DetailAST currentCase) {
446        final DetailAST nextSibling = currentCase.getNextSibling();
447        final DetailAST ast;
448        if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
449            ast = nextSibling.getFirstChild();
450        }
451        else {
452            ast = currentCase;
453        }
454        return hasReliefComment(ast);
455    }
456
457    /**
458     * Check if there is any fall through comment.
459     *
460     * @param ast ast to check
461     * @return true if relief comment found
462     */
463    private boolean hasReliefComment(DetailAST ast) {
464        final DetailAST nonCommentAst = getNextNonCommentAst(ast);
465        boolean result = false;
466        if (nonCommentAst != null) {
467            final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo();
468            result = Stream.iterate(nonCommentAst.getPreviousSibling(),
469                            Objects::nonNull,
470                            DetailAST::getPreviousSibling)
471                    .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber)
472                    .map(DetailAST::getFirstChild)
473                    .filter(Objects::nonNull)
474                    .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find());
475        }
476        return result;
477    }
478
479}