001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.design; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024import java.util.regex.Pattern; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030 031/** 032 * <p> 033 * Ensures that exception classes (classes with names conforming to some pattern 034 * and explicitly extending classes with names conforming to other 035 * pattern) are immutable, that is, that they have only final fields. 036 * </p> 037 * <p> 038 * The current algorithm is very simple: it checks that all members of exception are final. 039 * The user can still mutate an exception's instance (e.g. Throwable has a method called 040 * {@code setStackTrace} which changes the exception's stack trace). But, at least, all 041 * information provided by this exception type is unchangeable. 042 * </p> 043 * <p> 044 * Rationale: Exception instances should represent an error 045 * condition. Having non-final fields not only allows the state to be 046 * modified by accident and therefore mask the original condition but 047 * also allows developers to accidentally forget to set the initial state. 048 * In both cases, code catching the exception could draw incorrect 049 * conclusions based on the state. 050 * </p> 051 * <ul> 052 * <li> 053 * Property {@code format} - Specify pattern for exception class names. 054 * Type is {@code java.util.regex.Pattern}. 055 * Default value is {@code "^.*Exception$|^.*Error$|^.*Throwable$"}. 056 * </li> 057 * <li> 058 * Property {@code extendedClassNameFormat} - Specify pattern for extended class names. 059 * Type is {@code java.util.regex.Pattern}. 060 * Default value is {@code "^.*Exception$|^.*Error$|^.*Throwable$"}. 061 * </li> 062 * </ul> 063 * <p> 064 * To configure the check: 065 * </p> 066 * <pre> 067 * <module name="MutableException"/> 068 * </pre> 069 * <p>Example:</p> 070 * <pre> 071 * class FirstClass extends Exception { 072 * private int code; // OK, class name doesn't match with default pattern 073 * 074 * public FirstClass() { 075 * code = 1; 076 * } 077 * } 078 * 079 * class MyException extends Exception { 080 * private int code; // violation, The field 'code' must be declared final 081 * 082 * public MyException() { 083 * code = 2; 084 * } 085 * } 086 * 087 * class MyThrowable extends Throwable { 088 * final int code; // OK 089 * String message; // violation, The field 'message' must be declared final 090 * 091 * public MyThrowable(int code, String message) { 092 * this.code = code; 093 * this.message = message; 094 * } 095 * } 096 * 097 * class BadException extends java.lang.Exception { 098 * int code; // violation, The field 'code' must be declared final 099 * 100 * public BadException(int code) { 101 * this.code = code; 102 * } 103 * } 104 * </pre> 105 * <p> 106 * To configure the check so that it checks for class name that ends 107 * with 'Exception': 108 * </p> 109 * <pre> 110 * <module name="MutableException"> 111 * <property name="format" value="^.*Exception$"/> 112 * </module> 113 * </pre> 114 * <p>Example:</p> 115 * <pre> 116 * class FirstClass extends Exception { 117 * private int code; // OK, class name doesn't match with given pattern 118 * 119 * public FirstClass() { 120 * code = 1; 121 * } 122 * } 123 * 124 * class MyException extends Exception { 125 * private int code; // violation, The field 'code' must be declared final 126 * 127 * public MyException() { 128 * code = 2; 129 * } 130 * } 131 * 132 * class MyThrowable extends Throwable { 133 * final int code; // OK, class name doesn't match with given pattern 134 * String message; // OK, class name doesn't match with given pattern 135 * 136 * public MyThrowable(int code, String message) { 137 * this.code = code; 138 * this.message = message; 139 * } 140 * } 141 * 142 * class BadException extends java.lang.Exception { 143 * int code; // violation, The field 'code' must be declared final 144 * 145 * public BadException(int code) { 146 * this.code = code; 147 * } 148 * } 149 * </pre> 150 * <p> 151 * To configure the check so that it checks for type name that is used in 152 * 'extends' and ends with 'Throwable': 153 * </p> 154 * <pre> 155 * <module name="MutableException"> 156 * <property name="extendedClassNameFormat" value="^.*Throwable$"/> 157 * </module> 158 * </pre> 159 * <p>Example:</p> 160 * <pre> 161 * class FirstClass extends Exception { 162 * private int code; // OK, extended class name doesn't match with given pattern 163 * 164 * public FirstClass() { 165 * code = 1; 166 * } 167 * } 168 * 169 * class MyException extends Exception { 170 * private int code; // OK, extended class name doesn't match with given pattern 171 * 172 * public MyException() { 173 * code = 2; 174 * } 175 * } 176 * 177 * class MyThrowable extends Throwable { 178 * final int code; // OK 179 * String message; // violation, The field 'message' must be declared final 180 * 181 * public MyThrowable(int code, String message) { 182 * this.code = code; 183 * this.message = message; 184 * } 185 * } 186 * 187 * class BadException extends java.lang.Exception { 188 * int code; // OK, extended class name doesn't match with given pattern 189 * 190 * public BadException(int code) { 191 * this.code = code; 192 * } 193 * } 194 * </pre> 195 * <p> 196 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 197 * </p> 198 * <p> 199 * Violation Message Keys: 200 * </p> 201 * <ul> 202 * <li> 203 * {@code mutable.exception} 204 * </li> 205 * </ul> 206 * 207 * @since 3.2 208 */ 209@FileStatefulCheck 210public final class MutableExceptionCheck extends AbstractCheck { 211 212 /** 213 * A key is pointing to the warning message text in "messages.properties" 214 * file. 215 */ 216 public static final String MSG_KEY = "mutable.exception"; 217 218 /** Default value for format and extendedClassNameFormat properties. */ 219 private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$"; 220 /** Stack of checking information for classes. */ 221 private final Deque<Boolean> checkingStack = new ArrayDeque<>(); 222 /** Specify pattern for extended class names. */ 223 private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT); 224 /** Should we check current class or not. */ 225 private boolean checking; 226 /** Specify pattern for exception class names. */ 227 private Pattern format = extendedClassNameFormat; 228 229 /** 230 * Setter to specify pattern for extended class names. 231 * 232 * @param extendedClassNameFormat a {@code String} value 233 */ 234 public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) { 235 this.extendedClassNameFormat = extendedClassNameFormat; 236 } 237 238 /** 239 * Setter to specify pattern for exception class names. 240 * 241 * @param pattern the new pattern 242 */ 243 public void setFormat(Pattern pattern) { 244 format = pattern; 245 } 246 247 @Override 248 public int[] getDefaultTokens() { 249 return getRequiredTokens(); 250 } 251 252 @Override 253 public int[] getRequiredTokens() { 254 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF}; 255 } 256 257 @Override 258 public int[] getAcceptableTokens() { 259 return getRequiredTokens(); 260 } 261 262 @Override 263 public void visitToken(DetailAST ast) { 264 switch (ast.getType()) { 265 case TokenTypes.CLASS_DEF: 266 visitClassDef(ast); 267 break; 268 case TokenTypes.VARIABLE_DEF: 269 visitVariableDef(ast); 270 break; 271 default: 272 throw new IllegalStateException(ast.toString()); 273 } 274 } 275 276 @Override 277 public void leaveToken(DetailAST ast) { 278 if (ast.getType() == TokenTypes.CLASS_DEF) { 279 leaveClassDef(); 280 } 281 } 282 283 /** 284 * Called when we start processing class definition. 285 * 286 * @param ast class definition node 287 */ 288 private void visitClassDef(DetailAST ast) { 289 checkingStack.push(checking); 290 checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast); 291 } 292 293 /** Called when we leave class definition. */ 294 private void leaveClassDef() { 295 checking = checkingStack.pop(); 296 } 297 298 /** 299 * Checks variable definition. 300 * 301 * @param ast variable def node for check 302 */ 303 private void visitVariableDef(DetailAST ast) { 304 if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) { 305 final DetailAST modifiersAST = 306 ast.findFirstToken(TokenTypes.MODIFIERS); 307 308 if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) { 309 log(ast, MSG_KEY, ast.findFirstToken(TokenTypes.IDENT).getText()); 310 } 311 } 312 } 313 314 /** 315 * Checks that a class name conforms to specified format. 316 * 317 * @param ast class definition node 318 * @return true if a class name conforms to specified format 319 */ 320 private boolean isNamedAsException(DetailAST ast) { 321 final String className = ast.findFirstToken(TokenTypes.IDENT).getText(); 322 return format.matcher(className).find(); 323 } 324 325 /** 326 * Checks that if extended class name conforms to specified format. 327 * 328 * @param ast class definition node 329 * @return true if extended class name conforms to specified format 330 */ 331 private boolean isExtendedClassNamedAsException(DetailAST ast) { 332 boolean result = false; 333 final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 334 if (extendsClause != null) { 335 DetailAST currentNode = extendsClause; 336 while (currentNode.getLastChild() != null) { 337 currentNode = currentNode.getLastChild(); 338 } 339 final String extendedClassName = currentNode.getText(); 340 result = extendedClassNameFormat.matcher(extendedClassName).matches(); 341 } 342 return result; 343 } 344 345}