1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.design;
21
22 import java.util.Objects;
23
24 import com.puppycrawl.tools.checkstyle.StatelessCheck;
25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26 import com.puppycrawl.tools.checkstyle.api.DetailAST;
27 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28
29 /**
30 * <div>
31 * Restricts throws statements to a specified count.
32 * Methods with "Override" or "java.lang.Override" annotation are skipped
33 * from validation as current class cannot change signature of these methods.
34 * </div>
35 *
36 * <p>
37 * Rationale:
38 * Exceptions form part of a method's interface. Declaring
39 * a method to throw too many differently rooted
40 * exceptions makes exception handling onerous and leads
41 * to poor programming practices such as writing code like
42 * {@code catch(Exception ex)}. 4 is the empirical value which is based
43 * on reports that we had for the ThrowsCountCheck over big projects
44 * such as OpenJDK. This check also forces developers to put exceptions
45 * into a hierarchy such that in the simplest
46 * case, only one type of exception need be checked for by
47 * a caller but any subclasses can be caught
48 * specifically if necessary. For more information on rules
49 * for the exceptions and their issues, see Effective Java:
50 * Programming Language Guide Second Edition
51 * by Joshua Bloch pages 264-273.
52 * </p>
53 *
54 * <p>
55 * <b>ignorePrivateMethods</b> - allows to skip private methods as they do
56 * not cause problems for other classes.
57 * </p>
58 *
59 * @since 3.2
60 */
61 @StatelessCheck
62 public final class ThrowsCountCheck extends AbstractCheck {
63
64 /**
65 * A key is pointing to the warning message text in "messages.properties"
66 * file.
67 */
68 public static final String MSG_KEY = "throws.count";
69
70 /** Default value of max property. */
71 private static final int DEFAULT_MAX = 4;
72
73 /** Allow private methods to be ignored. */
74 private boolean ignorePrivateMethods = true;
75
76 /** Specify maximum allowed number of throws statements. */
77 private int max;
78
79 /** Creates new instance of the check. */
80 public ThrowsCountCheck() {
81 max = DEFAULT_MAX;
82 }
83
84 @Override
85 public int[] getDefaultTokens() {
86 return getRequiredTokens();
87 }
88
89 @Override
90 public int[] getRequiredTokens() {
91 return new int[] {
92 TokenTypes.LITERAL_THROWS,
93 };
94 }
95
96 @Override
97 public int[] getAcceptableTokens() {
98 return getRequiredTokens();
99 }
100
101 /**
102 * Setter to allow private methods to be ignored.
103 *
104 * @param ignorePrivateMethods whether private methods must be ignored.
105 * @since 6.7
106 */
107 public void setIgnorePrivateMethods(boolean ignorePrivateMethods) {
108 this.ignorePrivateMethods = ignorePrivateMethods;
109 }
110
111 /**
112 * Setter to specify maximum allowed number of throws statements.
113 *
114 * @param max maximum allowed throws statements.
115 * @since 3.2
116 */
117 public void setMax(int max) {
118 this.max = max;
119 }
120
121 @Override
122 public void visitToken(DetailAST ast) {
123 if (ast.getType() == TokenTypes.LITERAL_THROWS) {
124 visitLiteralThrows(ast);
125 }
126 else {
127 throw new IllegalStateException(ast.toString());
128 }
129 }
130
131 /**
132 * Checks number of throws statements.
133 *
134 * @param ast throws for check.
135 */
136 private void visitLiteralThrows(DetailAST ast) {
137 if ((!ignorePrivateMethods || !isInPrivateMethod(ast))
138 && !isOverriding(ast)) {
139 // Account for all the commas!
140 final int count = (ast.getChildCount() + 1) / 2;
141 if (count > max) {
142 log(ast, MSG_KEY, count, max);
143 }
144 }
145 }
146
147 /**
148 * Check if a method has annotation @Override.
149 *
150 * @param ast throws, which is being checked.
151 * @return true, if a method has annotation @Override.
152 */
153 private static boolean isOverriding(DetailAST ast) {
154 final DetailAST modifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
155 boolean isOverriding = false;
156 DetailAST child = modifiers.getFirstChild();
157 while (child != null) {
158 if (child.getType() == TokenTypes.ANNOTATION
159 && "Override".equals(getAnnotationName(child))) {
160 isOverriding = true;
161 break;
162 }
163 child = child.getNextSibling();
164 }
165 return isOverriding;
166 }
167
168 /**
169 * Gets name of an annotation.
170 *
171 * @param annotation to get name of.
172 * @return name of an annotation.
173 */
174 private static String getAnnotationName(DetailAST annotation) {
175 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
176 final DetailAST parent = Objects.requireNonNullElse(dotAst, annotation);
177 return parent.findFirstToken(TokenTypes.IDENT).getText();
178 }
179
180 /**
181 * Checks if method, which throws an exception is private.
182 *
183 * @param ast throws, which is being checked.
184 * @return true, if method, which throws an exception is private.
185 */
186 private static boolean isInPrivateMethod(DetailAST ast) {
187 final DetailAST methodModifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS);
188 return methodModifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
189 }
190
191 }