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