1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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 * <div>
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 * </div>
37 *
38 * <p>
39 * The current algorithm is very simple: it checks that all members of exception are final.
40 * The user can still mutate an exception's instance (e.g. Throwable has a method called
41 * {@code setStackTrace} which changes the exception's stack trace). But, at least, all
42 * information provided by this exception type is unchangeable.
43 * </p>
44 *
45 * <p>
46 * Rationale: Exception instances should represent an error
47 * condition. Having non-final fields not only allows the state to be
48 * modified by accident and therefore mask the original condition but
49 * also allows developers to accidentally forget to set the initial state.
50 * In both cases, code catching the exception could draw incorrect
51 * conclusions based on the state.
52 * </p>
53 *
54 * @since 3.2
55 */
56 @FileStatefulCheck
57 public final class MutableExceptionCheck extends AbstractCheck {
58
59 /**
60 * A key is pointing to the warning message text in "messages.properties"
61 * file.
62 */
63 public static final String MSG_KEY = "mutable.exception";
64
65 /** Default value for format and extendedClassNameFormat properties. */
66 private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$";
67 /** Stack of checking information for classes. */
68 private final Deque<Boolean> checkingStack = new ArrayDeque<>();
69 /** Specify pattern for extended class names. */
70 private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT);
71 /** Should we check current class or not. */
72 private boolean checking;
73 /** Specify pattern for exception class names. */
74 private Pattern format = extendedClassNameFormat;
75
76 /**
77 * Creates a new {@code MutableExceptionCheck} instance.
78 */
79 public MutableExceptionCheck() {
80 // no code by default
81 }
82
83 /**
84 * Setter to specify pattern for extended class names.
85 *
86 * @param extendedClassNameFormat a {@code String} value
87 * @since 6.2
88 */
89 public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) {
90 this.extendedClassNameFormat = extendedClassNameFormat;
91 }
92
93 /**
94 * Setter to specify pattern for exception class names.
95 *
96 * @param pattern the new pattern
97 * @since 3.2
98 */
99 public void setFormat(Pattern pattern) {
100 format = pattern;
101 }
102
103 @Override
104 public int[] getDefaultTokens() {
105 return getRequiredTokens();
106 }
107
108 @Override
109 public int[] getRequiredTokens() {
110 return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF};
111 }
112
113 @Override
114 public int[] getAcceptableTokens() {
115 return getRequiredTokens();
116 }
117
118 @Override
119 public void visitToken(DetailAST ast) {
120 switch (ast.getType()) {
121 case TokenTypes.CLASS_DEF -> visitClassDef(ast);
122 case TokenTypes.VARIABLE_DEF -> visitVariableDef(ast);
123 default -> throw new IllegalStateException(ast.toString());
124 }
125 }
126
127 @Override
128 public void leaveToken(DetailAST ast) {
129 if (ast.getType() == TokenTypes.CLASS_DEF) {
130 leaveClassDef();
131 }
132 }
133
134 /**
135 * Called when we start processing class definition.
136 *
137 * @param ast class definition node
138 */
139 private void visitClassDef(DetailAST ast) {
140 checkingStack.push(checking);
141 checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast);
142 }
143
144 /** Called when we leave class definition. */
145 private void leaveClassDef() {
146 checking = checkingStack.pop();
147 }
148
149 /**
150 * Checks variable definition.
151 *
152 * @param ast variable def node for check
153 */
154 private void visitVariableDef(DetailAST ast) {
155 if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) {
156 final DetailAST modifiersAST =
157 ast.findFirstToken(TokenTypes.MODIFIERS);
158
159 if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) {
160 log(ast, MSG_KEY, ast.findFirstToken(TokenTypes.IDENT).getText());
161 }
162 }
163 }
164
165 /**
166 * Checks that a class name conforms to specified format.
167 *
168 * @param ast class definition node
169 * @return true if a class name conforms to specified format
170 */
171 private boolean isNamedAsException(DetailAST ast) {
172 final String className = ast.findFirstToken(TokenTypes.IDENT).getText();
173 return format.matcher(className).find();
174 }
175
176 /**
177 * Checks that if extended class name conforms to specified format.
178 *
179 * @param ast class definition node
180 * @return true if extended class name conforms to specified format
181 */
182 private boolean isExtendedClassNamedAsException(DetailAST ast) {
183 boolean result = false;
184 final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
185 if (extendsClause != null) {
186 DetailAST currentNode = extendsClause;
187 while (currentNode.getLastChild() != null) {
188 currentNode = currentNode.getLastChild();
189 }
190 final String extendedClassName = currentNode.getText();
191 result = extendedClassNameFormat.matcher(extendedClassName).matches();
192 }
193 return result;
194 }
195
196 }