1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2024 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 /////////////////////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.design; 21 22 import java.util.ArrayDeque; 23 import java.util.Deque; 24 import java.util.regex.Pattern; 25 26 import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 28 import com.puppycrawl.tools.checkstyle.api.DetailAST; 29 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 30 31 /** 32 * <p> 33 * Ensures that exception classes (classes with names conforming to some pattern 34 * and explicitly extending classes with names conforming to other 35 * pattern) are immutable, that is, that they have only final fields. 36 * </p> 37 * <p> 38 * The current algorithm is very simple: it checks that all members of exception are final. 39 * The user can still mutate an exception's instance (e.g. Throwable has a method called 40 * {@code setStackTrace} which changes the exception's stack trace). But, at least, all 41 * information provided by this exception type is unchangeable. 42 * </p> 43 * <p> 44 * Rationale: Exception instances should represent an error 45 * condition. Having non-final fields not only allows the state to be 46 * modified by accident and therefore mask the original condition but 47 * also allows developers to accidentally forget to set the initial state. 48 * In both cases, code catching the exception could draw incorrect 49 * conclusions based on the state. 50 * </p> 51 * <ul> 52 * <li> 53 * Property {@code extendedClassNameFormat} - Specify pattern for extended class names. 54 * Type is {@code java.util.regex.Pattern}. 55 * Default value is {@code "^.*Exception$|^.*Error$|^.*Throwable$"}. 56 * </li> 57 * <li> 58 * Property {@code format} - Specify pattern for exception class names. 59 * Type is {@code java.util.regex.Pattern}. 60 * Default value is {@code "^.*Exception$|^.*Error$|^.*Throwable$"}. 61 * </li> 62 * </ul> 63 * <p> 64 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 65 * </p> 66 * <p> 67 * Violation Message Keys: 68 * </p> 69 * <ul> 70 * <li> 71 * {@code mutable.exception} 72 * </li> 73 * </ul> 74 * 75 * @since 3.2 76 */ 77 @FileStatefulCheck 78 public final class MutableExceptionCheck extends AbstractCheck { 79 80 /** 81 * A key is pointing to the warning message text in "messages.properties" 82 * file. 83 */ 84 public static final String MSG_KEY = "mutable.exception"; 85 86 /** Default value for format and extendedClassNameFormat properties. */ 87 private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$"; 88 /** Stack of checking information for classes. */ 89 private final Deque<Boolean> checkingStack = new ArrayDeque<>(); 90 /** Specify pattern for extended class names. */ 91 private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT); 92 /** Should we check current class or not. */ 93 private boolean checking; 94 /** Specify pattern for exception class names. */ 95 private Pattern format = extendedClassNameFormat; 96 97 /** 98 * Setter to specify pattern for extended class names. 99 * 100 * @param extendedClassNameFormat a {@code String} value 101 * @since 6.2 102 */ 103 public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) { 104 this.extendedClassNameFormat = extendedClassNameFormat; 105 } 106 107 /** 108 * Setter to specify pattern for exception class names. 109 * 110 * @param pattern the new pattern 111 * @since 3.2 112 */ 113 public void setFormat(Pattern pattern) { 114 format = pattern; 115 } 116 117 @Override 118 public int[] getDefaultTokens() { 119 return getRequiredTokens(); 120 } 121 122 @Override 123 public int[] getRequiredTokens() { 124 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF}; 125 } 126 127 @Override 128 public int[] getAcceptableTokens() { 129 return getRequiredTokens(); 130 } 131 132 @Override 133 public void visitToken(DetailAST ast) { 134 switch (ast.getType()) { 135 case TokenTypes.CLASS_DEF: 136 visitClassDef(ast); 137 break; 138 case TokenTypes.VARIABLE_DEF: 139 visitVariableDef(ast); 140 break; 141 default: 142 throw new IllegalStateException(ast.toString()); 143 } 144 } 145 146 @Override 147 public void leaveToken(DetailAST ast) { 148 if (ast.getType() == TokenTypes.CLASS_DEF) { 149 leaveClassDef(); 150 } 151 } 152 153 /** 154 * Called when we start processing class definition. 155 * 156 * @param ast class definition node 157 */ 158 private void visitClassDef(DetailAST ast) { 159 checkingStack.push(checking); 160 checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast); 161 } 162 163 /** Called when we leave class definition. */ 164 private void leaveClassDef() { 165 checking = checkingStack.pop(); 166 } 167 168 /** 169 * Checks variable definition. 170 * 171 * @param ast variable def node for check 172 */ 173 private void visitVariableDef(DetailAST ast) { 174 if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) { 175 final DetailAST modifiersAST = 176 ast.findFirstToken(TokenTypes.MODIFIERS); 177 178 if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) { 179 log(ast, MSG_KEY, ast.findFirstToken(TokenTypes.IDENT).getText()); 180 } 181 } 182 } 183 184 /** 185 * Checks that a class name conforms to specified format. 186 * 187 * @param ast class definition node 188 * @return true if a class name conforms to specified format 189 */ 190 private boolean isNamedAsException(DetailAST ast) { 191 final String className = ast.findFirstToken(TokenTypes.IDENT).getText(); 192 return format.matcher(className).find(); 193 } 194 195 /** 196 * Checks that if extended class name conforms to specified format. 197 * 198 * @param ast class definition node 199 * @return true if extended class name conforms to specified format 200 */ 201 private boolean isExtendedClassNamedAsException(DetailAST ast) { 202 boolean result = false; 203 final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 204 if (extendsClause != null) { 205 DetailAST currentNode = extendsClause; 206 while (currentNode.getLastChild() != null) { 207 currentNode = currentNode.getLastChild(); 208 } 209 final String extendedClassName = currentNode.getText(); 210 result = extendedClassNameFormat.matcher(extendedClassName).matches(); 211 } 212 return result; 213 } 214 215 }