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 /** 082 * Creates a new {@code FallThroughCheck} instance. 083 */ 084 public FallThroughCheck() { 085 // no code by default 086 } 087 088 @Override 089 public int[] getDefaultTokens() { 090 return getRequiredTokens(); 091 } 092 093 @Override 094 public int[] getRequiredTokens() { 095 return new int[] {TokenTypes.CASE_GROUP}; 096 } 097 098 @Override 099 public int[] getAcceptableTokens() { 100 return getRequiredTokens(); 101 } 102 103 @Override 104 public boolean isCommentNodesRequired() { 105 return true; 106 } 107 108 /** 109 * Setter to define the RegExp to match the relief comment that suppresses 110 * the warning about a fall through. 111 * 112 * @param pattern 113 * The regular expression pattern. 114 * @since 4.0 115 */ 116 public void setReliefPattern(Pattern pattern) { 117 reliefPattern = pattern; 118 } 119 120 /** 121 * Setter to control whether the last case group must be checked. 122 * 123 * @param value new value of the property. 124 * @since 4.0 125 */ 126 public void setCheckLastCaseGroup(boolean value) { 127 checkLastCaseGroup = value; 128 } 129 130 @Override 131 public void visitToken(DetailAST ast) { 132 final DetailAST nextGroup = ast.getNextSibling(); 133 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 134 if (!isLastGroup || checkLastCaseGroup) { 135 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 136 137 if (slist != null && !CheckUtil.isTerminated(slist) && !hasFallThroughComment(ast)) { 138 if (isLastGroup) { 139 log(ast, MSG_FALL_THROUGH_LAST); 140 } 141 else { 142 log(nextGroup, MSG_FALL_THROUGH); 143 } 144 } 145 } 146 } 147 148 /** 149 * Determines if the fall through case between {@code currentCase} and 150 * {@code nextCase} is relieved by an appropriate comment. 151 * 152 * <p>Handles</p> 153 * <pre> 154 * case 1: 155 * /* FALLTHRU */ case 2: 156 * 157 * switch(i) { 158 * default: 159 * /* FALLTHRU */} 160 * 161 * case 1: 162 * // FALLTHRU 163 * case 2: 164 * 165 * switch(i) { 166 * default: 167 * // FALLTHRU 168 * </pre> 169 * 170 * @param currentCase AST of the case that falls through to the next case. 171 * @return True if a relief comment was found 172 */ 173 private boolean hasFallThroughComment(DetailAST currentCase) { 174 final DetailAST nextSibling = currentCase.getNextSibling(); 175 final DetailAST ast; 176 if (nextSibling.getType() == TokenTypes.CASE_GROUP) { 177 ast = nextSibling.getFirstChild(); 178 } 179 else { 180 ast = currentCase; 181 } 182 return hasReliefComment(ast); 183 } 184 185 /** 186 * Check if there is any fall through comment. 187 * 188 * @param ast ast to check 189 * @return true if relief comment found 190 */ 191 private boolean hasReliefComment(DetailAST ast) { 192 final DetailAST nonCommentAst = CheckUtil.getNextNonCommentAst(ast); 193 boolean result = false; 194 if (nonCommentAst != null) { 195 final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo(); 196 result = Stream.iterate(nonCommentAst.getPreviousSibling(), 197 Objects::nonNull, 198 DetailAST::getPreviousSibling) 199 .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber) 200 .map(DetailAST::getFirstChild) 201 .filter(Objects::nonNull) 202 .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find()); 203 } 204 return result; 205 } 206 207}