001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 getRequiredTokens();
082    }
083
084    @Override
085    public int[] getRequiredTokens() {
086        return new int[] {
087            TokenTypes.LITERAL_THROWS,
088        };
089    }
090
091    @Override
092    public int[] getAcceptableTokens() {
093        return getRequiredTokens();
094    }
095
096    /**
097     * Sets whether private methods must be ignored.
098     * @param ignorePrivateMethods whether private methods must be ignored.
099     */
100    public void setIgnorePrivateMethods(boolean ignorePrivateMethods) {
101        this.ignorePrivateMethods = ignorePrivateMethods;
102    }
103
104    /**
105     * Setter for max property.
106     * @param max maximum allowed throws statements.
107     */
108    public void setMax(int max) {
109        this.max = max;
110    }
111
112    @Override
113    public void visitToken(DetailAST ast) {
114        if (ast.getType() == TokenTypes.LITERAL_THROWS) {
115            visitLiteralThrows(ast);
116        }
117        else {
118            throw new IllegalStateException(ast.toString());
119        }
120    }
121
122    /**
123     * Checks number of throws statements.
124     * @param ast throws for check.
125     */
126    private void visitLiteralThrows(DetailAST ast) {
127        if ((!ignorePrivateMethods || !isInPrivateMethod(ast))
128                && !isOverriding(ast)) {
129            // Account for all the commas!
130            final int count = (ast.getChildCount() + 1) / 2;
131            if (count > max) {
132                log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY,
133                    count, max);
134            }
135        }
136    }
137
138    /**
139     * Check if a method has annotation @Override.
140     * @param ast throws, which is being checked.
141     * @return true, if a method has annotation @Override.
142     */
143    private static boolean isOverriding(DetailAST ast) {
144        final DetailAST modifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
145        boolean isOverriding = false;
146        if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) {
147            DetailAST child = modifiers.getFirstChild();
148            while (child != null) {
149                if (child.getType() == TokenTypes.ANNOTATION
150                        && "Override".equals(getAnnotationName(child))) {
151                    isOverriding = true;
152                    break;
153                }
154                child = child.getNextSibling();
155            }
156        }
157        return isOverriding;
158    }
159
160    /**
161     * Gets name of an annotation.
162     * @param annotation to get name of.
163     * @return name of an annotation.
164     */
165    private static String getAnnotationName(DetailAST annotation) {
166        final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
167        final String name;
168        if (dotAst == null) {
169            name = annotation.findFirstToken(TokenTypes.IDENT).getText();
170        }
171        else {
172            name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
173        }
174        return name;
175    }
176
177    /**
178     * Checks if method, which throws an exception is private.
179     * @param ast throws, which is being checked.
180     * @return true, if method, which throws an exception is private.
181     */
182    private static boolean isInPrivateMethod(DetailAST ast) {
183        final DetailAST methodModifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
184        return methodModifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
185    }
186
187}