001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 030 031/** 032 * <p> Ensures that exceptions (classes with names conforming to some regular 033 * expression and explicitly extending classes with names conforming to other 034 * regular expression) are immutable. That is, they have only final fields.</p> 035 * <p> Rationale: Exception instances should represent an error 036 * condition. Having non final fields not only allows the state to be 037 * modified by accident and therefore mask the original condition but 038 * also allows developers to accidentally forget to initialise state 039 * thereby leading to code catching the exception to draw incorrect 040 * conclusions based on the state.</p> 041 * 042 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a> 043 */ 044public final class MutableExceptionCheck extends AbstractCheck { 045 046 /** 047 * A key is pointing to the warning message text in "messages.properties" 048 * file. 049 */ 050 public static final String MSG_KEY = "mutable.exception"; 051 052 /** Default value for format and extendedClassNameFormat properties. */ 053 private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$"; 054 /** Stack of checking information for classes. */ 055 private final Deque<Boolean> checkingStack = new ArrayDeque<>(); 056 /** Pattern for class name that is being extended. */ 057 private String extendedClassNameFormat = DEFAULT_FORMAT; 058 /** Should we check current class or not. */ 059 private boolean checking; 060 /** The format string of the regexp. */ 061 private String format = DEFAULT_FORMAT; 062 /** The regexp to match against. */ 063 private Pattern regexp = Pattern.compile(format); 064 065 /** 066 * Sets the format of extended class name to the specified regular expression. 067 * @param extendedClassNameFormat a {@code String} value 068 */ 069 public void setExtendedClassNameFormat(String extendedClassNameFormat) { 070 this.extendedClassNameFormat = extendedClassNameFormat; 071 } 072 073 /** 074 * Set the format to the specified regular expression. 075 * @param format a {@code String} value 076 * @throws org.apache.commons.beanutils.ConversionException unable to parse format 077 */ 078 public void setFormat(String format) { 079 this.format = format; 080 regexp = CommonUtils.createPattern(format); 081 } 082 083 @Override 084 public int[] getDefaultTokens() { 085 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF}; 086 } 087 088 @Override 089 public int[] getRequiredTokens() { 090 return getDefaultTokens(); 091 } 092 093 @Override 094 public int[] getAcceptableTokens() { 095 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF}; 096 } 097 098 @Override 099 public void visitToken(DetailAST ast) { 100 switch (ast.getType()) { 101 case TokenTypes.CLASS_DEF: 102 visitClassDef(ast); 103 break; 104 case TokenTypes.VARIABLE_DEF: 105 visitVariableDef(ast); 106 break; 107 default: 108 throw new IllegalStateException(ast.toString()); 109 } 110 } 111 112 @Override 113 public void leaveToken(DetailAST ast) { 114 if (ast.getType() == TokenTypes.CLASS_DEF) { 115 leaveClassDef(); 116 } 117 } 118 119 /** 120 * Called when we start processing class definition. 121 * @param ast class definition node 122 */ 123 private void visitClassDef(DetailAST ast) { 124 checkingStack.push(checking); 125 checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast); 126 } 127 128 /** Called when we leave class definition. */ 129 private void leaveClassDef() { 130 checking = checkingStack.pop(); 131 } 132 133 /** 134 * Checks variable definition. 135 * @param ast variable def node for check 136 */ 137 private void visitVariableDef(DetailAST ast) { 138 if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) { 139 final DetailAST modifiersAST = 140 ast.findFirstToken(TokenTypes.MODIFIERS); 141 142 if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) { 143 log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY, 144 ast.findFirstToken(TokenTypes.IDENT).getText()); 145 } 146 } 147 } 148 149 /** 150 * Checks that a class name conforms to specified format. 151 * @param ast class definition node 152 * @return true if a class name conforms to specified format 153 */ 154 private boolean isNamedAsException(DetailAST ast) { 155 final String className = ast.findFirstToken(TokenTypes.IDENT).getText(); 156 return regexp.matcher(className).find(); 157 } 158 159 /** 160 * Checks that if extended class name conforms to specified format. 161 * @param ast class definition node 162 * @return true if extended class name conforms to specified format 163 */ 164 private boolean isExtendedClassNamedAsException(DetailAST ast) { 165 final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 166 if (extendsClause != null) { 167 DetailAST currentNode = extendsClause; 168 while (currentNode.getLastChild() != null) { 169 currentNode = currentNode.getLastChild(); 170 } 171 final String extendedClassName = currentNode.getText(); 172 return extendedClassName.matches(extendedClassNameFormat); 173 } 174 return false; 175 } 176}