FallThroughCheck.java
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.coding;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* <p>
* Checks for fall-through in {@code switch} statements.
* Finds locations where a {@code case} <b>contains</b> Java code but lacks a
* {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement.
* </p>
* <p>
* The check honors special comments to suppress the warning.
* By default, the texts
* "fallthru", "fall thru", "fall-thru",
* "fallthrough", "fall through", "fall-through"
* "fallsthrough", "falls through", "falls-through" (case-sensitive).
* The comment containing these words must be all on one line,
* and must be on the last non-empty line before the {@code case} triggering
* the warning or on the same line before the {@code case}(ugly, but possible).
* Any other comment may follow on the same line.
* </p>
* <p>
* Note: The check assumes that there is no unreachable code in the {@code case}.
* </p>
* <ul>
* <li>
* Property {@code checkLastCaseGroup} - Control whether the last case group must be checked.
* Type is {@code boolean}.
* Default value is {@code false}.
* </li>
* <li>
* Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses
* the warning about a fall through.
* Type is {@code java.util.regex.Pattern}.
* Default value is {@code "falls?[ -]?thr(u|ough)"}.
* </li>
* </ul>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code fall.through}
* </li>
* <li>
* {@code fall.through.last}
* </li>
* </ul>
*
* @since 3.4
*/
@StatelessCheck
public class FallThroughCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_FALL_THROUGH = "fall.through";
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
/** Control whether the last case group must be checked. */
private boolean checkLastCaseGroup;
/**
* Define the RegExp to match the relief comment that suppresses
* the warning about a fall through.
*/
private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[] {TokenTypes.CASE_GROUP};
}
@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}
@Override
public boolean isCommentNodesRequired() {
return true;
}
/**
* Setter to define the RegExp to match the relief comment that suppresses
* the warning about a fall through.
*
* @param pattern
* The regular expression pattern.
* @since 4.0
*/
public void setReliefPattern(Pattern pattern) {
reliefPattern = pattern;
}
/**
* Setter to control whether the last case group must be checked.
*
* @param value new value of the property.
* @since 4.0
*/
public void setCheckLastCaseGroup(boolean value) {
checkLastCaseGroup = value;
}
@Override
public void visitToken(DetailAST ast) {
final DetailAST nextGroup = ast.getNextSibling();
final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
if (!isLastGroup || checkLastCaseGroup) {
final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
if (slist != null && !isTerminated(slist, true, true, new HashSet<>())
&& !hasFallThroughComment(ast)) {
if (isLastGroup) {
log(ast, MSG_FALL_THROUGH_LAST);
}
else {
log(nextGroup, MSG_FALL_THROUGH);
}
}
}
}
/**
* Checks if a given subtree terminated by return, throw or,
* if allowed break, continue.
* When analyzing fall-through cases in switch statements, a Set of String labels
* is used to keep track of the labels encountered in the enclosing switch statements.
*
* @param ast root of given subtree
* @param useBreak should we consider break as terminator
* @param useContinue should we consider continue as terminator
* @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
* @return true if the subtree is terminated.
*/
private boolean isTerminated(final DetailAST ast, boolean useBreak,
boolean useContinue, Set<String> labelsForCurrentSwitchScope) {
final boolean terminated;
switch (ast.getType()) {
case TokenTypes.LITERAL_RETURN:
case TokenTypes.LITERAL_YIELD:
case TokenTypes.LITERAL_THROW:
terminated = true;
break;
case TokenTypes.LITERAL_BREAK:
terminated =
useBreak || hasLabel(ast, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_CONTINUE:
terminated =
useContinue || hasLabel(ast, labelsForCurrentSwitchScope);
break;
case TokenTypes.SLIST:
terminated =
checkSlist(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_IF:
terminated =
checkIf(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_FOR:
case TokenTypes.LITERAL_WHILE:
case TokenTypes.LITERAL_DO:
terminated = checkLoop(ast, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_TRY:
terminated =
checkTry(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_SWITCH:
terminated =
checkSwitch(ast, useContinue, labelsForCurrentSwitchScope);
break;
case TokenTypes.LITERAL_SYNCHRONIZED:
terminated =
checkSynchronized(ast, useBreak, useContinue, labelsForCurrentSwitchScope);
break;
case TokenTypes.LABELED_STAT:
labelsForCurrentSwitchScope.add(ast.getFirstChild().getText());
terminated =
isTerminated(ast.getLastChild(), useBreak, useContinue,
labelsForCurrentSwitchScope);
break;
default:
terminated = false;
}
return terminated;
}
/**
* Checks if given break or continue ast has outer label.
*
* @param statement break or continue node
* @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch
* @return true if local label used
*/
private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) {
return Optional.ofNullable(statement)
.map(DetailAST::getFirstChild)
.filter(child -> child.getType() == TokenTypes.IDENT)
.map(DetailAST::getText)
.filter(label -> !labelsForCurrentSwitchScope.contains(label))
.isPresent();
}
/**
* Checks if a given SLIST terminated by return, throw or,
* if allowed break, continue.
*
* @param slistAst SLIST to check
* @param useBreak should we consider break as terminator
* @param useContinue should we consider continue as terminator
* @param labels label names
* @return true if SLIST is terminated.
*/
private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
boolean useContinue, Set<String> labels) {
DetailAST lastStmt = slistAst.getLastChild();
if (lastStmt.getType() == TokenTypes.RCURLY) {
lastStmt = lastStmt.getPreviousSibling();
}
while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT,
TokenTypes.BLOCK_COMMENT_BEGIN)) {
lastStmt = lastStmt.getPreviousSibling();
}
return lastStmt != null
&& isTerminated(lastStmt, useBreak, useContinue, labels);
}
/**
* Checks if a given IF terminated by return, throw or,
* if allowed break, continue.
*
* @param ast IF to check
* @param useBreak should we consider break as terminator
* @param useContinue should we consider continue as terminator
* @param labels label names
* @return true if IF is terminated.
*/
private boolean checkIf(final DetailAST ast, boolean useBreak,
boolean useContinue, Set<String> labels) {
final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN));
final DetailAST elseStmt = getNextNonCommentAst(thenStmt);
return elseStmt != null
&& isTerminated(thenStmt, useBreak, useContinue, labels)
&& isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels);
}
/**
* This method will skip the comment content while finding the next ast of current ast.
*
* @param ast current ast
* @return next ast after skipping comment
*/
private static DetailAST getNextNonCommentAst(DetailAST ast) {
DetailAST nextSibling = ast.getNextSibling();
while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT,
TokenTypes.BLOCK_COMMENT_BEGIN)) {
nextSibling = nextSibling.getNextSibling();
}
return nextSibling;
}
/**
* Checks if a given loop terminated by return, throw or,
* if allowed break, continue.
*
* @param ast loop to check
* @param labels label names
* @return true if loop is terminated.
*/
private boolean checkLoop(final DetailAST ast, Set<String> labels) {
final DetailAST loopBody;
if (ast.getType() == TokenTypes.LITERAL_DO) {
final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
loopBody = lparen.getPreviousSibling();
}
else {
final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
loopBody = rparen.getNextSibling();
}
return isTerminated(loopBody, false, false, labels);
}
/**
* Checks if a given try/catch/finally block terminated by return, throw or,
* if allowed break, continue.
*
* @param ast loop to check
* @param useBreak should we consider break as terminator
* @param useContinue should we consider continue as terminator
* @param labels label names
* @return true if try/catch/finally block is terminated
*/
private boolean checkTry(final DetailAST ast, boolean useBreak,
boolean useContinue, Set<String> labels) {
final DetailAST finalStmt = ast.getLastChild();
boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
&& isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
useBreak, useContinue, labels);
if (!isTerminated) {
DetailAST firstChild = ast.getFirstChild();
if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
firstChild = firstChild.getNextSibling();
}
isTerminated = isTerminated(firstChild,
useBreak, useContinue, labels);
DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
while (catchStmt != null
&& isTerminated
&& catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
final DetailAST catchBody =
catchStmt.findFirstToken(TokenTypes.SLIST);
isTerminated = isTerminated(catchBody, useBreak, useContinue, labels);
catchStmt = catchStmt.getNextSibling();
}
}
return isTerminated;
}
/**
* Checks if a given switch terminated by return, throw or,
* if allowed break, continue.
*
* @param literalSwitchAst loop to check
* @param useContinue should we consider continue as terminator
* @param labels label names
* @return true if switch is terminated
*/
private boolean checkSwitch(DetailAST literalSwitchAst,
boolean useContinue, Set<String> labels) {
DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
boolean isTerminated = caseGroup != null;
while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
final DetailAST caseBody =
caseGroup.findFirstToken(TokenTypes.SLIST);
isTerminated = caseBody != null
&& isTerminated(caseBody, false, useContinue, labels);
caseGroup = caseGroup.getNextSibling();
}
return isTerminated;
}
/**
* Checks if a given synchronized block terminated by return, throw or,
* if allowed break, continue.
*
* @param synchronizedAst synchronized block to check.
* @param useBreak should we consider break as terminator
* @param useContinue should we consider continue as terminator
* @param labels label names
* @return true if synchronized block is terminated
*/
private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
boolean useContinue, Set<String> labels) {
return isTerminated(
synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels);
}
/**
* Determines if the fall through case between {@code currentCase} and
* {@code nextCase} is relieved by an appropriate comment.
*
* <p>Handles</p>
* <pre>
* case 1:
* /* FALLTHRU */ case 2:
*
* switch(i) {
* default:
* /* FALLTHRU */}
*
* case 1:
* // FALLTHRU
* case 2:
*
* switch(i) {
* default:
* // FALLTHRU
* </pre>
*
* @param currentCase AST of the case that falls through to the next case.
* @return True if a relief comment was found
*/
private boolean hasFallThroughComment(DetailAST currentCase) {
final DetailAST nextSibling = currentCase.getNextSibling();
final DetailAST ast;
if (nextSibling.getType() == TokenTypes.CASE_GROUP) {
ast = nextSibling.getFirstChild();
}
else {
ast = currentCase;
}
return hasReliefComment(ast);
}
/**
* Check if there is any fall through comment.
*
* @param ast ast to check
* @return true if relief comment found
*/
private boolean hasReliefComment(DetailAST ast) {
final DetailAST nonCommentAst = getNextNonCommentAst(ast);
boolean result = false;
if (nonCommentAst != null) {
final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo();
result = Stream.iterate(nonCommentAst.getPreviousSibling(),
Objects::nonNull,
DetailAST::getPreviousSibling)
.takeWhile(sibling -> sibling.getLineNo() == prevLineNumber)
.map(DetailAST::getFirstChild)
.filter(Objects::nonNull)
.anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find());
}
return result;
}
}