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 com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026
027/**
028 * <p>
029 * Restricts throws statements to a specified count (default = 4).
030 * Methods with "Override" or "java.lang.Override" annotation are skipped
031 * from validation as current class cannot change signature of these methods.
032 * </p>
033 * <p>
034 * Rationale:
035 * Exceptions form part of a methods interface. Declaring
036 * a method to throw too many differently rooted
037 * exceptions makes exception handling onerous and leads
038 * to poor programming practices such as catch
039 * (Exception). 4 is the empirical value which is based
040 * on reports that we had for the ThrowsCountCheck over big projects
041 * such as OpenJDK. This check also forces developers to put exceptions
042 * into a hierarchy such that in the simplest
043 * case, only one type of exception need be checked for by
044 * a caller but allows any sub-classes to be caught
045 * specifically if necessary. For more information on rules
046 * for the exceptions and their issues, see Effective Java:
047 * Programming Language Guide Second Edition
048 * by Joshua Bloch pages 264-273.
049 * </p>
050 * <p>
051 * <b>ignorePrivateMethods</b> - allows to skip private methods as they do
052 * not cause problems for other classes.
053 * </p>
054 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
055 */
056@StatelessCheck
057public final class ThrowsCountCheck extends AbstractCheck {
058
059    /**
060     * A key is pointing to the warning message text in "messages.properties"
061     * file.
062     */
063    public static final String MSG_KEY = "throws.count";
064
065    /** Default value of max property. */
066    private static final int DEFAULT_MAX = 4;
067
068    /** Whether private methods must be ignored. **/
069    private boolean ignorePrivateMethods = true;
070
071    /** Maximum allowed throws statements. */
072    private int max;
073
074    /** Creates new instance of the check. */
075    public ThrowsCountCheck() {
076        max = DEFAULT_MAX;
077    }
078
079    @Override
080    public int[] getDefaultTokens() {
081        return new int[] {
082            TokenTypes.LITERAL_THROWS,
083        };
084    }
085
086    @Override
087    public int[] getRequiredTokens() {
088        return getDefaultTokens();
089    }
090
091    @Override
092    public int[] getAcceptableTokens() {
093        return new int[] {
094            TokenTypes.LITERAL_THROWS,
095        };
096    }
097
098    /**
099     * Sets whether private methods must be ignored.
100     * @param ignorePrivateMethods whether private methods must be ignored.
101     */
102    public void setIgnorePrivateMethods(boolean ignorePrivateMethods) {
103        this.ignorePrivateMethods = ignorePrivateMethods;
104    }
105
106    /**
107     * Setter for max property.
108     * @param max maximum allowed throws statements.
109     */
110    public void setMax(int max) {
111        this.max = max;
112    }
113
114    @Override
115    public void visitToken(DetailAST ast) {
116        if (ast.getType() == TokenTypes.LITERAL_THROWS) {
117            visitLiteralThrows(ast);
118        }
119        else {
120            throw new IllegalStateException(ast.toString());
121        }
122    }
123
124    /**
125     * Checks number of throws statements.
126     * @param ast throws for check.
127     */
128    private void visitLiteralThrows(DetailAST ast) {
129        if ((!ignorePrivateMethods || !isInPrivateMethod(ast))
130                && !isOverriding(ast)) {
131            // Account for all the commas!
132            final int count = (ast.getChildCount() + 1) / 2;
133            if (count > max) {
134                log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY,
135                    count, max);
136            }
137        }
138    }
139
140    /**
141     * Check if a method has annotation @Override.
142     * @param ast throws, which is being checked.
143     * @return true, if a method has annotation @Override.
144     */
145    private static boolean isOverriding(DetailAST ast) {
146        final DetailAST modifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
147        boolean isOverriding = false;
148        if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) {
149            DetailAST child = modifiers.getFirstChild();
150            while (child != null) {
151                if (child.getType() == TokenTypes.ANNOTATION
152                        && "Override".equals(getAnnotationName(child))) {
153                    isOverriding = true;
154                    break;
155                }
156                child = child.getNextSibling();
157            }
158        }
159        return isOverriding;
160    }
161
162    /**
163     * Gets name of an annotation.
164     * @param annotation to get name of.
165     * @return name of an annotation.
166     */
167    private static String getAnnotationName(DetailAST annotation) {
168        final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
169        final String name;
170        if (dotAst == null) {
171            name = annotation.findFirstToken(TokenTypes.IDENT).getText();
172        }
173        else {
174            name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
175        }
176        return name;
177    }
178
179    /**
180     * Checks if method, which throws an exception is private.
181     * @param ast throws, which is being checked.
182     * @return true, if method, which throws an exception is private.
183     */
184    private static boolean isInPrivateMethod(DetailAST ast) {
185        final DetailAST methodModifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
186        return methodModifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
187    }
188}