/*
 * Cobertura - http://cobertura.sourceforge.net/
 *
 * Copyright (C) 2005 Mark Doliner
 * Copyright (C) 2005 Jeremy Thomerson
 * Copyright (C) 2005 Grzegorz Lukasik
 * Copyright (C) 2008 Tri Bao Ho
 * Copyright (C) 2009 John Lewis
 * Copyright (C) 2014 Kunal Shah
 *
 * Cobertura is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * Cobertura 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Cobertura; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */
package net.sourceforge.cobertura.reporting;

import net.sourceforge.cobertura.coveragedata.ClassData;
import net.sourceforge.cobertura.coveragedata.PackageData;
import net.sourceforge.cobertura.coveragedata.ProjectData;
import net.sourceforge.cobertura.coveragedata.SourceFileData;
import net.sourceforge.cobertura.javancss.FunctionMetric;
import net.sourceforge.cobertura.javancss.Javancss;
import net.sourceforge.cobertura.javancss.JavancssFactory;

import net.sourceforge.cobertura.util.FileFinder;
import net.sourceforge.cobertura.util.Source;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.Validate;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.util.TraceSignatureVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

Allows complexity computing for source files, packages and a whole project. Average McCabe's number for methods contained in the specified entity is returned. This class depends on FileFinder which is used to map source file names to existing files.

One instance of this class should be used for the same set of source files - an object of this class can cache computed results.

