TCC metric

This commit is contained in:
Clément Fournier
2017-08-20 00:27:06 +02:00
parent a9be7d4f59
commit 6a6804c7a1
8 changed files with 349 additions and 7 deletions

View File

@ -12,19 +12,19 @@ package net.sourceforge.pmd.lang.java.ast;
public class JavaParserVisitorReducedAdapter extends JavaParserVisitorAdapter {
@Override
public final Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
return visit((ASTAnyTypeDeclaration) node, data);
}
@Override
public final Object visit(ASTAnnotationTypeDeclaration node, Object data) {
public Object visit(ASTAnnotationTypeDeclaration node, Object data) {
return visit((ASTAnyTypeDeclaration) node, data);
}
@Override
public final Object visit(ASTEnumDeclaration node, Object data) {
public Object visit(ASTEnumDeclaration node, Object data) {
return visit((ASTAnyTypeDeclaration) node, data);
}
@ -35,13 +35,13 @@ public class JavaParserVisitorReducedAdapter extends JavaParserVisitorAdapter {
@Override
public final Object visit(ASTMethodDeclaration node, Object data) {
public Object visit(ASTMethodDeclaration node, Object data) {
return visit((ASTMethodOrConstructorDeclaration) node, data);
}
@Override
public final Object visit(ASTConstructorDeclaration node, Object data) {
public Object visit(ASTConstructorDeclaration node, Object data) {
return visit((ASTMethodOrConstructorDeclaration) node, data);
}

View File

@ -10,6 +10,7 @@ import net.sourceforge.pmd.lang.java.metrics.impl.LocMetric.LocClassMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.NcssMetric.NcssClassMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.NoamMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.NopaMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.TccMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.WmcMetric;
import net.sourceforge.pmd.lang.java.metrics.impl.WocMetric;
import net.sourceforge.pmd.lang.metrics.MetricKey;
@ -60,13 +61,20 @@ public enum JavaClassMetricKey implements MetricKey<ASTAnyTypeDeclaration> {
* @see NopaMetric
*/
NOAM(new NoamMetric()),
/**
* Weight of class.
*
* @see WocMetric
*/
WOC(new WocMetric());
WOC(new WocMetric()),
/**
* Tight Class Cohesion.
*
* @see TccMetric
*/
TCC(new TccMetric());
private final JavaClassMetric calculator;

View File

@ -0,0 +1,80 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.metrics.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sourceforge.pmd.lang.java.ast.ASTAnyTypeDeclaration;
import net.sourceforge.pmd.lang.java.metrics.impl.visitors.TccMethodPairVisitor;
import net.sourceforge.pmd.lang.metrics.MetricOptions;
/**
* Tight class cohesion.
*
* @author Clément Fournier
* @since 6.0.0
*/
public class TccMetric extends AbstractJavaClassMetric {
@Override
public double computeFor(ASTAnyTypeDeclaration node, MetricOptions options) {
@SuppressWarnings("unchecked")
Map<String, Set<String>> usagesByMethod = (Map<String, Set<String>>) node.jjtAccept(new TccMethodPairVisitor(), null);
int numPairs = numMethodsRelatedByAttributeAccess(usagesByMethod);
int maxPairs = maxMethodPairs(usagesByMethod.size());
double tcc = maxPairs == 0 ? 0. : numPairs / (double) maxPairs;
return tcc;
}
/**
* Gets the number of pairs of methods that use at least one attribute in common.
*
* @param usagesByMethod Map of method name to names of local attributes accessed
*
* @return The number of pairs
*/
private int numMethodsRelatedByAttributeAccess(Map<String, Set<String>> usagesByMethod) {
List<String> methods = new ArrayList<>(usagesByMethod.keySet());
int methodCount = methods.size();
int pairs = 0;
if (methodCount > 1) {
for (int i = 0; i < methodCount; i++) {
for (int j = i + 1; j < methodCount; j++) {
String firstMethodName = methods.get(i);
String secondMethodName = methods.get(j);
if (!Collections.disjoint(usagesByMethod.get(firstMethodName),
usagesByMethod.get(secondMethodName))) {
pairs++;
}
}
}
}
return pairs;
}
/**
* Calculates the number of possible method pairs of two methods.
*
* @param methods Number of methods in the class
*
* @return Number of possible method pairs
*/
private int maxMethodPairs(int methods) {
return methods * (methods - 1) / 2;
}
}

View File

@ -0,0 +1,121 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.metrics.impl.visitors;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import net.sourceforge.pmd.lang.java.ast.ASTAnyTypeDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
import net.sourceforge.pmd.lang.java.ast.JavaParserVisitorReducedAdapter;
import net.sourceforge.pmd.lang.java.symboltable.ClassScope;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.symboltable.Scope;
/**
* Returns the map of method names to the set local attributes accessed when visiting a class.
*
* @author Clément Fournier
* @since 6.0.0
*/
public class TccMethodPairVisitor extends JavaParserVisitorReducedAdapter {
/**
* Collects for each method of the current class, which local attributes are accessed.
*/
Stack<Map<String, Set<String>>> methodAttributeAccess = new Stack<>();
/** The name of the current method. */
private String currentMethodName;
@Override
public Object visit(ASTAnyTypeDeclaration node, Object data) {
methodAttributeAccess.push(new HashMap<String, Set<String>>());
super.visit(node, data);
methodAttributeAccess.peek().remove(null);
return methodAttributeAccess.pop();
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
if (!node.isAbstract()) {
currentMethodName = node.getQualifiedName().getOperation();
methodAttributeAccess.peek().put(currentMethodName, new HashSet<String>());
super.visit(node, data);
currentMethodName = null;
}
return null;
}
/**
* The primary expression node is used to detect access to attributes and method calls. If the access is not for a
* foreign class, then the {@link #methodAttributeAccess} map is updated for the current method.
*/
@Override
public Object visit(ASTPrimaryExpression node, Object data) {
if (currentMethodName != null) {
Set<String> methodAccess = methodAttributeAccess.peek().get(currentMethodName);
String variableName = getVariableName(node);
if (isLocalAttributeAccess(variableName, node.getScope())) {
methodAccess.add(variableName);
}
}
return super.visit(node, data);
}
private String getVariableName(ASTPrimaryExpression node) {
ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
String variableName = null;
if (name != null) {
int dotIndex = name.getImage().indexOf(".");
if (dotIndex == -1) {
variableName = name.getImage();
} else {
variableName = name.getImage().substring(0, dotIndex);
}
}
return variableName;
}
private boolean isLocalAttributeAccess(String varName, Scope scope) {
Scope currentScope = scope;
while (currentScope != null) {
for (VariableNameDeclaration decl : currentScope.getDeclarations(VariableNameDeclaration.class).keySet()) {
if (decl.getImage().equals(varName)) {
if (currentScope instanceof ClassScope) {
return true;
}
}
}
currentScope = currentScope.getParent(); // WARNING doesn't consider inherited fields or static imports
}
return false;
}
}

View File

@ -36,6 +36,7 @@ public class AllMetricsTest extends SimpleAggregatorTst {
addRule(RULESET, "NopaTest");
addRule(RULESET, "NoamTest");
addRule(RULESET, "WocTest");
addRule(RULESET, "TccTest");
}
}

View File

@ -0,0 +1,26 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.metrics.impl;
import net.sourceforge.pmd.lang.java.metrics.api.JavaClassMetricKey;
import net.sourceforge.pmd.lang.java.metrics.api.JavaOperationMetricKey;
/**
* @author Clément Fournier
* @since 6.0.0
*/
public class TccTestRule extends AbstractMetricTestRule {
@Override
protected JavaClassMetricKey getClassKey() {
return JavaClassMetricKey.TCC;
}
@Override
protected JavaOperationMetricKey getOpKey() {
return null;
}
}

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<test-data>
<code-fragment id="full-example"><![CDATA[
// Data class from ArgoUML
/**
* A property that can be displayed and edited within a PropertyTable.
*
* @author Jeremy Jones
**/
public class Property implements Comparable {
private String _name;
private Class _valueType;
private Object _initialValue;
private Object _currentValue;
private Object[] _availableValues;
public Property(String name, Class valueType, Object initialValue) {
this(name, valueType, initialValue, null);
}
public Property(String name, Class valueType, Object initialValue, Object[] values) {
_name = name;
_valueType = valueType;
_initialValue = initialValue;
_availableValues = values;
_currentValue = _initialValue;
}
public String getName() {
return _name;
}
public Class getValueType() {
return _valueType;
}
public Object getInitialValue() {
return _initialValue;
}
public Object[] getAvailableValues() {
return _availableValues;
}
public Object getCurrentValue() {
return _currentValue;
}
public void setCurrentValue(Object value) {
_currentValue = value;
}
public int compareTo(Object o) {
return _name.compareTo(((Property) o)._name);
}
}
]]></code-fragment>
<test-code>
<description>Full example</description>
<expected-problems>1</expected-problems>
<expected-messages>
<message>'.Property' has value 9.52%.</message>
</expected-messages>
<code-ref id="full-example"/>
</test-code>
<test-code>
<description>Test empty class</description>
<expected-problems>1</expected-problems>
<expected-messages>
<message>'.Foo' has value 0.</message>
</expected-messages>
<code><![CDATA[
public class Foo {
}
]]></code>
</test-code>
<test-code>
<description>TCC doesn't support interfaces or annotations</description>
<expected-problems>0</expected-problems>
<code><![CDATA[
public interface Foo {
public final int h;
@interface Tag {
public static final int num = 0;
}
}
]]></code>
</test-code>
</test-data>

View File

@ -57,4 +57,10 @@
metrics="true">
</rule>
<rule name="TccTest"
message = "''{0}'' has value {1}."
class="net.sourceforge.pmd.lang.java.metrics.impl.TccTestRule"
metrics="true">
</rule>
</ruleset>