com.puppycrawl.tools.checkstyle.checks.metrics.AbstractClassCouplingCheck Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of checkstyle Show documentation
Show all versions of checkstyle Show documentation
Checkstyle is a development tool to help programmers write Java code
that adheres to a coding standard
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2020 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.FileStatefulCheck;
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.CheckUtil;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
/**
* Base class for coupling calculation.
*
*/
@FileStatefulCheck
public abstract class AbstractClassCouplingCheck extends AbstractCheck {
/** A package separator - "." */
private static final String DOT = ".";
/** Class names to ignore. */
private static final Set 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",
"Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
}).collect(Collectors.toSet()));
/** Package names to ignore. */
private static final Set DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
/** Specify user-configured regular expressions to ignore classes. */
private final List excludeClassesRegexps = new ArrayList<>();
/** A map of (imported class name -> class name with package) pairs. */
private final Map importedClassPackages = new HashMap<>();
/** Stack of class contexts. */
private final Deque classesContexts = new ArrayDeque<>();
/** Specify user-configured class names to ignore. */
private Set excludedClasses = DEFAULT_EXCLUDED_CLASSES;
/**
* Specify user-configured packages to ignore. All excluded packages
* should end with a period, so it also appends a dot to a package name.
*/
private Set excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
/** Specify the maximum threshold allowed. */
private int max;
/** Current file package. */
private String packageName;
/**
* Creates new instance of the check.
* @param defaultMax default value for allowed complexity.
*/
protected AbstractClassCouplingCheck(int defaultMax) {
max = defaultMax;
excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
}
/**
* Returns message key we use for log violations.
* @return message key we use for log violations.
*/
protected abstract String getLogMessageId();
@Override
public final int[] getDefaultTokens() {
return getRequiredTokens();
}
/**
* Setter to specify the maximum threshold allowed.
*
* @param max allowed complexity.
*/
public final void setMax(int max) {
this.max = max;
}
/**
* Setter to specify user-configured class names 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()));
}
/**
* Setter to specify user-configured regular expressions to ignore classes.
*
* @param from array representing regular expressions of classes to ignore.
*/
public void setExcludeClassesRegexps(String... from) {
excludeClassesRegexps.addAll(Arrays.stream(from.clone())
.map(CommonUtil::createPattern)
.collect(Collectors.toSet()));
}
/**
* Setter to specify user-configured packages to ignore. All excluded 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 invalidIdentifiers = Arrays.stream(excludedPackages)
.filter(excludedPackageName -> !CommonUtil.isName(excludedPackageName))
.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) {
importedClassPackages.clear();
classesContexts.clear();
classesContexts.push(new ClassContext("", null));
packageName = "";
}
@Override
public void visitToken(DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.PACKAGE_DEF:
visitPackageDef(ast);
break;
case TokenTypes.IMPORT:
registerImport(ast);
break;
case TokenTypes.CLASS_DEF:
case TokenTypes.INTERFACE_DEF:
case TokenTypes.ANNOTATION_DEF:
case TokenTypes.ENUM_DEF:
visitClassDef(ast);
break;
case TokenTypes.EXTENDS_CLAUSE:
case TokenTypes.IMPLEMENTS_CLAUSE:
case TokenTypes.TYPE:
visitType(ast);
break;
case TokenTypes.LITERAL_NEW:
visitLiteralNew(ast);
break;
case TokenTypes.LITERAL_THROWS:
visitLiteralThrows(ast);
break;
case TokenTypes.ANNOTATION:
visitAnnotationType(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());
packageName = 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();
createNewClassContext(className, classDef);
}
/** Restores previous context. */
private void leaveClassDef() {
checkCurrentClassAndRestorePrevious();
}
/**
* Registers given import. This allows us to track imported classes.
* @param imp import definition.
*/
private void registerImport(DetailAST imp) {
final FullIdent ident = FullIdent.createFullIdent(
imp.getLastChild().getPreviousSibling());
final String fullName = ident.getText();
final int lastDot = fullName.lastIndexOf(DOT);
importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
}
/**
* Creates new inner class context with given name and location.
* @param className The class name.
* @param ast The class ast.
*/
private void createNewClassContext(String className, DetailAST ast) {
classesContexts.push(new ClassContext(className, ast));
}
/** Restores previous context. */
private void checkCurrentClassAndRestorePrevious() {
classesContexts.pop().checkCoupling();
}
/**
* Visits type token for the current class context.
* @param ast TYPE token.
*/
private void visitType(DetailAST ast) {
classesContexts.peek().visitType(ast);
}
/**
* Visits NEW token for the current class context.
* @param ast NEW token.
*/
private void visitLiteralNew(DetailAST ast) {
classesContexts.peek().visitLiteralNew(ast);
}
/**
* Visits THROWS token for the current class context.
* @param ast THROWS token.
*/
private void visitLiteralThrows(DetailAST ast) {
classesContexts.peek().visitLiteralThrows(ast);
}
/**
* Visit ANNOTATION literal and get its type to referenced classes of context.
* @param annotationAST Annotation ast.
*/
private void visitAnnotationType(DetailAST annotationAST) {
final DetailAST children = annotationAST.getFirstChild();
final DetailAST type = children.getNextSibling();
classesContexts.peek().addReferencedClassName(type.getText());
}
/**
* Encapsulates information about class coupling.
*
*/
private class ClassContext {
/**
* Set of referenced classes.
* Sorted by name for predictable violation messages in unit tests.
*/
private final Set referencedClassNames = new TreeSet<>();
/** Own class name. */
private final String className;
/* Location of own class. (Used to log violations) */
/** AST of class definition. */
private final DetailAST classAst;
/**
* Create new context associated with given class.
* @param className name of the given class.
* @param ast ast of class definition.
*/
/* package */ ClassContext(String className, DetailAST ast) {
this.className = className;
classAst = ast;
}
/**
* 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 = CheckUtil.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(packageName + DOT + className);
if (referencedClassNames.size() > max) {
log(classAst, getLogMessageId(),
referencedClassNames.size(), max,
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) {
return !excludedClasses.contains(candidateClassName)
&& !isFromExcludedPackage(candidateClassName)
&& !isExcludedClassRegexp(candidateClassName);
}
/**
* 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 = getClassNameWithPackage(candidateClassName)
.orElse("");
}
boolean isFromExcludedPackage = false;
if (classNameWithPackage.contains(DOT)) {
final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
final String candidatePackageName =
classNameWithPackage.substring(0, lastDotIndex);
isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
|| excludedPackages.contains(candidatePackageName);
}
return isFromExcludedPackage;
}
/**
* Retrieves class name with packages. Uses previously registered imports to
* get the full class name.
* @param examineClassName Class name to be retrieved.
* @return Class name with package name, if found, {@link Optional#empty()} otherwise.
*/
private Optional getClassNameWithPackage(String examineClassName) {
return Optional.ofNullable(importedClassPackages.get(examineClassName));
}
/**
* Checks if given class should be ignored as it belongs to excluded class regexp.
* @param candidateClassName class to check.
* @return true if we should not count this class.
*/
private boolean isExcludedClassRegexp(String candidateClassName) {
boolean result = false;
for (Pattern pattern : excludeClassesRegexps) {
if (pattern.matcher(candidateClassName).matches()) {
result = true;
break;
}
}
return result;
}
}
}