View Javadoc
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 }