All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.puppycrawl.tools.checkstyle.checks.coding.FallThroughCheck Maven / Gradle / Ivy

Go to download

Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard

The newest version!
///////////////////////////////////////////////////////////////////////////////////////////////
// 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;

/**
 * 
* Checks for fall-through in {@code switch} statements. * Finds locations where a {@code case} contains Java code but lacks a * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement. *
* *

* 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. *

* *

* Note: The check assumes that there is no unreachable code in the {@code case}. *

*
    *
  • * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked. * Type is {@code boolean}. * Default value is {@code false}. *
  • *
  • * 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)"}. *
  • *
* *

* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} *

* *

* Violation Message Keys: *

*
    *
  • * {@code fall.through} *
  • *
  • * {@code fall.through.last} *
  • *
* * @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 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 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 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 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 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 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 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 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. * *

Handles

*
     * case 1:
     * /* FALLTHRU */ case 2:
     *
     * switch(i) {
     * default:
     * /* FALLTHRU */}
     *
     * case 1:
     * // FALLTHRU
     * case 2:
     *
     * switch(i) {
     * default:
     * // FALLTHRU
     * 
* * @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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy