001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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> Ensures that exceptions (classes with names conforming to some regular
033 * expression and explicitly extending classes with names conforming to other
034 * regular expression) are immutable. That is, they have only final fields.</p>
035 * <p> Rationale: Exception instances should represent an error
036 * condition. Having non final fields not only allows the state to be
037 * modified by accident and therefore mask the original condition but
038 * also allows developers to accidentally forget to initialise state
039 * thereby leading to code catching the exception to draw incorrect
040 * conclusions based on the state.</p>
041 *
042 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
043 */
044@FileStatefulCheck
045public final class MutableExceptionCheck extends AbstractCheck {
046
047    /**
048     * A key is pointing to the warning message text in "messages.properties"
049     * file.
050     */
051    public static final String MSG_KEY = "mutable.exception";
052
053    /** Default value for format and extendedClassNameFormat properties. */
054    private static final String DEFAULT_FORMAT = "^.*Exception$|^.*Error$|^.*Throwable$";
055    /** Stack of checking information for classes. */
056    private final Deque<Boolean> checkingStack = new ArrayDeque<>();
057    /** Pattern for class name that is being extended. */
058    private Pattern extendedClassNameFormat = Pattern.compile(DEFAULT_FORMAT);
059    /** Should we check current class or not. */
060    private boolean checking;
061    /** The regexp to match against. */
062    private Pattern format = Pattern.compile(DEFAULT_FORMAT);
063
064    /**
065     * Sets the format of extended class name to the specified regular expression.
066     * @param extendedClassNameFormat a {@code String} value
067     */
068    public void setExtendedClassNameFormat(Pattern extendedClassNameFormat) {
069        this.extendedClassNameFormat = extendedClassNameFormat;
070    }
071
072    /**
073     * Set the format for the specified regular expression.
074     * @param pattern the new pattern
075     */
076    public void setFormat(Pattern pattern) {
077        format = pattern;
078    }
079
080    @Override
081    public int[] getDefaultTokens() {
082        return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF};
083    }
084
085    @Override
086    public int[] getRequiredTokens() {
087        return getDefaultTokens();
088    }
089
090    @Override
091    public int[] getAcceptableTokens() {
092        return new int[] {TokenTypes.CLASS_DEF, TokenTypes.VARIABLE_DEF};
093    }
094
095    @Override
096    public void visitToken(DetailAST ast) {
097        switch (ast.getType()) {
098            case TokenTypes.CLASS_DEF:
099                visitClassDef(ast);
100                break;
101            case TokenTypes.VARIABLE_DEF:
102                visitVariableDef(ast);
103                break;
104            default:
105                throw new IllegalStateException(ast.toString());
106        }
107    }
108
109    @Override
110    public void leaveToken(DetailAST ast) {
111        if (ast.getType() == TokenTypes.CLASS_DEF) {
112            leaveClassDef();
113        }
114    }
115
116    /**
117     * Called when we start processing class definition.
118     * @param ast class definition node
119     */
120    private void visitClassDef(DetailAST ast) {
121        checkingStack.push(checking);
122        checking = isNamedAsException(ast) && isExtendedClassNamedAsException(ast);
123    }
124
125    /** Called when we leave class definition. */
126    private void leaveClassDef() {
127        checking = checkingStack.pop();
128    }
129
130    /**
131     * Checks variable definition.
132     * @param ast variable def node for check
133     */
134    private void visitVariableDef(DetailAST ast) {
135        if (checking && ast.getParent().getType() == TokenTypes.OBJBLOCK) {
136            final DetailAST modifiersAST =
137                ast.findFirstToken(TokenTypes.MODIFIERS);
138
139            if (modifiersAST.findFirstToken(TokenTypes.FINAL) == null) {
140                log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY,
141                        ast.findFirstToken(TokenTypes.IDENT).getText());
142            }
143        }
144    }
145
146    /**
147     * Checks that a class name conforms to specified format.
148     * @param ast class definition node
149     * @return true if a class name conforms to specified format
150     */
151    private boolean isNamedAsException(DetailAST ast) {
152        final String className = ast.findFirstToken(TokenTypes.IDENT).getText();
153        return format.matcher(className).find();
154    }
155
156    /**
157     * Checks that if extended class name conforms to specified format.
158     * @param ast class definition node
159     * @return true if extended class name conforms to specified format
160     */
161    private boolean isExtendedClassNamedAsException(DetailAST ast) {
162        boolean result = false;
163        final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
164        if (extendsClause != null) {
165            DetailAST currentNode = extendsClause;
166            while (currentNode.getLastChild() != null) {
167                currentNode = currentNode.getLastChild();
168            }
169            final String extendedClassName = currentNode.getText();
170            result = extendedClassNameFormat.matcher(extendedClassName).matches();
171        }
172        return result;
173    }
174}