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.ArrayList; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Set; 026 027import com.puppycrawl.tools.checkstyle.StatelessCheck; 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031 032/** 033 * <div> 034 * Checks that exception throw statements in catch blocks use context variables. 035 * </div> 036 * 037 * <p> 038 * Rationale: When catching an exception and throwing a new exception, it is 039 * recommended to include contextual information in the new exception constructor 040 * (such as method parameters, local variables declared outside the catch block, or fields). 041 * Failing to include context makes troubleshooting and debugging production issues 042 * significantly harder. 043 * </p> 044 * 045 * @since 14.4.0 046 */ 047@StatelessCheck 048public class ExceptionThrowShouldUseContextCheck extends AbstractCheck { 049 050 /** 051 * A key is pointing to the warning message text in "messages.properties" file. 052 */ 053 public static final String MSG_KEY = "exception.throw.should.use.context"; 054 055 /** 056 * Creates a new {@code ExceptionThrowShouldUseContextCheck} instance. 057 */ 058 public ExceptionThrowShouldUseContextCheck() { 059 // no code by default 060 } 061 062 @Override 063 public int[] getDefaultTokens() { 064 return getRequiredTokens(); 065 } 066 067 @Override 068 public int[] getAcceptableTokens() { 069 return getRequiredTokens(); 070 } 071 072 @Override 073 public int[] getRequiredTokens() { 074 return new int[] { 075 TokenTypes.LITERAL_CATCH, 076 }; 077 } 078 079 @Override 080 public void visitToken(DetailAST ast) { 081 final DetailAST paramDef = ast.findFirstToken(TokenTypes.PARAMETER_DEF); 082 final DetailAST paramIdent = paramDef.findFirstToken(TokenTypes.IDENT); 083 final String catchParamName = paramIdent.getText(); 084 final List<DetailAST> throwStatements = new ArrayList<>(); 085 collectThrowStatements(ast, throwStatements); 086 for (DetailAST throwAst : throwStatements) { 087 if (isThrowWithoutContext(throwAst, catchParamName, ast)) { 088 log(throwAst, MSG_KEY); 089 } 090 } 091 } 092 093 /** 094 * Checks if a throw statement throws a new exception without using context variables. 095 * 096 * @param throwAst throw statement ast 097 * @param catchParamName name of the catch parameter 098 * @param catchAst enclosing catch ast 099 * @return true if the throw statement throws a new exception without using context variables 100 */ 101 private static boolean isThrowWithoutContext(DetailAST throwAst, String catchParamName, 102 DetailAST catchAst) { 103 boolean result = false; 104 final DetailAST expr = throwAst.getFirstChild(); 105 final DetailAST thrownExpr = expr.getFirstChild(); 106 if (thrownExpr.getType() == TokenTypes.LITERAL_NEW) { 107 final Set<String> contextVars = collectContextVariables(catchAst, 108 catchParamName); 109 if (!contextVars.isEmpty()) { 110 final DetailAST elist = thrownExpr.findFirstToken(TokenTypes.ELIST); 111 result = !containsAnyVariable(elist, contextVars, catchParamName); 112 } 113 } 114 return result; 115 } 116 117 /** 118 * Recursively collects all throw statements in a block, skipping nested catches, 119 * classes, records, interfaces, and lambdas. 120 * 121 * @param node AST node to search 122 * @param result list to collect throw statements into 123 */ 124 private static void collectThrowStatements(DetailAST node, List<DetailAST> result) { 125 for (DetailAST child = node.getFirstChild(); child != null; 126 child = child.getNextSibling()) { 127 final int type = child.getType(); 128 switch (type) { 129 case TokenTypes.LITERAL_THROW -> result.add(child); 130 case TokenTypes.CLASS_DEF, TokenTypes.RECORD_DEF, TokenTypes.INTERFACE_DEF, 131 TokenTypes.LAMBDA, TokenTypes.LITERAL_CATCH -> { 132 // Do not descend into nested scopes 133 } 134 default -> collectThrowStatements(child, result); 135 } 136 } 137 } 138 139 /** 140 * Checks if an AST node contains any identifier from the variable set, excluding a given name. 141 * 142 * @param node AST node to search 143 * @param variables set of variable names 144 * @param excludedName identifier name to exclude 145 * @return true if any variable is found 146 */ 147 private static boolean containsAnyVariable(DetailAST node, Set<String> variables, 148 String excludedName) { 149 boolean result = false; 150 for (DetailAST child = node.getFirstChild(); child != null && !result; 151 child = child.getNextSibling()) { 152 if (child.getType() == TokenTypes.IDENT) { 153 result = !excludedName.equals(child.getText()) 154 && variables.contains(child.getText()); 155 } 156 else { 157 result = containsAnyVariable(child, variables, excludedName); 158 } 159 } 160 return result; 161 } 162 163 /** 164 * Collects all available context variables in scope: parameters, local variables outside 165 * the catch block, applicable enclosing class fields, and variables initialized with context 166 * inside the catch block. 167 * 168 * @param catchAst catch AST node 169 * @param catchParamName catch parameter name 170 * @return set of available context variable names 171 */ 172 private static Set<String> collectContextVariables(DetailAST catchAst, 173 String catchParamName) { 174 final Set<String> contextVars = new HashSet<>(); 175 final List<DetailAST> methodScopes = findEnclosingMethodScopes(catchAst); 176 for (DetailAST methodScope : methodScopes) { 177 collectParameters(methodScope, contextVars); 178 collectLocalVariablesOutsideCatch(methodScope, catchAst, contextVars); 179 } 180 final DetailAST enclosingClass = findEnclosingClassLike(catchAst); 181 final boolean isStaticMethod = isStaticScope(methodScopes); 182 collectFields(enclosingClass, contextVars, isStaticMethod); 183 if (!isStaticMethod && isInnerClass(enclosingClass)) { 184 final DetailAST outerClass = findEnclosingClassLike(enclosingClass); 185 collectFields(outerClass, contextVars, false); 186 } 187 collectCatchVariablesWithContext(catchAst, contextVars, catchParamName); 188 return contextVars; 189 } 190 191 /** 192 * Finds all enclosing method-like scopes (methods, constructors, or lambdas). 193 * 194 * @param node starting AST node 195 * @return list of enclosing method-like AST nodes 196 */ 197 private static List<DetailAST> findEnclosingMethodScopes(DetailAST node) { 198 final List<DetailAST> scopes = new ArrayList<>(); 199 DetailAST current = node; 200 boolean methodScopeFound = false; 201 while (!methodScopeFound && current.getType() != TokenTypes.OBJBLOCK) { 202 final int type = current.getType(); 203 switch (type) { 204 case TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF, 205 TokenTypes.COMPACT_CTOR_DEF -> { 206 scopes.add(current); 207 methodScopeFound = true; 208 } 209 case TokenTypes.LAMBDA -> { 210 scopes.add(current); 211 current = current.getParent(); 212 } 213 default -> current = current.getParent(); 214 } 215 } 216 return scopes; 217 } 218 219 /** 220 * Checks if any of the enclosing method scopes is declared static. 221 * 222 * @param methodScopes method-like scopes 223 * @return true if any enclosing scope is static 224 */ 225 private static boolean isStaticScope(Iterable<DetailAST> methodScopes) { 226 boolean result = false; 227 for (DetailAST scope : methodScopes) { 228 final DetailAST modifiers = scope.findFirstToken(TokenTypes.MODIFIERS); 229 if (modifiers != null && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null) { 230 result = true; 231 break; 232 } 233 } 234 return result; 235 } 236 237 /** 238 * Collects parameters from a method, constructor, or lambda. 239 * 240 * @param methodAst method-like AST node 241 * @param contextVars set to add parameter names to 242 */ 243 private static void collectParameters(DetailAST methodAst, Set<String> contextVars) { 244 if (methodAst.getType() == TokenTypes.COMPACT_CTOR_DEF) { 245 collectCompactCtorParameters(methodAst, contextVars); 246 } 247 else { 248 collectStandardParameters(methodAst, contextVars); 249 } 250 } 251 252 /** 253 * Collects parameters from a compact constructor in a record. 254 * 255 * @param compactCtorDef compact constructor AST node 256 * @param contextVars set to add parameter names to 257 */ 258 private static void collectCompactCtorParameters(DetailAST compactCtorDef, 259 Set<String> contextVars) { 260 final DetailAST recordDef = compactCtorDef.getParent().getParent(); 261 final DetailAST recordComponents = 262 recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS); 263 addRecordComponents(recordComponents, contextVars); 264 } 265 266 /** 267 * Adds record component names to context variables. 268 * 269 * @param recordComponents record components AST node 270 * @param contextVars set to add component names to 271 */ 272 private static void addRecordComponents(DetailAST recordComponents, 273 Set<String> contextVars) { 274 for (DetailAST comp = recordComponents.getFirstChild(); 275 comp != null; 276 comp = comp.getNextSibling()) { 277 if (comp.getType() == TokenTypes.RECORD_COMPONENT_DEF) { 278 final DetailAST ident = comp.findFirstToken(TokenTypes.IDENT); 279 contextVars.add(ident.getText()); 280 } 281 } 282 } 283 284 /** 285 * Collects parameters from a standard method, constructor, or lambda. 286 * 287 * @param methodAst method-like AST node 288 * @param contextVars set to add parameter names to 289 */ 290 private static void collectStandardParameters(DetailAST methodAst, 291 Set<String> contextVars) { 292 final DetailAST parameters = methodAst.findFirstToken(TokenTypes.PARAMETERS); 293 if (parameters != null) { 294 for (DetailAST param = parameters.getFirstChild(); 295 param != null; 296 param = param.getNextSibling()) { 297 if (param.getType() == TokenTypes.PARAMETER_DEF) { 298 final DetailAST ident = param.findFirstToken(TokenTypes.IDENT); 299 contextVars.add(ident.getText()); 300 } 301 } 302 } 303 else { 304 final DetailAST singleParam = methodAst.findFirstToken(TokenTypes.IDENT); 305 contextVars.add(singleParam.getText()); 306 } 307 } 308 309 /** 310 * Recursively collects local variables within a method, skipping the catch AST node 311 * and nested types. 312 * 313 * @param node AST node to search 314 * @param catchAst catch AST node to skip 315 * @param contextVars set to add variable names to 316 */ 317 private static void collectLocalVariablesOutsideCatch(DetailAST node, DetailAST catchAst, 318 Set<String> contextVars) { 319 for (DetailAST child = node.getFirstChild(); child != null; 320 child = child.getNextSibling()) { 321 if (child.equals(catchAst)) { 322 continue; 323 } 324 processLocalVariableDef(child, contextVars); 325 final int type = child.getType(); 326 if (type != TokenTypes.CLASS_DEF 327 && type != TokenTypes.RECORD_DEF 328 && type != TokenTypes.INTERFACE_DEF) { 329 collectLocalVariablesOutsideCatch(child, catchAst, contextVars); 330 } 331 } 332 } 333 334 /** 335 * Collects variables declared within the catch block whose initializers reference 336 * an already identified context variable. 337 * 338 * @param catchAst catch AST node 339 * @param contextVars set of known context variables 340 * @param catchParamName catch parameter name 341 */ 342 private static void collectCatchVariablesWithContext(DetailAST catchAst, 343 Set<String> contextVars, 344 String catchParamName) { 345 final DetailAST slist = catchAst.findFirstToken(TokenTypes.SLIST); 346 for (DetailAST child = slist.getFirstChild(); child != null; 347 child = child.getNextSibling()) { 348 if (child.getType() == TokenTypes.VARIABLE_DEF) { 349 final DetailAST assign = child.findFirstToken(TokenTypes.ASSIGN); 350 if (assign != null && containsAnyVariable(assign, contextVars, 351 catchParamName)) { 352 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 353 contextVars.add(ident.getText()); 354 } 355 } 356 } 357 } 358 359 /** 360 * Processes a child node and adds any defined local variable name to context variables. 361 * 362 * @param child AST node to inspect 363 * @param contextVars set to add variable names to 364 */ 365 private static void processLocalVariableDef(DetailAST child, Set<String> contextVars) { 366 final int type = child.getType(); 367 switch (type) { 368 case TokenTypes.VARIABLE_DEF, TokenTypes.PATTERN_VARIABLE_DEF -> { 369 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 370 contextVars.add(ident.getText()); 371 } 372 case TokenTypes.RESOURCE -> { 373 final DetailAST resourceIdent = child.findFirstToken(TokenTypes.IDENT); 374 if (resourceIdent != null) { 375 contextVars.add(resourceIdent.getText()); 376 } 377 } 378 default -> { 379 // Ignore other tokens 380 } 381 } 382 } 383 384 /** 385 * Checks if an enclosing class is a non-static inner class. 386 * 387 * @param classDef class AST node 388 * @return true if the class is a non-static inner class 389 */ 390 private static boolean isInnerClass(DetailAST classDef) { 391 final boolean result; 392 if (classDef.getType() == TokenTypes.CLASS_DEF) { 393 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 394 final boolean isStatic = 395 modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 396 result = !isStatic && classDef.getParent().getType() == TokenTypes.OBJBLOCK; 397 } 398 else { 399 result = false; 400 } 401 return result; 402 } 403 404 /** 405 * Finds the nearest enclosing class, record, interface, or enum. 406 * 407 * @param node starting AST node 408 * @return enclosing type AST node, or null if none 409 */ 410 private static DetailAST findEnclosingClassLike(DetailAST node) { 411 DetailAST current = node.getParent(); 412 while (true) { 413 final int type = current.getType(); 414 switch (type) { 415 case TokenTypes.CLASS_DEF, TokenTypes.RECORD_DEF, TokenTypes.INTERFACE_DEF, 416 TokenTypes.ENUM_DEF, TokenTypes.COMPACT_COMPILATION_UNIT -> { 417 return current; 418 } 419 default -> current = current.getParent(); 420 } 421 } 422 } 423 424 /** 425 * Collects field names from an enclosing type's object block, filtering out instance 426 * fields when within a static scope. 427 * 428 * @param classDef enclosing type AST node 429 * @param contextVars set to add field names to 430 * @param isStaticMethod true if enclosing method is static 431 */ 432 private static void collectFields(DetailAST classDef, Set<String> contextVars, 433 boolean isStaticMethod) { 434 final DetailAST container; 435 if (classDef.getType() == TokenTypes.COMPACT_COMPILATION_UNIT) { 436 container = classDef; 437 } 438 else { 439 container = classDef.findFirstToken(TokenTypes.OBJBLOCK); 440 } 441 for (DetailAST child = container.getFirstChild(); 442 child != null; 443 child = child.getNextSibling()) { 444 if (child.getType() == TokenTypes.VARIABLE_DEF) { 445 final DetailAST modifiers = child.findFirstToken(TokenTypes.MODIFIERS); 446 final boolean isStaticField = 447 modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 448 if (!isStaticMethod || isStaticField) { 449 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 450 contextVars.add(ident.getText()); 451 } 452 } 453 } 454 } 455 456}