001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.metrics; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024import java.util.Set; 025 026import com.google.common.collect.ImmutableSet; 027import com.google.common.collect.Sets; 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.FullIdent; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CheckUtils; 033 034/** 035 * Base class for coupling calculation. 036 * 037 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a> 038 * @author o_sukhodolsky 039 */ 040public abstract class AbstractClassCouplingCheck extends AbstractCheck { 041 /** Class names to ignore. */ 042 private static final Set<String> DEFAULT_EXCLUDED_CLASSES = 043 ImmutableSet.<String>builder() 044 // primitives 045 .add("boolean", "byte", "char", "double", "float", "int") 046 .add("long", "short", "void") 047 // wrappers 048 .add("Boolean", "Byte", "Character", "Double", "Float") 049 .add("Integer", "Long", "Short", "Void") 050 // java.lang.* 051 .add("Object", "Class") 052 .add("String", "StringBuffer", "StringBuilder") 053 // Exceptions 054 .add("ArrayIndexOutOfBoundsException", "Exception") 055 .add("RuntimeException", "IllegalArgumentException") 056 .add("IllegalStateException", "IndexOutOfBoundsException") 057 .add("NullPointerException", "Throwable", "SecurityException") 058 .add("UnsupportedOperationException") 059 // java.util.* 060 .add("List", "ArrayList", "Deque", "Queue", "LinkedList") 061 .add("Set", "HashSet", "SortedSet", "TreeSet") 062 .add("Map", "HashMap", "SortedMap", "TreeMap") 063 .build(); 064 065 /** Stack of contexts. */ 066 private final Deque<Context> contextStack = new ArrayDeque<>(); 067 068 /** User-configured class names to ignore. */ 069 private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES; 070 /** Allowed complexity. */ 071 private int max; 072 /** Package of the file we check. */ 073 private String packageName; 074 075 /** Current context. */ 076 private Context context = new Context("", 0, 0); 077 078 /** 079 * Creates new instance of the check. 080 * @param defaultMax default value for allowed complexity. 081 */ 082 protected AbstractClassCouplingCheck(int defaultMax) { 083 max = defaultMax; 084 } 085 086 /** 087 * @return message key we use for log violations. 088 */ 089 protected abstract String getLogMessageId(); 090 091 @Override 092 public final int[] getDefaultTokens() { 093 return getRequiredTokens(); 094 } 095 096 /** 097 * @return allowed complexity. 098 */ 099 public final int getMax() { 100 return max; 101 } 102 103 /** 104 * Sets maximum allowed complexity. 105 * @param max allowed complexity. 106 */ 107 public final void setMax(int max) { 108 this.max = max; 109 } 110 111 /** 112 * Sets user-excluded classes to ignore. 113 * @param excludedClasses the list of classes to ignore. 114 */ 115 public final void setExcludedClasses(String... excludedClasses) { 116 this.excludedClasses = ImmutableSet.copyOf(excludedClasses); 117 } 118 119 @Override 120 public final void beginTree(DetailAST ast) { 121 packageName = ""; 122 } 123 124 @Override 125 public void visitToken(DetailAST ast) { 126 switch (ast.getType()) { 127 case TokenTypes.PACKAGE_DEF: 128 visitPackageDef(ast); 129 break; 130 case TokenTypes.CLASS_DEF: 131 case TokenTypes.INTERFACE_DEF: 132 case TokenTypes.ANNOTATION_DEF: 133 case TokenTypes.ENUM_DEF: 134 visitClassDef(ast); 135 break; 136 case TokenTypes.TYPE: 137 context.visitType(ast); 138 break; 139 case TokenTypes.LITERAL_NEW: 140 context.visitLiteralNew(ast); 141 break; 142 case TokenTypes.LITERAL_THROWS: 143 context.visitLiteralThrows(ast); 144 break; 145 default: 146 throw new IllegalArgumentException("Unknown type: " + ast); 147 } 148 } 149 150 @Override 151 public void leaveToken(DetailAST ast) { 152 switch (ast.getType()) { 153 case TokenTypes.CLASS_DEF: 154 case TokenTypes.INTERFACE_DEF: 155 case TokenTypes.ANNOTATION_DEF: 156 case TokenTypes.ENUM_DEF: 157 leaveClassDef(); 158 break; 159 default: 160 // Do nothing 161 } 162 } 163 164 /** 165 * Stores package of current class we check. 166 * @param pkg package definition. 167 */ 168 private void visitPackageDef(DetailAST pkg) { 169 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild() 170 .getPreviousSibling()); 171 packageName = ident.getText(); 172 } 173 174 /** 175 * Creates new context for a given class. 176 * @param classDef class definition node. 177 */ 178 private void visitClassDef(DetailAST classDef) { 179 contextStack.push(context); 180 final String className = 181 classDef.findFirstToken(TokenTypes.IDENT).getText(); 182 context = new Context(className, 183 classDef.getLineNo(), 184 classDef.getColumnNo()); 185 } 186 187 /** Restores previous context. */ 188 private void leaveClassDef() { 189 context.checkCoupling(); 190 context = contextStack.pop(); 191 } 192 193 /** 194 * Encapsulates information about class coupling. 195 * 196 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a> 197 * @author o_sukhodolsky 198 */ 199 private class Context { 200 /** 201 * Set of referenced classes. 202 * Sorted by name for predictable error messages in unit tests. 203 */ 204 private final Set<String> referencedClassNames = Sets.newTreeSet(); 205 /** Own class name. */ 206 private final String className; 207 /* Location of own class. (Used to log violations) */ 208 /** Line number of class definition. */ 209 private final int lineNo; 210 /** Column number of class definition. */ 211 private final int columnNo; 212 213 /** 214 * Create new context associated with given class. 215 * @param className name of the given class. 216 * @param lineNo line of class definition. 217 * @param columnNo column of class definition. 218 */ 219 Context(String className, int lineNo, int columnNo) { 220 this.className = className; 221 this.lineNo = lineNo; 222 this.columnNo = columnNo; 223 } 224 225 /** 226 * Visits throws clause and collects all exceptions we throw. 227 * @param literalThrows throws to process. 228 */ 229 public void visitLiteralThrows(DetailAST literalThrows) { 230 for (DetailAST childAST = literalThrows.getFirstChild(); 231 childAST != null; 232 childAST = childAST.getNextSibling()) { 233 if (childAST.getType() != TokenTypes.COMMA) { 234 addReferencedClassName(childAST); 235 } 236 } 237 } 238 239 /** 240 * Visits type. 241 * @param ast type to process. 242 */ 243 public void visitType(DetailAST ast) { 244 final String fullTypeName = CheckUtils.createFullType(ast).getText(); 245 context.addReferencedClassName(fullTypeName); 246 } 247 248 /** 249 * Visits NEW. 250 * @param ast NEW to process. 251 */ 252 public void visitLiteralNew(DetailAST ast) { 253 context.addReferencedClassName(ast.getFirstChild()); 254 } 255 256 /** 257 * Adds new referenced class. 258 * @param ast a node which represents referenced class. 259 */ 260 private void addReferencedClassName(DetailAST ast) { 261 final String fullIdentName = FullIdent.createFullIdent(ast).getText(); 262 addReferencedClassName(fullIdentName); 263 } 264 265 /** 266 * Adds new referenced class. 267 * @param referencedClassName class name of the referenced class. 268 */ 269 private void addReferencedClassName(String referencedClassName) { 270 if (isSignificant(referencedClassName)) { 271 referencedClassNames.add(referencedClassName); 272 } 273 } 274 275 /** Checks if coupling less than allowed or not. */ 276 public void checkCoupling() { 277 referencedClassNames.remove(className); 278 referencedClassNames.remove(packageName + "." + className); 279 280 if (referencedClassNames.size() > max) { 281 log(lineNo, columnNo, getLogMessageId(), 282 referencedClassNames.size(), getMax(), 283 referencedClassNames.toString()); 284 } 285 } 286 287 /** 288 * Checks if given class shouldn't be ignored and not from java.lang. 289 * @param candidateClassName class to check. 290 * @return true if we should count this class. 291 */ 292 private boolean isSignificant(String candidateClassName) { 293 return !excludedClasses.contains(candidateClassName) 294 && !candidateClassName.startsWith("java.lang."); 295 } 296 } 297}