001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.Objects; 023import java.util.regex.Pattern; 024import java.util.stream.Stream; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 031 032/** 033 * <div> 034 * Checks for fall-through in {@code switch} statements. 035 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a 036 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement. 037 * </div> 038 * 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 * Any other comment may follow on the same line. 049 * </p> 050 * 051 * <p> 052 * Note: The check assumes that there is no unreachable code in the {@code case}. 053 * </p> 054 * 055 * @since 3.4 056 */ 057@StatelessCheck 058public class FallThroughCheck extends AbstractCheck { 059 060 /** 061 * A key is pointing to the warning message text in "messages.properties" 062 * file. 063 */ 064 public static final String MSG_FALL_THROUGH = "fall.through"; 065 066 /** 067 * A key is pointing to the warning message text in "messages.properties" 068 * file. 069 */ 070 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 071 072 /** Control whether the last case group must be checked. */ 073 private boolean checkLastCaseGroup; 074 075 /** 076 * Define the RegExp to match the relief comment that suppresses 077 * the warning about a fall through. 078 */ 079 private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)"); 080 081 @Override 082 public int[] getDefaultTokens() { 083 return getRequiredTokens(); 084 } 085 086 @Override 087 public int[] getRequiredTokens() { 088 return new int[] {TokenTypes.CASE_GROUP}; 089 } 090 091 @Override 092 public int[] getAcceptableTokens() { 093 return getRequiredTokens(); 094 } 095 096 @Override 097 public boolean isCommentNodesRequired() { 098 return true; 099 } 100 101 /** 102 * Setter to define the RegExp to match the relief comment that suppresses 103 * the warning about a fall through. 104 * 105 * @param pattern 106 * The regular expression pattern. 107 * @since 4.0 108 */ 109 public void setReliefPattern(Pattern pattern) { 110 reliefPattern = pattern; 111 } 112 113 /** 114 * Setter to control whether the last case group must be checked. 115 * 116 * @param value new value of the property. 117 * @since 4.0 118 */ 119 public void setCheckLastCaseGroup(boolean value) { 120 checkLastCaseGroup = value; 121 } 122 123 @Override 124 public void visitToken(DetailAST ast) { 125 final DetailAST nextGroup = ast.getNextSibling(); 126 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 127 if (!isLastGroup || checkLastCaseGroup) { 128 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 129 130 if (slist != null && !CheckUtil.isTerminated(slist) && !hasFallThroughComment(ast)) { 131 if (isLastGroup) { 132 log(ast, MSG_FALL_THROUGH_LAST); 133 } 134 else { 135 log(nextGroup, MSG_FALL_THROUGH); 136 } 137 } 138 } 139 } 140 141 /** 142 * Determines if the fall through case between {@code currentCase} and 143 * {@code nextCase} is relieved by an appropriate comment. 144 * 145 * <p>Handles</p> 146 * <pre> 147 * case 1: 148 * /* FALLTHRU */ case 2: 149 * 150 * switch(i) { 151 * default: 152 * /* FALLTHRU */} 153 * 154 * case 1: 155 * // FALLTHRU 156 * case 2: 157 * 158 * switch(i) { 159 * default: 160 * // FALLTHRU 161 * </pre> 162 * 163 * @param currentCase AST of the case that falls through to the next case. 164 * @return True if a relief comment was found 165 */ 166 private boolean hasFallThroughComment(DetailAST currentCase) { 167 final DetailAST nextSibling = currentCase.getNextSibling(); 168 final DetailAST ast; 169 if (nextSibling.getType() == TokenTypes.CASE_GROUP) { 170 ast = nextSibling.getFirstChild(); 171 } 172 else { 173 ast = currentCase; 174 } 175 return hasReliefComment(ast); 176 } 177 178 /** 179 * Check if there is any fall through comment. 180 * 181 * @param ast ast to check 182 * @return true if relief comment found 183 */ 184 private boolean hasReliefComment(DetailAST ast) { 185 final DetailAST nonCommentAst = CheckUtil.getNextNonCommentAst(ast); 186 boolean result = false; 187 if (nonCommentAst != null) { 188 final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo(); 189 result = Stream.iterate(nonCommentAst.getPreviousSibling(), 190 Objects::nonNull, 191 DetailAST::getPreviousSibling) 192 .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber) 193 .map(DetailAST::getFirstChild) 194 .filter(Objects::nonNull) 195 .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find()); 196 } 197 return result; 198 } 199 200}