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 * &lt;module name=&quot;MutableException&quot;/&gt;
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 * &lt;module name=&quot;MutableException&quot;&gt;
111 *   &lt;property name=&quot;format&quot; value=&quot;^.*Exception$&quot;/&gt;
112 * &lt;/module&gt;
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 * &lt;module name=&quot;MutableException&quot;&gt;
156 *   &lt;property name=&quot;extendedClassNameFormat&quot; value=&quot;^.*Throwable$&quot;/&gt;
157 * &lt;/module&gt;
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}