Author:Grzegorz Lukasik
/** * Allows complexity computing for source files, packages and a whole project. Average * McCabe's number for methods contained in the specified entity is returned. This class * depends on FileFinder which is used to map source file names to existing files. * <p/> * <p>One instance of this class should be used for the same set of source files - an * object of this class can cache computed results.</p> * * @author Grzegorz Lukasik */
public class ComplexityCalculator { private static final Logger logger = LoggerFactory .getLogger(ComplexityCalculator.class); public static final Complexity ZERO_COMPLEXITY = new Complexity(); // Finder used to map source file names to existing files private final FileFinder finder; // Factory use to get instances of {@code Javancss} private final JavancssFactory javancssFactory; // Contains pairs (String sourceFileName, Complexity complexity) private Map sourceFileCNNCache = new HashMap(); // Contains pairs (String packageName, Complexity complexity) private Map packageCNNCache = new HashMap(); // Cache for source file name to its function metrics parsed by {@link Javancss} private static final int FILE_FUNCTION_METRIC_CACHE_SIZE = 6; private Map<String, List<FunctionMetric>> sourceFileFunctionMetricCache = new LinkedHashMap<String, List<FunctionMetric>>( FILE_FUNCTION_METRIC_CACHE_SIZE, 0.75f, true) { private static final long serialVersionUID = 1L; @Override protected boolean removeEldestEntry(Entry<String, List<FunctionMetric>> arg0) { return size() > FILE_FUNCTION_METRIC_CACHE_SIZE; } }; private boolean calculateMethodComplexity; private String encoding;
Creates new calculator. Passed FileFinder will be used to map source file names to existing files when needed.
Params:
  • finder – FileFinder that allows to find source files
Throws:
/** * Creates new calculator. Passed {@link FileFinder} will be used to * map source file names to existing files when needed. * * @param finder {@link FileFinder} that allows to find source files * * @throws NullPointerException if finder is null */
public ComplexityCalculator(FileFinder finder) { this(finder, new JavancssFactory()); }
Creates new calculator. Passed FileFinder will be used to map source file names to existing files when needed.
Params:
  • finder – FileFinder that allows to find source files
  • javancssFactory – factory to get instances of Javancss
Throws:
/** * Creates new calculator. Passed {@link FileFinder} will be used to * map source file names to existing files when needed. * * @param finder {@link FileFinder} that allows to find source files * @param javancssFactory factory to get instances of {@link Javancss} * @throws NullPointerException if finder or javancssFactory is null */
public ComplexityCalculator(FileFinder finder, JavancssFactory javancssFactory) { this.finder = Validate.notNull(finder, "finder should not be null"); this.javancssFactory = Validate.notNull(javancssFactory, "javancssFactory should not be null"); }
Calculates the code complexity number for single source file. "CCN" stands for "code complexity number." This is sometimes referred to as McCabe's number. This method calculates the average cyclomatic code complexity of all methods of all classes in a given directory.
Params:
  • sourceFileName – the name of the source file for which you want to calculate the complexity
Throws:
Returns:average complexity for the specified source file
/** * Calculates the code complexity number for single source file. * "CCN" stands for "code complexity number." This is * sometimes referred to as McCabe's number. This method * calculates the average cyclomatic code complexity of all * methods of all classes in a given directory. * * @param sourceFileName the name of the source file for which you want to calculate the complexity * @return average complexity for the specified source file * @throws IOException */
private Complexity getAccumlatedCCNForSingleFile(String sourceFileName) throws IOException { List methodMetrics = getFunctionMetricsForSingleFile(sourceFileName); if (methodMetrics.isEmpty()) { return ZERO_COMPLEXITY; } int classCcn = 0; for (Iterator method = methodMetrics.iterator(); method.hasNext(); ) { FunctionMetric singleMethodMetrics = (FunctionMetric) method.next(); classCcn += singleMethodMetrics.ccn; } return new Complexity(classCcn, methodMetrics.size()); }
Computes CCN for all sources contained in the project. CCN for whole project is an average CCN for source files. All source files for which CCN cannot be computed are ignored.
Params:
  • projectData – project to compute CCN for
Throws:
Returns:CCN for project or 0 if no source files were found
/** * Computes CCN for all sources contained in the project. * CCN for whole project is an average CCN for source files. * All source files for which CCN cannot be computed are ignored. * * @param projectData project to compute CCN for * * @return CCN for project or 0 if no source files were found * * @throws NullPointerException if projectData is null */
public double getCCNForProject(ProjectData projectData) { // Sum complexity for all packages Complexity act = new Complexity(); for (Object pkg : projectData.getPackages()) { PackageData packageData = (PackageData) pkg; act.add(getCCNForPackageInternal(packageData)); } // Return average CCN for source files return act.averageCCN(); }
Computes CCN for all sources contained in the specified package. All source files that cannot be mapped to existing files are ignored.
Params:
  • packageData – package to compute CCN for
Throws:
Returns:CCN for the specified package or 0 if no source files were found
/** * Computes CCN for all sources contained in the specified package. * All source files that cannot be mapped to existing files are ignored. * * @param packageData package to compute CCN for * * @return CCN for the specified package or 0 if no source files were found * * @throws NullPointerException if <code>packageData</code> is <code>null</code> */
public double getCCNForPackage(PackageData packageData) { return getCCNForPackageInternal(packageData).averageCCN(); } private Complexity getCCNForPackageInternal(PackageData packageData) { // Return CCN if computed earlier Complexity cachedCCN = (Complexity) packageCNNCache.get(packageData .getName()); if (cachedCCN != null) { return cachedCCN; } // Compute CCN for all source files inside package Complexity act = new Complexity(); for (Iterator it = packageData.getSourceFiles().iterator(); it .hasNext();) { SourceFileData sourceData = (SourceFileData) it.next(); act.add(getCCNForSourceFileNameInternal(sourceData.getName())); } // Cache result and return it packageCNNCache.put(packageData.getName(), act); return act; }
Computes CCN for single source file.
Params:
  • sourceFile – source file to compute CCN for
Throws:
Returns:CCN for the specified source file, 0 if cannot map sourceFile to existing file
/** * Computes CCN for single source file. * * @param sourceFile source file to compute CCN for * * @return CCN for the specified source file, 0 if cannot map <code>sourceFile</code> to existing file * * @throws NullPointerException if <code>sourceFile</code> is <code>null</code> */
public double getCCNForSourceFile(SourceFileData sourceFile) { return getCCNForSourceFileNameInternal(sourceFile.getName()) .averageCCN(); }
Get the function metrics for the given source file. Use this over the sourceFileFunctionMetricCache field.
Params:
  • sourceFileName – the name of the source file
Returns:the function metrics for the given source file (parsed by Javancss)
/** * Get the function metrics for the given source file. Use this over the {@link #sourceFileFunctionMetricCache} field. * * @param sourceFileName the name of the source file * @return the function metrics for the given source file (parsed by {@link Javancss}) */
private List<FunctionMetric> getFunctionMetricsForSingleFile(String sourceFileName) { List<FunctionMetric> functionMetrics = Collections.emptyList(); if (!sourceFileFunctionMetricCache.containsKey(sourceFileName)) { Source source = null; try { source = finder.getSource(sourceFileName); if (source != null && sourceFileName.endsWith(".java")) { Javancss javancss = javancssFactory.newInstance(source.getInputStream(), encoding); if (javancss.getLastErrorMessage() != null) { //there is an error while parsing the java file. log it logger.warn("JavaNCSS got an error while parsing the java " + source.getOriginDesc() + "\n" + javancss.getLastErrorMessage()); } functionMetrics = javancss.getFunctionMetrics(); } } finally { if (source != null) { source.close(); } } sourceFileFunctionMetricCache.put(sourceFileName, functionMetrics); } return sourceFileFunctionMetricCache.get(sourceFileName); } private Complexity getCCNForSourceFileNameInternal(String sourceFileName) { // Return CCN if computed earlier Complexity cachedCCN = (Complexity) sourceFileCNNCache .get(sourceFileName); if (cachedCCN != null) { return cachedCCN; } // Compute CCN and cache it for further use Complexity result = ZERO_COMPLEXITY; try { result = getAccumlatedCCNForSingleFile(sourceFileName); } catch (IOException ex) { logger .info("Cannot find source file during CCN computation, source=[" + sourceFileName + "]"); } sourceFileCNNCache.put(sourceFileName, result); return result; }
Computes CCN for source file the specified class belongs to.
Params:
  • classData – package to compute CCN for
Throws:
Returns:CCN for source file the specified class belongs to
/** * Computes CCN for source file the specified class belongs to. * * @param classData package to compute CCN for * * @return CCN for source file the specified class belongs to * * @throws NullPointerException if <code>classData</code> is <code>null</code> */
public double getCCNForClass(ClassData classData) { return getCCNForSourceFileNameInternal(classData.getSourceFileName()) .averageCCN(); }
Computes CCN for a method.
Params:
  • classData – class data for the class which contains the method to compute CCN for
  • methodName – the name of the method to compute CCN for
  • methodDescriptor – the descriptor of the method to compute CCN for
Throws:
Returns:CCN for the method
/** * Computes CCN for a method. * * @param classData class data for the class which contains the method to compute CCN for * @param methodName the name of the method to compute CCN for * @param methodDescriptor the descriptor of the method to compute CCN for * @return CCN for the method * @throws NullPointerException if <code>classData</code> is <code>null</code> */
public int getCCNForMethod(ClassData classData, String methodName, String methodDescriptor) { if(!calculateMethodComplexity) { return 0; } Validate.notNull(classData, "classData must not be null"); Validate.notNull(methodName, "methodName must not be null"); Validate.notNull(methodDescriptor, "methodDescriptor must not be null"); int complexity = 0; List<FunctionMetric> methodMetrics = getFunctionMetricsForSingleFile(classData.getSourceFileName()); // golden method = method for which we need ccn String goldenMethodName = methodName; boolean isConstructor = false; if (goldenMethodName.equals("<init>")) { isConstructor = true; goldenMethodName = classData.getBaseName(); } // fully-qualify the method goldenMethodName = classData.getName() + "." + goldenMethodName; // replace nested class separator $ by . goldenMethodName = goldenMethodName.replaceAll(Pattern.quote("$"), "."); TraceSignatureVisitor v = new TraceSignatureVisitor(Opcodes.ACC_PUBLIC); SignatureReader r = new SignatureReader(methodDescriptor); r.accept(v); // for the scope of this method, signature = signature of the method excluding the method name String goldenSignature = v.getDeclaration(); // get parameter type list string which is enclosed by round brackets () goldenSignature = goldenSignature.substring(1, goldenSignature.length() - 1); // collect all the signatures with the same method name as golden method Map<String, Integer> candidateSignatureToCcn = new HashMap<String, Integer>(); for (FunctionMetric singleMethodMetrics : methodMetrics) { String candidateMethodName = singleMethodMetrics.name.substring(0, singleMethodMetrics.name.indexOf('(')); String candidateSignature = stripTypeParameters(singleMethodMetrics.name.substring(singleMethodMetrics.name.indexOf('(') + 1, singleMethodMetrics.name.length() - 1)); if (goldenMethodName.equals(candidateMethodName)) { candidateSignatureToCcn.put(candidateSignature, singleMethodMetrics.ccn); } } // if only one signature, no signature matching needed if (candidateSignatureToCcn.size() == 1) { return candidateSignatureToCcn.values().iterator().next(); } // else, do signature matching and find the best match // update golden signature using reflection if (!goldenSignature.isEmpty()) { try { String[] goldenParameterTypeStrings = goldenSignature.split(","); Class<?>[] goldenParameterTypes = new Class[goldenParameterTypeStrings.length]; for (int i = 0; i < goldenParameterTypeStrings.length; i++) { goldenParameterTypes[i] = ClassUtils.getClass(goldenParameterTypeStrings[i].trim(), false); } Class<?> klass = ClassUtils.getClass(classData.getName(), false); if (isConstructor) { Constructor<?> realMethod = klass.getDeclaredConstructor(goldenParameterTypes); goldenSignature = realMethod.toGenericString(); } else { Method realMethod = klass.getDeclaredMethod(methodName, goldenParameterTypes); goldenSignature = realMethod.toGenericString(); } // replace varargs ellipsis with array notation goldenSignature = goldenSignature.replaceAll("\\.\\.\\.", "[]"); // extract the parameter type list string goldenSignature = goldenSignature.substring(goldenSignature.indexOf("(") + 1, goldenSignature.length() - 1); // strip the type parameters to get raw types goldenSignature = stripTypeParameters(goldenSignature); } catch (Exception e) { logger.error("Error while getting method CC for " + goldenMethodName, e); return 0; } } // replace nested class separator $ by . goldenSignature = goldenSignature.replaceAll(Pattern.quote("$"), "."); // signature matching - due to loss of fully qualified parameter types from JavaCC, get ccn for the closest match double signatureMatchPercentTillNow = 0; for (Entry<String, Integer> candidateSignatureToCcnEntry : candidateSignatureToCcn.entrySet()) { String candidateSignature = candidateSignatureToCcnEntry.getKey(); double currentMatchPercent = matchSignatures(candidateSignature, goldenSignature); if (currentMatchPercent == 1) { return candidateSignatureToCcnEntry.getValue(); } if (currentMatchPercent > signatureMatchPercentTillNow) { complexity = candidateSignatureToCcnEntry.getValue(); signatureMatchPercentTillNow = currentMatchPercent; } } return complexity; }
Strip the type parameters from the signature with parameterized types. Spaces are preserved.

E.g. stripTypeParamaters("List<String> a, Map<String, List<? extends Person>> b, int c") returns "List, Map, int".
Params:
  • signature – the signature with parameterized types which needs to be stripped
Returns:the stripped signature
/** * Strip the type parameters from the signature with parameterized types. Spaces are preserved. * <p/> * E.g. {@code stripTypeParamaters("List<String> a, Map<String, List<? extends Person>> b, int c")} returns {@code "List, Map, int"}. * * @param signature the signature with parameterized types which needs to be stripped * @return the stripped signature */
private static final String stripTypeParameters(String signature) { StringBuilder strippedSignature = new StringBuilder(); int openIndex = -1; int openCount = 0; final char open = '<'; final char close = '>'; while ((openIndex = signature.indexOf(open)) > -1) { strippedSignature.append(signature.substring(0, openIndex)); for (int i = openIndex + 1; i < signature.length(); i++) { if (signature.charAt(i) == close) { if (openCount == 0) { signature = signature.substring(i + 1); break; } openCount--; } else if (signature.charAt(i) == open) { openCount++; } } } strippedSignature.append(signature); return strippedSignature.toString(); }
Match the candidate signature against the golden signature and return the match confidence. A signature, for the scope of this method, is just the string of method parameter types; for e.g. signature for method public void process(List<String> names, int[] scores) is java.util.List, int[].

Formula for calculating the return value is:
Match Confidence = Average of Individual Parameter Type Match Percents

Important: As indicated by the formula, do not compare the confidences across different golden signatures since the confidence values are not absolute.
Params:
  • candidate – signature which is to be matched; can have types which are not fully qualified
  • golden – signature against which candidate is matched; should have only fully qualified types
Returns:a value denoting the confidence that the given candidate signature matches the golden signature; between 0 and 1; 0 means guaranteed mismatch and a 1 means that guaranteed match.
/** * Match the {@code candidate} signature against the {@code golden} signature and return the match confidence. * A signature, for the scope of this method, is just the string of method parameter types; for e.g. signature for method * {@code public void process(List<String> names, int[] scores)} is {@code java.util.List, int[]}. * <p/> * Formula for calculating the return value is:<br/> * Match Confidence = Average of Individual Parameter Type Match Percents<br/> * <p/> * Important: As indicated by the formula, do not compare the confidences across different golden signatures since the confidence values are not * absolute. * * @param candidate signature which is to be matched; can have types which are not fully qualified * @param golden signature against which {@code candidate} is matched; should have only fully qualified types * @return a value denoting the confidence that the given {@code candidate} signature matches the {@code golden} signature; between 0 and 1; 0 * means guaranteed mismatch and a 1 means that guaranteed match. */
private static final double matchSignatures(String candidate, String golden) { // assumption: golden is assumed to have fully qualified types and candidate may have types which are not fully qualified String[] candidateParamTypes = candidate.split(","); String[] goldenParamTypes = golden.split(","); // mismatch: if count of parameters are not same if (goldenParamTypes.length != candidateParamTypes.length) { return 0; } int totalParamTypes = goldenParamTypes.length; // complete match: if no parameters if (totalParamTypes == 0) { return 1; } double totalMatchPercent = 0; for (int i = 0; i < totalParamTypes; i++) { String goldenParamType = goldenParamTypes[i].trim(); String candidateParamType = candidateParamTypes[i].trim(); // mismatch: if golden parameter type is smaller than candidate parameter type if (goldenParamType.length() < candidateParamType.length()) { return 0; } if (goldenParamType.equals(candidateParamType)) { // complete match totalMatchPercent += 1; } else { int partialMatchIndex = goldenParamType.lastIndexOf(candidateParamType); // package separator '.' cannot be before position 1; e.g. a.type if (partialMatchIndex > 1 && goldenParamType.length() == (partialMatchIndex + candidateParamType.length()) && goldenParamType.charAt(partialMatchIndex - 1) == '.') { // partial match totalMatchPercent += (1 - (double) partialMatchIndex / goldenParamType.length()); } else { // mismatch return 0; } } } return totalMatchPercent / totalParamTypes; }
Indicate whether complexity should be calculated for methods.
Params:
  • calculateMethodComplexity – Whether to calculate method complexity
/** * Indicate whether complexity should be calculated for methods. * * @param calculateMethodComplexity Whether to calculate method complexity */
public void setCalculateMethodComplexity(boolean calculateMethodComplexity) { this.calculateMethodComplexity = calculateMethodComplexity; }
Set the encoding to be used when reading input streams.
Params:
  • encoding – The encoding to use
/** * Set the encoding to be used when reading input streams. * * @param encoding * The encoding to use */
public void setEncoding(String encoding) { this.encoding = encoding; }
Represents complexity of source file, package or project. Stores the number of methods inside entity and accumlated complexity for these methods.
/** * Represents complexity of source file, package or project. Stores the number of * methods inside entity and accumlated complexity for these methods. */
private static class Complexity { private double accumlatedCCN; private int methodsNum; public Complexity(double accumlatedCCN, int methodsNum) { this.accumlatedCCN = accumlatedCCN; this.methodsNum = methodsNum; } public Complexity() { this(0, 0); } public double averageCCN() { if (methodsNum == 0) { return 0; } return accumlatedCCN / methodsNum; } public void add(Complexity second) { accumlatedCCN += second.accumlatedCCN; methodsNum += second.methodsNum; } } }