////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.metrics;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
/**
* Base class for coupling calculation.
*
* @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
* @author o_sukhodolsky
*/
public abstract class AbstractClassCouplingCheck extends AbstractCheck {
/** A package separator - "." */
private static final String DOT = ".";
/** Class names to ignore. */
private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
Arrays.stream(new String[] {
// primitives
"boolean", "byte", "char", "double", "float", "int",
"long", "short", "void",
// wrappers
"Boolean", "Byte", "Character", "Double", "Float",
"Integer", "Long", "Short", "Void",
// java.lang.*
"Object", "Class",
"String", "StringBuffer", "StringBuilder",
// Exceptions
"ArrayIndexOutOfBoundsException", "Exception",
"RuntimeException", "IllegalArgumentException",
"IllegalStateException", "IndexOutOfBoundsException",
"NullPointerException", "Throwable", "SecurityException",
"UnsupportedOperationException",
// java.util.*
"List", "ArrayList", "Deque", "Queue", "LinkedList",
"Set", "HashSet", "SortedSet", "TreeSet",
"Map", "HashMap", "SortedMap", "TreeMap",
}).collect(Collectors.toSet()));
/** Package names to ignore. */
private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
/** User-configured regular expressions to ignore classes. */
private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
/** User-configured class names to ignore. */
private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
/** User-configured package names to ignore. */
private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
/** Allowed complexity. */
private int max;
/** Current file context. */
private FileContext fileContext;
/**
* Creates new instance of the check.
* @param defaultMax default value for allowed complexity.
*/
protected AbstractClassCouplingCheck(int defaultMax) {
max = defaultMax;
excludeClassesRegexps.add(CommonUtils.createPattern("^$"));
}
/**
* @return message key we use for log violations.
*/
protected abstract String getLogMessageId();
@Override
public final int[] getDefaultTokens() {
return getRequiredTokens();
}
/**
* @return allowed complexity.
*/
public final int getMax() {
return max;
}
/**
* Sets maximum allowed complexity.
* @param max allowed complexity.
*/
public final void setMax(int max) {
this.max = max;
}
/**
* Sets user-excluded classes to ignore.
* @param excludedClasses the list of classes to ignore.
*/
public final void setExcludedClasses(String... excludedClasses) {
this.excludedClasses =
Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
}
/**
* Sets user-excluded regular expression of classes to ignore.
* @param from array representing regular expressions of classes to ignore.
*/
public void setExcludeClassesRegexps(String... from) {
excludeClassesRegexps.clear();
excludeClassesRegexps.addAll(Arrays.stream(from.clone())
.map(CommonUtils::createPattern)
.collect(Collectors.toSet()));
}
/**
* Sets user-excluded pakcages to ignore. All exlcuded packages should end with a period,
* so it also appends a dot to a package name.
* @param excludedPackages the list of packages to ignore.
*/
public final void setExcludedPackages(String... excludedPackages) {
final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
.filter(x -> !CommonUtils.isName(x))
.collect(Collectors.toList());
if (!invalidIdentifiers.isEmpty()) {
throw new IllegalArgumentException(
"the following values are not valid identifiers: "
+ invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
}
this.excludedPackages = Collections.unmodifiableSet(
Arrays.stream(excludedPackages).collect(Collectors.toSet()));
}
@Override
public final void beginTree(DetailAST ast) {
fileContext = new FileContext();
}
@Override
public void visitToken(DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.PACKAGE_DEF:
visitPackageDef(ast);
break;
case TokenTypes.IMPORT:
fileContext.registerImport(ast);
break;
case TokenTypes.CLASS_DEF:
case TokenTypes.INTERFACE_DEF:
case TokenTypes.ANNOTATION_DEF:
case TokenTypes.ENUM_DEF:
visitClassDef(ast);
break;
case TokenTypes.TYPE:
fileContext.visitType(ast);
break;
case TokenTypes.LITERAL_NEW:
fileContext.visitLiteralNew(ast);
break;
case TokenTypes.LITERAL_THROWS:
fileContext.visitLiteralThrows(ast);
break;
default:
throw new IllegalArgumentException("Unknown type: " + ast);
}
}
@Override
public void leaveToken(DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.CLASS_DEF:
case TokenTypes.INTERFACE_DEF:
case TokenTypes.ANNOTATION_DEF:
case TokenTypes.ENUM_DEF:
leaveClassDef();
break;
default:
// Do nothing
}
}
/**
* Stores package of current class we check.
* @param pkg package definition.
*/
private void visitPackageDef(DetailAST pkg) {
final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
fileContext.setPackageName(ident.getText());
}
/**
* Creates new context for a given class.
* @param classDef class definition node.
*/
private void visitClassDef(DetailAST classDef) {
final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
fileContext.createNewClassContext(className, classDef.getLineNo(), classDef.getColumnNo());
}
/** Restores previous context. */
private void leaveClassDef() {
fileContext.checkCurrentClassAndRestorePrevious();
}
/**
* Encapsulates information about classes coupling inside single file.
*/
private class FileContext {
/** A map of (imported class name -> class name with package) pairs. */
private final Map<String, String> importedClassPackage = new HashMap<>();
/** Stack of class contexts. */
private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
/** Current file package. */
private String packageName = "";
/** Current context. */
private ClassContext classContext = new ClassContext(this, "", 0, 0);
/**
* Retrieves current file package name.
* @return Package name.
*/
public String getPackageName() {
return packageName;
}
/**
* Sets current context package name.
* @param packageName Package name to be set.
*/
public void setPackageName(String packageName) {
this.packageName = packageName;
}
/**
* Registers given import. This allows us to track imported classes.
* @param imp import definition.
*/
public void registerImport(DetailAST imp) {
final FullIdent ident = FullIdent.createFullIdent(
imp.getLastChild().getPreviousSibling());
final String fullName = ident.getText();
if (fullName.charAt(fullName.length() - 1) != '*') {
final int lastDot = fullName.lastIndexOf(DOT);
importedClassPackage.put(fullName.substring(lastDot + 1), fullName);
}
}
/**
* Retrieves class name with packages. Uses previously registered imports to
* get the full class name.
* @param className Class name to be retrieved.
* @return Class name with package name, if found, {@link Optional#empty()} otherwise.
*/
public Optional<String> getClassNameWithPackage(String className) {
return Optional.ofNullable(importedClassPackage.get(className));
}
/**
* Creates new inner class context with given name and location.
* @param className The class name.
* @param lineNo The class line number.
* @param columnNo The class column number.
*/
public void createNewClassContext(String className, int lineNo, int columnNo) {
classesContexts.push(classContext);
classContext = new ClassContext(this, className, lineNo, columnNo);
}
/** Restores previous context. */
public void checkCurrentClassAndRestorePrevious() {
classContext.checkCoupling();
classContext = classesContexts.pop();
}
/**
* Visits type token for the current class context.
* @param ast TYPE token.
*/
public void visitType(DetailAST ast) {
classContext.visitType(ast);
}
/**
* Visits NEW token for the current class context.
* @param ast NEW token.
*/
public void visitLiteralNew(DetailAST ast) {
classContext.visitLiteralNew(ast);
}
/**
* Visits THROWS token for the current class context.
* @param ast THROWS token.
*/
public void visitLiteralThrows(DetailAST ast) {
classContext.visitLiteralThrows(ast);
}
}
/**
* Encapsulates information about class coupling.
*
* @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
* @author o_sukhodolsky
*/
private class ClassContext {
/** Parent file context. */
private final FileContext parentContext;
/**
* Set of referenced classes.
* Sorted by name for predictable error messages in unit tests.
*/
private final Set<String> referencedClassNames = new TreeSet<>();
/** Own class name. */
private final String className;
/* Location of own class. (Used to log violations) */
/** Line number of class definition. */
private final int lineNo;
/** Column number of class definition. */
private final int columnNo;
/**
* Create new context associated with given class.
* @param parentContext Parent file context.
* @param className name of the given class.
* @param lineNo line of class definition.
* @param columnNo column of class definition.
*/
ClassContext(FileContext parentContext, String className, int lineNo, int columnNo) {
this.parentContext = parentContext;
this.className = className;
this.lineNo = lineNo;
this.columnNo = columnNo;
}
/**
* Visits throws clause and collects all exceptions we throw.
* @param literalThrows throws to process.
*/
public void visitLiteralThrows(DetailAST literalThrows) {
for (DetailAST childAST = literalThrows.getFirstChild();
childAST != null;
childAST = childAST.getNextSibling()) {
if (childAST.getType() != TokenTypes.COMMA) {
addReferencedClassName(childAST);
}
}
}
/**
* Visits type.
* @param ast type to process.
*/
public void visitType(DetailAST ast) {
final String fullTypeName = CheckUtils.createFullType(ast).getText();
addReferencedClassName(fullTypeName);
}
/**
* Visits NEW.
* @param ast NEW to process.
*/
public void visitLiteralNew(DetailAST ast) {
addReferencedClassName(ast.getFirstChild());
}
/**
* Adds new referenced class.
* @param ast a node which represents referenced class.
*/
private void addReferencedClassName(DetailAST ast) {
final String fullIdentName = FullIdent.createFullIdent(ast).getText();
addReferencedClassName(fullIdentName);
}
/**
* Adds new referenced class.
* @param referencedClassName class name of the referenced class.
*/
private void addReferencedClassName(String referencedClassName) {
if (isSignificant(referencedClassName)) {
referencedClassNames.add(referencedClassName);
}
}
/** Checks if coupling less than allowed or not. */
public void checkCoupling() {
referencedClassNames.remove(className);
referencedClassNames.remove(parentContext.getPackageName() + DOT + className);
if (referencedClassNames.size() > max) {
log(lineNo, columnNo, getLogMessageId(),
referencedClassNames.size(), getMax(),
referencedClassNames.toString());
}
}
/**
* Checks if given class shouldn't be ignored and not from java.lang.
* @param candidateClassName class to check.
* @return true if we should count this class.
*/
private boolean isSignificant(String candidateClassName) {
boolean result = !excludedClasses.contains(candidateClassName)
&& !isFromExcludedPackage(candidateClassName);
if (result) {
for (Pattern pattern : excludeClassesRegexps) {
if (pattern.matcher(candidateClassName).matches()) {
result = false;
break;
}
}
}
return result;
}
/**
* Checks if given class should be ignored as it belongs to excluded package.
* @param candidateClassName class to check
* @return true if we should not count this class.
*/
private boolean isFromExcludedPackage(String candidateClassName) {
String classNameWithPackage = candidateClassName;
if (!candidateClassName.contains(DOT)) {
classNameWithPackage = parentContext.getClassNameWithPackage(candidateClassName)
.orElse("");
}
boolean isFromExcludedPackage = false;
if (classNameWithPackage.contains(DOT)) {
final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
final String packageName = classNameWithPackage.substring(0, lastDotIndex);
isFromExcludedPackage = packageName.startsWith("java.lang")
|| excludedPackages.contains(packageName);
}
return isFromExcludedPackage;
}
}
}