001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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 (default = 4). 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 methods 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 catch 039 * (Exception). 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 allows any sub-classes to 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 */ 055@StatelessCheck 056public final class ThrowsCountCheck extends AbstractCheck { 057 058 /** 059 * A key is pointing to the warning message text in "messages.properties" 060 * file. 061 */ 062 public static final String MSG_KEY = "throws.count"; 063 064 /** Default value of max property. */ 065 private static final int DEFAULT_MAX = 4; 066 067 /** Whether private methods must be ignored. **/ 068 private boolean ignorePrivateMethods = true; 069 070 /** Maximum allowed throws statements. */ 071 private int max; 072 073 /** Creates new instance of the check. */ 074 public ThrowsCountCheck() { 075 max = DEFAULT_MAX; 076 } 077 078 @Override 079 public int[] getDefaultTokens() { 080 return getRequiredTokens(); 081 } 082 083 @Override 084 public int[] getRequiredTokens() { 085 return new int[] { 086 TokenTypes.LITERAL_THROWS, 087 }; 088 } 089 090 @Override 091 public int[] getAcceptableTokens() { 092 return getRequiredTokens(); 093 } 094 095 /** 096 * Sets whether private methods must be ignored. 097 * @param ignorePrivateMethods whether private methods must be ignored. 098 */ 099 public void setIgnorePrivateMethods(boolean ignorePrivateMethods) { 100 this.ignorePrivateMethods = ignorePrivateMethods; 101 } 102 103 /** 104 * Setter for max property. 105 * @param max maximum allowed throws statements. 106 */ 107 public void setMax(int max) { 108 this.max = max; 109 } 110 111 @Override 112 public void visitToken(DetailAST ast) { 113 if (ast.getType() == TokenTypes.LITERAL_THROWS) { 114 visitLiteralThrows(ast); 115 } 116 else { 117 throw new IllegalStateException(ast.toString()); 118 } 119 } 120 121 /** 122 * Checks number of throws statements. 123 * @param ast throws for check. 124 */ 125 private void visitLiteralThrows(DetailAST ast) { 126 if ((!ignorePrivateMethods || !isInPrivateMethod(ast)) 127 && !isOverriding(ast)) { 128 // Account for all the commas! 129 final int count = (ast.getChildCount() + 1) / 2; 130 if (count > max) { 131 log(ast, MSG_KEY, count, max); 132 } 133 } 134 } 135 136 /** 137 * Check if a method has annotation @Override. 138 * @param ast throws, which is being checked. 139 * @return true, if a method has annotation @Override. 140 */ 141 private static boolean isOverriding(DetailAST ast) { 142 final DetailAST modifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS); 143 boolean isOverriding = false; 144 if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) { 145 DetailAST child = modifiers.getFirstChild(); 146 while (child != null) { 147 if (child.getType() == TokenTypes.ANNOTATION 148 && "Override".equals(getAnnotationName(child))) { 149 isOverriding = true; 150 break; 151 } 152 child = child.getNextSibling(); 153 } 154 } 155 return isOverriding; 156 } 157 158 /** 159 * Gets name of an annotation. 160 * @param annotation to get name of. 161 * @return name of an annotation. 162 */ 163 private static String getAnnotationName(DetailAST annotation) { 164 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 165 final String name; 166 if (dotAst == null) { 167 name = annotation.findFirstToken(TokenTypes.IDENT).getText(); 168 } 169 else { 170 name = dotAst.findFirstToken(TokenTypes.IDENT).getText(); 171 } 172 return name; 173 } 174 175 /** 176 * Checks if method, which throws an exception is private. 177 * @param ast throws, which is being checked. 178 * @return true, if method, which throws an exception is private. 179 */ 180 private static boolean isInPrivateMethod(DetailAST ast) { 181 final DetailAST methodModifiers = ast.getParent().findFirstToken(TokenTypes.MODIFIERS); 182 return methodModifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null; 183 } 184 185}