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