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