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.ArrayDeque; 023import java.util.Deque; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029 030/** 031 * <div> 032 * Checks that throw statements within catch blocks do not use 033 * the caught exception's getMessage() call when the thrown exception 034 * is the same type as the caught exception, as rethrowing the same 035 * exception type with its own message is redundant. 036 * </div> 037 * 038 * <p> 039 * Rationale: When throwing an exception of the same type as the caught 040 * exception and including the caught exception's message via getMessage(), 041 * the information is redundant. The original exception should be rethrown 042 * directly, or a different exception type should be used. 043 * </p> 044 * 045 * <p> 046 * Example of violations: 047 * </p> 048 * {@snippet lang="text" : 049 * catch (IOException ex) { 050 * throw new IOException("Error: " + ex.getMessage()); // violation 051 * } 052 * } 053 * 054 * <p> 055 * Correct usage: 056 * </p> 057 * {@snippet lang="text" : 058 * catch (IOException ex) { 059 * throw new RuntimeException("Error: " + ex.getMessage(), ex); // OK, different type 060 * } 061 * catch (IOException ex) { 062 * throw new IOException("Error processing file", ex); // OK, no getMessage() 063 * } 064 * } 065 * 066 * @since 14.4.0 067 */ 068@FileStatefulCheck 069public class NoCauseGetMessageInExceptionConstructorCheck extends AbstractCheck { 070 071 /** 072 * A key is pointing to the warning message text in "messages.properties" file. 073 */ 074 public static final String MSG_KEY = "no.cause.getmessage.in.exception.constructor"; 075 076 /** Stack of catch blocks currently in scope during traversal. */ 077 private final Deque<DetailAST> catchScopes = new ArrayDeque<>(); 078 079 /** 080 * Creates a new {@code NoCauseGetMessageInExceptionConstructorCheck} instance. 081 */ 082 public NoCauseGetMessageInExceptionConstructorCheck() { 083 // no code by default 084 } 085 086 @Override 087 public int[] getDefaultTokens() { 088 return getRequiredTokens(); 089 } 090 091 @Override 092 public int[] getAcceptableTokens() { 093 return getRequiredTokens(); 094 } 095 096 @Override 097 public int[] getRequiredTokens() { 098 return new int[] {TokenTypes.LITERAL_THROW, TokenTypes.LITERAL_CATCH}; 099 } 100 101 @Override 102 public void beginTree(DetailAST rootAST) { 103 catchScopes.clear(); 104 } 105 106 @Override 107 public void visitToken(DetailAST ast) { 108 if (ast.getType() == TokenTypes.LITERAL_CATCH) { 109 catchScopes.push(ast); 110 } 111 else { 112 checkThrow(ast); 113 } 114 } 115 116 @Override 117 public void leaveToken(DetailAST ast) { 118 if (ast.getType() == TokenTypes.LITERAL_CATCH) { 119 catchScopes.pop(); 120 } 121 } 122 123 /** 124 * Reports the throw statement if it references a matching caught exception's 125 * {@code getMessage()} inside its expression. 126 * 127 * @param throwAst the LITERAL_THROW AST node 128 */ 129 private void checkThrow(DetailAST throwAst) { 130 final String thrownType = getThrownType(throwAst); 131 if (thrownType != null && hasViolatingGetMessage(throwAst, thrownType)) { 132 log(throwAst, MSG_KEY); 133 } 134 } 135 136 /** 137 * Gets the simple type name of the exception being thrown via a new expression. 138 * 139 * @param throwAst the LITERAL_THROW AST node 140 * @return the simple type name, or null if the throw does not use a new expression 141 */ 142 private static String getThrownType(DetailAST throwAst) { 143 final DetailAST expr = throwAst.getFirstChild(); 144 final DetailAST literalNew = expr.findFirstToken(TokenTypes.LITERAL_NEW); 145 String result = null; 146 if (literalNew != null) { 147 // Check for simple name (IDENT) 148 final DetailAST ident = literalNew.findFirstToken(TokenTypes.IDENT); 149 if (ident != null) { 150 result = ident.getText(); 151 } 152 else { 153 // Check for qualified name (DOT) 154 final DetailAST dot = literalNew.findFirstToken(TokenTypes.DOT); 155 // Get the last part of the qualified name (the simple class name) 156 result = dot.getLastChild().getText(); 157 } 158 } 159 return result; 160 } 161 162 /** 163 * Recursively checks if the AST subtree contains a {@code getMessage()} call 164 * on an enclosing caught exception whose declared type matches the thrown type. 165 * 166 * @param ast the AST node to search 167 * @param thrownType the simple type name of the thrown exception 168 * @return true if a violating call is found 169 */ 170 private boolean hasViolatingGetMessage(DetailAST ast, String thrownType) { 171 boolean found = isViolatingGetMessage(ast, thrownType); 172 for (DetailAST child = ast.getFirstChild(); 173 child != null && !found; child = child.getNextSibling()) { 174 found = hasViolatingGetMessage(child, thrownType); 175 } 176 return found; 177 } 178 179 /** 180 * Checks if a single node is {@code <var>.getMessage()} where {@code var} is 181 * declared by any enclosing catch whose type matches the thrown type. 182 * 183 * @param node the AST node to check 184 * @param thrownType the simple type name of the thrown exception 185 * @return true if this is a matching call 186 */ 187 private boolean isViolatingGetMessage(DetailAST node, String thrownType) { 188 boolean result = false; 189 final DetailAST dot = node.findFirstToken(TokenTypes.DOT); 190 if (dot != null && "getMessage".equals(dot.getLastChild().getText())) { 191 final String varName = dot.getFirstChild().getText(); 192 for (DetailAST catchBlock : catchScopes) { 193 if (matchesCatchScope(catchBlock, varName, thrownType)) { 194 result = true; 195 break; 196 } 197 } 198 } 199 return result; 200 } 201 202 /** 203 * Checks whether the given catch block declares the specified variable and 204 * catches a type matching the thrown type. 205 * 206 * @param catchBlock the LITERAL_CATCH AST node 207 * @param varName the caught-exception variable name to match 208 * @param thrownType the simple type name of the thrown exception 209 * @return true if the catch matches both the variable and the type 210 */ 211 private static boolean matchesCatchScope(DetailAST catchBlock, String varName, 212 String thrownType) { 213 final DetailAST paramDef = catchBlock.findFirstToken(TokenTypes.PARAMETER_DEF); 214 final DetailAST ident = paramDef.findFirstToken(TokenTypes.IDENT); 215 return varName.equals(ident.getText()) 216 && containsTypeName(paramDef, thrownType); 217 } 218 219 /** 220 * Recursively checks if the given AST contains a type matching the specified name. 221 * Handles IDENT (simple names), DOT (qualified names), and BOR (multi-catch) nodes. 222 * 223 * @param ast the AST node to search 224 * @param typeName the type name to match 225 * @return true if a matching type is found 226 */ 227 private static boolean containsTypeName(DetailAST ast, String typeName) { 228 boolean result = typeName.equals(ast.getText()); 229 230 for (DetailAST child = ast.getFirstChild(); 231 child != null && !result; child = child.getNextSibling()) { 232 result = containsTypeName(child, typeName); 233 } 234 235 return result; 236 } 237 238}