001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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. 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 method's 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 writing code like 039 * {@code catch(Exception ex)}. 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 any subclasses can 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 * <ul> 055 * <li> 056 * Property {@code max} - Specify maximum allowed number of throws statements. 057 * Default value is {@code 4}. 058 * </li> 059 * <li> 060 * Property {@code ignorePrivateMethods} - Allow private methods to be ignored. 061 * Default value is {@code true}. 062 * </li> 063 * </ul> 064 * <p> 065 * To configure check: 066 * </p> 067 * <pre> 068 * <module name="ThrowsCount"/> 069 * </pre> 070 * <p> 071 * Example: 072 * </p> 073 * <pre> 074 * class Test { 075 * public void myFunction() throws CloneNotSupportedException, 076 * ArrayIndexOutOfBoundsException, 077 * StringIndexOutOfBoundsException, 078 * IllegalStateException, 079 * NullPointerException { // violation, max allowed is 4 080 * // body 081 * } 082 * 083 * public void myFunc() throws ArithmeticException, 084 * NumberFormatException { // ok 085 * // body 086 * } 087 * 088 * private void privateFunc() throws CloneNotSupportedException, 089 * ClassNotFoundException, 090 * IllegalAccessException, 091 * ArithmeticException, 092 * ClassCastException { // ok, private methods are ignored 093 * // body 094 * } 095 * 096 * } 097 * </pre> 098 * <p> 099 * To configure the check so that it doesn't allow more than two throws per method: 100 * </p> 101 * <pre> 102 * <module name="ThrowsCount"> 103 * <property name="max" value="2"/> 104 * </module> 105 * </pre> 106 * <p> 107 * Example: 108 * </p> 109 * <pre> 110 * class Test { 111 * public void myFunction() throws IllegalStateException, 112 * ArrayIndexOutOfBoundsException, 113 * NullPointerException { // violation, max allowed is 2 114 * // body 115 * } 116 * 117 * public void myFunc() throws ArithmeticException, 118 * NumberFormatException { // ok 119 * // body 120 * } 121 * 122 * private void privateFunc() throws CloneNotSupportedException, 123 * ClassNotFoundException, 124 * IllegalAccessException, 125 * ArithmeticException, 126 * ClassCastException { // ok, private methods are ignored 127 * // body 128 * } 129 * 130 * } 131 * </pre> 132 * <p> 133 * To configure the check so that it doesn't skip private methods: 134 * </p> 135 * <pre> 136 * <module name="ThrowsCount"> 137 * <property name="ignorePrivateMethods" value="false"/> 138 * </module> 139 * </pre> 140 * <p> 141 * Example: 142 * </p> 143 * <pre> 144 * class Test { 145 * public void myFunction() throws CloneNotSupportedException, 146 * ArrayIndexOutOfBoundsException, 147 * StringIndexOutOfBoundsException, 148 * IllegalStateException, 149 * NullPointerException { // violation, max allowed is 4 150 * // body 151 * } 152 * 153 * public void myFunc() throws ArithmeticException, 154 * NumberFormatException { // ok 155 * // body 156 * } 157 * 158 * private void privateFunc() throws CloneNotSupportedException, 159 * ClassNotFoundException, 160 * IllegalAccessException, 161 * ArithmeticException, 162 * ClassCastException { // violation, max allowed is 4 163 * // body 164 * } 165 * 166 * private void func() throws IllegalStateException, 167 * NullPointerException { // ok 168 * // body 169 * } 170 * 171 * } 172 * </pre> 173 * 174 * @since 3.2 175 */ 176@StatelessCheck 177public final class ThrowsCountCheck extends AbstractCheck { 178 179 /** 180 * A key is pointing to the warning message text in "messages.properties" 181 * file. 182 */ 183 public static final String MSG_KEY = "throws.count"; 184 185 /** Default value of max property. */ 186 private static final int DEFAULT_MAX = 4; 187 188 /** Allow private methods to be ignored. */ 189 private boolean ignorePrivateMethods = true; 190 191 /** Specify maximum allowed number of throws statements. */ 192 private int max; 193 194 /** Creates new instance of the check. */ 195 public ThrowsCountCheck() { 196 max = DEFAULT_MAX; 197 } 198 199 @Override 200 public int[] getDefaultTokens() { 201 return getRequiredTokens(); 202 } 203 204 @Override 205 public int[] getRequiredTokens() { 206 return new int[] { 207 TokenTypes.LITERAL_THROWS, 208 }; 209 } 210 211 @Override 212 public int[] getAcceptableTokens() { 213 return getRequiredTokens(); 214 } 215 216 /** 217 * Setter to allow private methods to be ignored. 218 * 219 * @param ignorePrivateMethods whether private methods must be ignored. 220 */ 221 public void setIgnorePrivateMethods(boolean ignorePrivateMethods) { 222 this.ignorePrivateMethods = ignorePrivateMethods; 223 } 224 225 /** 226 * Setter to specify maximum allowed number of throws statements. 227 * 228 * @param max maximum allowed throws statements. 229 */ 230 public void setMax(int max) { 231 this.max = max; 232 } 233 234 @Override 235 public void visitToken(DetailAST ast) { 236 if (ast.getType() == TokenTypes.LITERAL_THROWS) { 237 visitLiteralThrows(ast); 238 } 239 else { 240 throw new IllegalStateException(ast.toString()); 241 } 242 } 243 244 /** 245 * Checks number of throws statements. 246 * 247 * @param ast throws for check. 248 */ 249 private void visitLiteralThrows(DetailAST ast) { 250 if ((!ignorePrivateMethods || !isInPrivateMethod(ast)) 251 && !isOverriding(ast)) { 252 // Account for all the commas! 253 final int count = (ast.getChildCount() + 1) / 2; 254 if (count > max) { 255 log(ast, MSG_KEY, count, max); 256 } 257 } 258 } 259 260 /** 261 * Check if a method has annotation @Override. 262 * 263 * @param ast throws, which is being checked. 264 * @return true, if a method has annotation @Override. 265 */ 266 private static boolean isOverriding(DetailAST ast) { 267 final DetailAST modifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS); 268 boolean isOverriding = false; 269 DetailAST child = modifiers.getFirstChild(); 270 while (child != null) { 271 if (child.getType() == TokenTypes.ANNOTATION 272 && "Override".equals(getAnnotationName(child))) { 273 isOverriding = true; 274 break; 275 } 276 child = child.getNextSibling(); 277 } 278 return isOverriding; 279 } 280 281 /** 282 * Gets name of an annotation. 283 * 284 * @param annotation to get name of. 285 * @return name of an annotation. 286 */ 287 private static String getAnnotationName(DetailAST annotation) { 288 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 289 final String name; 290 if (dotAst == null) { 291 name = annotation.findFirstToken(TokenTypes.IDENT).getText(); 292 } 293 else { 294 name = dotAst.findFirstToken(TokenTypes.IDENT).getText(); 295 } 296 return name; 297 } 298 299 /** 300 * Checks if method, which throws an exception is private. 301 * 302 * @param ast throws, which is being checked. 303 * @return true, if method, which throws an exception is private. 304 */ 305 private static boolean isInPrivateMethod(DetailAST ast) { 306 final DetailAST methodModifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS); 307 return methodModifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null; 308 } 309 310}