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.api.AbstractCheck;
023import com.puppycrawl.tools.checkstyle.api.DetailAST;
024import com.puppycrawl.tools.checkstyle.api.TokenTypes;
025
026/**
027 * <p>
028 * Restricts throws statements to a specified count (default = 4).
029 * Methods with "Override" or "java.lang.Override" annotation are skipped
030 * from validation as current class cannot change signature of these methods.
031 * </p>
032 * <p>
033 * Rationale:
034 * Exceptions form part of a methods interface. Declaring
035 * a method to throw too many differently rooted
036 * exceptions makes exception handling onerous and leads
037 * to poor programming practices such as catch
038 * (Exception). 4 is the empirical value which is based
039 * on reports that we had for the ThrowsCountCheck over big projects
040 * such as OpenJDK. This check also forces developers to put exceptions
041 * into a hierarchy such that in the simplest
042 * case, only one type of exception need be checked for by
043 * a caller but allows any sub-classes to be caught
044 * specifically if necessary. For more information on rules
045 * for the exceptions and their issues, see Effective Java:
046 * Programming Language Guide Second Edition
047 * by Joshua Bloch pages 264-273.
048 * </p>
049 * <p>
050 * <b>ignorePrivateMethods</b> - allows to skip private methods as they do
051 * not cause problems for other classes.
052 * </p>
053 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
054 */
055public final class ThrowsCountCheck extends AbstractCheck {
056
057    /**
058     * A key is pointing to the warning message text in "messages.properties"
059     * file.
060     */
061    public static final String MSG_KEY = "throws.count";
062
063    /** Default value of max property. */
064    private static final int DEFAULT_MAX = 4;
065
066    /** Whether private methods must be ignored. **/
067    private boolean ignorePrivateMethods = true;
068
069    /** Maximum allowed throws statements. */
070    private int max;
071
072    /** Creates new instance of the check. */
073    public ThrowsCountCheck() {
074        max = DEFAULT_MAX;
075    }
076
077    @Override
078    public int[] getDefaultTokens() {
079        return new int[] {
080            TokenTypes.LITERAL_THROWS,
081        };
082    }
083
084    @Override
085    public int[] getRequiredTokens() {
086        return getDefaultTokens();
087    }
088
089    @Override
090    public int[] getAcceptableTokens() {
091        return new int[] {
092            TokenTypes.LITERAL_THROWS,
093        };
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.branchContains(TokenTypes.ANNOTATION)) {
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}