001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 extendedClassNameFormat} - Specify pattern for extended 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 format} - Specify pattern for exception 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 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
065 * </p>
066 * <p>
067 * Violation Message Keys:
068 * </p>
069 * <ul>
070 * <li>
071 * {@code mutable.exception}
072 * </li>
073 * </ul>
074 *
075 * @since 3.2
076 */
077@FileStatefulCheck
078public final class MutableExceptionCheck extends AbstractCheck {
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_KEY = "mutable.exception";
085
086    /** Default value for format and extendedClassNameFormat properties. */
087    private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$";
088    /** Stack of checking information for classes. */
089    private final Deque<Boolean> checkingStack = new ArrayDeque<>();
090    /** Specify pattern for extended class names. */
091    private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT);
092    /** Should we check current class or not. */
093    private boolean checking;
094    /** Specify pattern for exception class names. */
095    private Pattern format = extendedClassNameFormat;
096
097    /**
098     * Setter to specify pattern for extended class names.
099     *
100     * @param extendedClassNameFormat a {@code String} value
101     * @since 6.2
102     */
103    public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) {
104        this.extendedClassNameFormat = extendedClassNameFormat;
105    }
106
107    /**
108     * Setter to specify pattern for exception class names.
109     *
110     * @param pattern the new pattern
111     * @since 3.2
112     */
113    public void setFormat(Pattern pattern) {
114        format = pattern;
115    }
116
117    @Override
118    public int[] getDefaultTokens() {
119        return getRequiredTokens();
120    }
121
122    @Override
123    public int[] getRequiredTokens() {
124        return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF};
125    }
126
127    @Override
128    public int[] getAcceptableTokens() {
129        return getRequiredTokens();
130    }
131
132    @Override
133    public void visitToken(DetailAST ast) {
134        switch (ast.getType()) {
135            case TokenTypes.CLASS_DEF:
136                visitClassDef(ast);
137                break;
138            case TokenTypes.VARIABLE_DEF:
139                visitVariableDef(ast);
140                break;
141            default:
142                throw new IllegalStateException(ast.toString());
143        }
144    }
145
146    @Override
147    public void leaveToken(DetailAST ast) {
148        if (ast.getType() == TokenTypes.CLASS_DEF) {
149            leaveClassDef();
150        }
151    }
152
153    /**
154     * Called when we start processing class definition.
155     *
156     * @param ast class definition node
157     */
158    private void visitClassDef(DetailAST ast) {
159        checkingStack.push(checking);
160        checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast);
161    }
162
163    /** Called when we leave class definition. */
164    private void leaveClassDef() {
165        checking = checkingStack.pop();
166    }
167
168    /**
169     * Checks variable definition.
170     *
171     * @param ast variable def node for check
172     */
173    private void visitVariableDef(DetailAST ast) {
174        if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) {
175            final DetailAST modifiersAST =
176                ast.findFirstToken(TokenTypes.MODIFIERS);
177
178            if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) {
179                log(ast, MSG_KEY, ast.findFirstToken(TokenTypes.IDENT).getText());
180            }
181        }
182    }
183
184    /**
185     * Checks that a class name conforms to specified format.
186     *
187     * @param ast class definition node
188     * @return true if a class name conforms to specified format
189     */
190    private boolean isNamedAsException(DetailAST ast) {
191        final String className = ast.findFirstToken(TokenTypes.IDENT).getText();
192        return format.matcher(className).find();
193    }
194
195    /**
196     * Checks that if extended class name conforms to specified format.
197     *
198     * @param ast class definition node
199     * @return true if extended class name conforms to specified format
200     */
201    private boolean isExtendedClassNamedAsException(DetailAST ast) {
202        boolean result = false;
203        final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
204        if (extendsClause != null) {
205            DetailAST currentNode = extendsClause;
206            while (currentNode.getLastChild() != null) {
207                currentNode = currentNode.getLastChild();
208            }
209            final String extendedClassName = currentNode.getText();
210            result = extendedClassNameFormat.matcher(extendedClassName).matches();
211        }
212        return result;
213    }
214
215}