1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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> Ensures that exceptions (classes with names conforming to some regular
33   * expression and explicitly extending classes with names conforming to other
34   * regular expression) are immutable. That is, they have only final fields.</p>
35   * <p> Rationale: Exception instances should represent an error
36   * condition. Having non final fields not only allows the state to be
37   * modified by accident and therefore mask the original condition but
38   * also allows developers to accidentally forget to initialise state
39   * thereby leading to code catching the exception to draw incorrect
40   * conclusions based on the state.</p>
41   *
42   */
43  @FileStatefulCheck
44  public final class MutableExceptionCheck extends AbstractCheck {
45  
46      /**
47       * A key is pointing to the warning message text in "messages.properties"
48       * file.
49       */
50      public static final String MSG_KEY = "mutable.exception";
51  
52      /** Default value for format and extendedClassNameFormat properties. */
53      private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$";
54      /** Stack of checking information for classes. */
55      private final Deque<Boolean> checkingStack = new ArrayDeque<>();
56      /** Pattern for class name that is being extended. */
57      private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT);
58      /** Should we check current class or not. */
59      private boolean checking;
60      /** The regexp to match against. */
61      private Pattern format = Pattern.compile(DEFAULT_FORMAT);
62  
63      /**
64       * Sets the format of extended class name to the specified regular expression.
65       * @param extendedClassNameFormat a {@code String} value
66       */
67      public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) {
68          this.extendedClassNameFormat = extendedClassNameFormat;
69      }
70  
71      /**
72       * Set the format for the specified regular expression.
73       * @param pattern the new pattern
74       */
75      public void setFormat(Pattern pattern) {
76          format = pattern;
77      }
78  
79      @Override
80      public int[] getDefaultTokens() {
81          return getRequiredTokens();
82      }
83  
84      @Override
85      public int[] getRequiredTokens() {
86          return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF};
87      }
88  
89      @Override
90      public int[] getAcceptableTokens() {
91          return getRequiredTokens();
92      }
93  
94      @Override
95      public void visitToken(DetailAST ast) {
96          switch (ast.getType()) {
97              case TokenTypes.CLASS_DEF:
98                  visitClassDef(ast);
99                  break;
100             case TokenTypes.VARIABLE_DEF:
101                 visitVariableDef(ast);
102                 break;
103             default:
104                 throw new IllegalStateException(ast.toString());
105         }
106     }
107 
108     @Override
109     public void leaveToken(DetailAST ast) {
110         if (ast.getType() == TokenTypes.CLASS_DEF) {
111             leaveClassDef();
112         }
113     }
114 
115     /**
116      * Called when we start processing class definition.
117      * @param ast class definition node
118      */
119     private void visitClassDef(DetailAST ast) {
120         checkingStack.push(checking);
121         checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast);
122     }
123 
124     /** Called when we leave class definition. */
125     private void leaveClassDef() {
126         checking = checkingStack.pop();
127     }
128 
129     /**
130      * Checks variable definition.
131      * @param ast variable def node for check
132      */
133     private void visitVariableDef(DetailAST ast) {
134         if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) {
135             final DetailAST modifiersAST =
136                 ast.findFirstToken(TokenTypes.MODIFIERS);
137 
138             if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) {
139                 log(ast, MSG_KEY, ast.findFirstToken(TokenTypes.IDENT).getText());
140             }
141         }
142     }
143 
144     /**
145      * Checks that a class name conforms to specified format.
146      * @param ast class definition node
147      * @return true if a class name conforms to specified format
148      */
149     private boolean isNamedAsException(DetailAST ast) {
150         final String className = ast.findFirstToken(TokenTypes.IDENT).getText();
151         return format.matcher(className).find();
152     }
153 
154     /**
155      * Checks that if extended class name conforms to specified format.
156      * @param ast class definition node
157      * @return true if extended class name conforms to specified format
158      */
159     private boolean isExtendedClassNamedAsException(DetailAST ast) {
160         boolean result = false;
161         final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
162         if (extendsClause != null) {
163             DetailAST currentNode = extendsClause;
164             while (currentNode.getLastChild() != null) {
165                 currentNode = currentNode.getLastChild();
166             }
167             final String extendedClassName = currentNode.getText();
168             result = extendedClassNameFormat.matcher(extendedClassName).matches();
169         }
170         return result;
171     }
172 
173 }