View Javadoc
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 }