classesToRemove = new HashSet<>();
for (String classFqn : classReferencesGraph.vertexSet()) {
if (!packagesInCodebase.contains(getPackage(classFqn))) {
classesToRemove.add(classFqn);
}
}
classReferencesGraph.removeAllVertices(classesToRemove);
}
String getPackage(String fqn) {
// handle no package
if (!fqn.contains(".")) {
return "";
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(0, lastIndex);
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseCodebaseVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import lombok.Getter;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.java.JavaIsoVisitor;
@Getter
public abstract class BaseCodebaseVisitor extends JavaIsoVisitor
{
protected final DependencyCollector dependencyCollector;
protected BaseCodebaseVisitor(DependencyCollector dependencyCollector) {
this.dependencyCollector = dependencyCollector;
}
protected abstract String getCurrentOwnerFqn();
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseTypeProcessor.java
================================================
package org.hjug.graphbuilder.visitor;
import lombok.extern.slf4j.Slf4j;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.Cursor;
import org.openrewrite.java.service.AnnotationService;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeTree;
@Slf4j
public abstract class BaseTypeProcessor {
private final TypeDependencyExtractor typeDependencyExtractor = new TypeDependencyExtractor();
protected abstract DependencyCollector getDependencyCollector();
protected void processType(String ownerFqn, JavaType javaType) {
if (javaType == null || javaType instanceof JavaType.Unknown) {
return;
}
for (String dependency : typeDependencyExtractor.extractDependencies(javaType)) {
getDependencyCollector().addClassDependency(ownerFqn, dependency);
}
}
protected void processAnnotation(String ownerFqn, J.Annotation annotation, Cursor cursor) {
if (annotation.getType() instanceof JavaType.Unknown) {
return;
}
JavaType.Class type = (JavaType.Class) annotation.getType();
if (null != type) {
String annotationFqn = type.getFullyQualifiedName();
log.debug("Variable Annotation FQN: {}", annotationFqn);
getDependencyCollector().addClassDependency(ownerFqn, annotationFqn);
if (null != annotation.getArguments()) {
for (Expression argument : annotation.getArguments()) {
processType(ownerFqn, argument.getType());
}
}
}
}
protected void processTypeParameter(String ownerFqn, J.TypeParameter typeParameter, Cursor cursor) {
if (null != typeParameter.getBounds()) {
for (TypeTree bound : typeParameter.getBounds()) {
processType(ownerFqn, bound.getType());
}
}
if (!typeParameter.getAnnotations().isEmpty()) {
for (J.Annotation annotation : typeParameter.getAnnotations()) {
processAnnotation(ownerFqn, annotation, cursor);
}
}
}
protected void processAnnotations(String ownerFqn, Cursor cursor) {
AnnotationService annotationService = new AnnotationService();
for (J.Annotation annotation : annotationService.getAllAnnotations(cursor)) {
processAnnotation(ownerFqn, annotation, cursor);
}
}
protected String getPackageFromFqn(String fqn) {
if (!fqn.contains(".")) {
return "";
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(0, lastIndex);
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/FqnCapturingProcessor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openrewrite.java.tree.J;
public interface FqnCapturingProcessor {
default J.ClassDeclaration captureClassDeclarations(
J.ClassDeclaration classDecl, Map> fqns) {
// get class fqn (including "$")
String fqn = classDecl.getType().getFullyQualifiedName();
String currentPackage = getPackage(fqn);
String className = getClassName(fqn);
Map classesInPackage = fqns.getOrDefault(currentPackage, new HashMap<>());
if (className.contains("$")) {
String normalizedClassName = className.replace('$', '.');
List parts = Arrays.asList(normalizedClassName.split("\\."));
for (int i = 0; i < parts.size(); i++) {
String key = String.join(".", parts.subList(i, parts.size()));
classesInPackage.put(key, currentPackage + "." + normalizedClassName);
}
} else {
classesInPackage.put(className, fqn);
}
fqns.put(currentPackage, classesInPackage);
return classDecl;
}
default String getPackage(String fqn) {
// handle no package
if (!fqn.contains(".")) {
return "";
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(0, lastIndex);
}
/**
*
* @param fqn
* @return Class name (including "$") after last period in FQN
*/
default String getClassName(String fqn) {
// handle no package
if (!fqn.contains(".")) {
return fqn;
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(lastIndex + 1);
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.java.tree.*;
@Slf4j
public class JavaClassDeclarationVisitor extends BaseCodebaseVisitor
{
private final BaseTypeProcessor typeProcessor;
private String currentOwnerFqn;
public JavaClassDeclarationVisitor(DependencyCollector dependencyCollector) {
super(dependencyCollector);
this.typeProcessor = new BaseTypeProcessor() {
@Override
protected DependencyCollector getDependencyCollector() {
return dependencyCollector;
}
};
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {
JavaType.FullyQualified type = classDecl.getType();
if (type == null) {
log.warn("ClassDeclaration has null type, skipping: {}", classDecl.getSimpleName());
return classDecl;
}
String owningFqn = type.getFullyQualifiedName();
String previousOwner = currentOwnerFqn;
currentOwnerFqn = owningFqn;
try {
typeProcessor.processType(owningFqn, type);
TypeTree extendsTypeTree = classDecl.getExtends();
if (null != extendsTypeTree) {
typeProcessor.processType(owningFqn, extendsTypeTree.getType());
}
List implementsTypeTree = classDecl.getImplements();
if (null != implementsTypeTree) {
for (TypeTree typeTree : implementsTypeTree) {
typeProcessor.processType(owningFqn, typeTree.getType());
}
}
for (J.Annotation leadingAnnotation : classDecl.getLeadingAnnotations()) {
typeProcessor.processAnnotation(owningFqn, leadingAnnotation, getCursor());
}
if (null != classDecl.getTypeParameters()) {
for (J.TypeParameter typeParameter : classDecl.getTypeParameters()) {
typeProcessor.processTypeParameter(owningFqn, typeParameter, getCursor());
}
}
return super.visitClassDeclaration(classDecl, p);
} finally {
currentOwnerFqn = previousOwner;
}
}
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P p) {
J.MethodInvocation methodInvocation = super.visitMethodInvocation(method, p);
if (currentOwnerFqn == null) {
return methodInvocation;
}
JavaType.Method methodType = methodInvocation.getMethodType();
if (null != methodType && null != methodType.getDeclaringType()) {
typeProcessor.processType(currentOwnerFqn, methodType.getDeclaringType());
}
if (null != methodInvocation.getTypeParameters()
&& !methodInvocation.getTypeParameters().isEmpty()) {
for (Expression typeParameter : methodInvocation.getTypeParameters()) {
typeProcessor.processType(currentOwnerFqn, typeParameter.getType());
}
}
return methodInvocation;
}
@Override
public J.NewClass visitNewClass(J.NewClass newClass, P p) {
J.NewClass result = super.visitNewClass(newClass, p);
if (currentOwnerFqn != null) {
typeProcessor.processType(currentOwnerFqn, newClass.getType());
}
return result;
}
@Override
public J.Lambda visitLambda(J.Lambda lambda, P p) {
if (currentOwnerFqn != null && lambda.getType() != null) {
typeProcessor.processType(currentOwnerFqn, lambda.getType());
}
// Recursively visit the lambda body to capture method invocations and type references
// The super.visitLambda call will traverse into the lambda's body and parameters
return super.visitLambda(lambda, p);
}
@Override
public J.If visitIf(J.If iff, P p) {
return super.visitIf(iff, p);
}
@Override
public J.ForLoop visitForLoop(J.ForLoop forLoop, P p) {
return super.visitForLoop(forLoop, p);
}
@Override
public J.ForEachLoop visitForEachLoop(J.ForEachLoop forEachLoop, P p) {
return super.visitForEachLoop(forEachLoop, p);
}
@Override
public J.WhileLoop visitWhileLoop(J.WhileLoop whileLoop, P p) {
return super.visitWhileLoop(whileLoop, p);
}
@Override
public J.DoWhileLoop visitDoWhileLoop(J.DoWhileLoop doWhileLoop, P p) {
return super.visitDoWhileLoop(doWhileLoop, p);
}
@Override
public J.Switch visitSwitch(J.Switch switchStatement, P p) {
return super.visitSwitch(switchStatement, p);
}
@Override
public J.Try visitTry(J.Try tryStatement, P p) {
J.Try result = super.visitTry(tryStatement, p);
if (currentOwnerFqn != null && tryStatement.getCatches() != null) {
for (J.Try.Catch catchClause : tryStatement.getCatches()) {
if (catchClause.getParameter().getTree() instanceof J.VariableDeclarations) {
J.VariableDeclarations varDecl =
(J.VariableDeclarations) catchClause.getParameter().getTree();
if (varDecl.getTypeExpression() != null) {
typeProcessor.processType(
currentOwnerFqn, varDecl.getTypeExpression().getType());
}
}
}
}
return result;
}
@Override
public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, P p) {
J.InstanceOf result = super.visitInstanceOf(instanceOf, p);
if (currentOwnerFqn != null && instanceOf.getClazz() != null && instanceOf.getClazz() instanceof TypeTree) {
typeProcessor.processType(currentOwnerFqn, ((TypeTree) instanceOf.getClazz()).getType());
}
return result;
}
@Override
public J.TypeCast visitTypeCast(J.TypeCast typeCast, P p) {
J.TypeCast result = super.visitTypeCast(typeCast, p);
if (currentOwnerFqn != null && typeCast.getClazz() != null) {
typeProcessor.processType(
currentOwnerFqn, typeCast.getClazz().getTree().getType());
}
return result;
}
@Override
public J.MemberReference visitMemberReference(J.MemberReference memberRef, P p) {
J.MemberReference result = super.visitMemberReference(memberRef, p);
if (currentOwnerFqn != null && memberRef.getType() != null) {
typeProcessor.processType(currentOwnerFqn, memberRef.getType());
}
return result;
}
@Override
public J.NewArray visitNewArray(J.NewArray newArray, P p) {
J.NewArray result = super.visitNewArray(newArray, p);
if (currentOwnerFqn != null && newArray.getType() != null) {
typeProcessor.processType(currentOwnerFqn, newArray.getType());
}
return result;
}
@Override
protected String getCurrentOwnerFqn() {
return currentOwnerFqn;
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.*;
import lombok.Getter;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
/**
* Captures Fully Qualified Names (FQN) of classes as they will be imported in import statements.
* fqns map that is populated by this visitor is used to resolve Generic types.
*
* @param
*/
@Getter
public class JavaFqnCapturingVisitor
extends JavaIsoVisitor
{
// consider using ConcurrentHashMap to scale performance
// package -> name, FQN
private final Map> fqnMap = new HashMap<>();
private final Set fqns = new HashSet<>();
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {
captureClassDeclarations(classDecl, fqnMap);
return classDecl;
}
J.ClassDeclaration captureClassDeclarations(J.ClassDeclaration classDecl, Map> fqnMap) {
String fqn = classDecl.getType().getFullyQualifiedName();
fqns.add(fqn);
return classDecl;
}
String getPackage(String fqn) {
// handle no package
if (!fqn.contains(".")) {
return "";
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(0, lastIndex);
}
/**
*
* @param fqn
* @return Class name (including "$") after last period in FQN
*/
String getClassName(String fqn) {
// handle no package
if (!fqn.contains(".")) {
return fqn;
}
int lastIndex = fqn.lastIndexOf(".");
return fqn.substring(lastIndex + 1);
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.NameTree;
import org.openrewrite.java.tree.TypeTree;
@Slf4j
public class JavaMethodDeclarationVisitor extends BaseCodebaseVisitor
{
private final BaseTypeProcessor typeProcessor;
public JavaMethodDeclarationVisitor(DependencyCollector dependencyCollector) {
super(dependencyCollector);
this.typeProcessor = new BaseTypeProcessor() {
@Override
protected DependencyCollector getDependencyCollector() {
return dependencyCollector;
}
};
}
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P p) {
J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, p);
JavaType.Method methodType = methodDeclaration.getMethodType();
if (null == methodType) {
log.warn("MethodDeclaration has null methodType, skipping: {}", methodDeclaration.getSimpleName());
return methodDeclaration;
}
if (methodType.getDeclaringType() == null) {
log.warn("MethodDeclaration has null declaring type, skipping: {}", methodDeclaration.getSimpleName());
return methodDeclaration;
}
String owner = methodType.getDeclaringType().getFullyQualifiedName();
TypeTree returnTypeExpression = methodDeclaration.getReturnTypeExpression();
if (returnTypeExpression != null) {
JavaType returnType = returnTypeExpression.getType();
if (!(returnType instanceof JavaType.Primitive)) {
typeProcessor.processType(owner, returnType);
}
}
for (J.Annotation leadingAnnotation : methodDeclaration.getLeadingAnnotations()) {
typeProcessor.processAnnotation(owner, leadingAnnotation, getCursor());
}
if (null != methodDeclaration.getTypeParameters()) {
for (J.TypeParameter typeParameter : methodDeclaration.getTypeParameters()) {
typeProcessor.processTypeParameter(owner, typeParameter, getCursor());
}
}
List throwz = methodDeclaration.getThrows();
if (null != throwz && !throwz.isEmpty()) {
for (NameTree thrown : throwz) {
typeProcessor.processType(owner, thrown.getType());
}
}
return methodDeclaration;
}
@Override
protected String getCurrentOwnerFqn() {
return null;
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.java.tree.*;
@Slf4j
public class JavaVariableTypeVisitor extends BaseCodebaseVisitor
{
private final BaseTypeProcessor typeProcessor;
public JavaVariableTypeVisitor(DependencyCollector dependencyCollector) {
super(dependencyCollector);
this.typeProcessor = new BaseTypeProcessor() {
@Override
protected DependencyCollector getDependencyCollector() {
return dependencyCollector;
}
};
}
@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, P p) {
J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, p);
List variables = variableDeclarations.getVariables();
if (null == variables || variables.isEmpty() || null == variables.get(0).getVariableType()) {
log.debug("Skipping variable declaration with null variable type");
return variableDeclarations;
}
JavaType owner = variables.get(0).getVariableType().getOwner();
String ownerFqn = "";
if (owner instanceof JavaType.Method) {
JavaType.Method m = (JavaType.Method) owner;
if (m.getDeclaringType() == null) {
log.warn("Method owner has null declaring type, skipping variable declaration");
return variableDeclarations;
}
ownerFqn = m.getDeclaringType().getFullyQualifiedName();
} else if (owner instanceof JavaType.Class) {
JavaType.Class c = (JavaType.Class) owner;
ownerFqn = c.getFullyQualifiedName();
} else {
log.debug("Unknown owner type: {}", owner != null ? owner.getClass() : "null");
return variableDeclarations;
}
log.debug("Processing variable declaration in: {}", ownerFqn);
TypeTree typeTree = variableDeclarations.getTypeExpression();
JavaType javaType;
if (null != typeTree) {
javaType = typeTree.getType();
} else {
return variableDeclarations;
}
typeProcessor.processAnnotations(ownerFqn, getCursor());
if (javaType instanceof JavaType.Primitive) {
return variableDeclarations;
}
typeProcessor.processType(ownerFqn, javaType);
return variableDeclarations;
}
@Override
protected String getCurrentOwnerFqn() {
return null;
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hjug.graphbuilder.DependencyCollector;
import org.openrewrite.java.tree.*;
@Slf4j
public class JavaVisitor extends BaseCodebaseVisitor
{
@Getter
private final Map classToSourceFilePathMapping = new HashMap<>();
private final JavaClassDeclarationVisitor javaClassDeclarationVisitor;
public JavaVisitor(DependencyCollector dependencyCollector) {
super(dependencyCollector);
javaClassDeclarationVisitor = new JavaClassDeclarationVisitor<>(dependencyCollector);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {
return javaClassDeclarationVisitor.visitClassDeclaration(classDecl, p);
}
// Map each class to its source file
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, P p) {
J.CompilationUnit compilationUnit = super.visitCompilationUnit(cu, p);
J.Package packageDeclaration = compilationUnit.getPackageDeclaration();
if (null == packageDeclaration) {
return compilationUnit;
}
dependencyCollector.registerPackage(packageDeclaration.getPackageName());
for (J.ClassDeclaration aClass : compilationUnit.getClasses()) {
String classFqn = aClass.getType().getFullyQualifiedName();
String sourcePath = compilationUnit.getSourcePath().toUri().toString();
classToSourceFilePathMapping.put(classFqn, sourcePath);
dependencyCollector.recordClassLocation(classFqn, sourcePath);
}
return compilationUnit;
}
@Override
protected String getCurrentOwnerFqn() {
return null;
}
}
================================================
FILE: codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeDependencyExtractor.java
================================================
package org.hjug.graphbuilder.visitor;
import java.util.HashSet;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.openrewrite.java.tree.JavaType;
@Slf4j
public class TypeDependencyExtractor {
/**
* Extracts all type dependencies from a JavaType
*
* @param javaType The type to extract dependencies from
* @return Set of fully qualified type names that the given type depends on
*/
public Set extractDependencies(JavaType javaType) {
Set dependencies = new HashSet<>();
if (javaType == null) {
return dependencies;
}
extractDependenciesRecursive(javaType, dependencies);
return dependencies;
}
private void extractDependenciesRecursive(JavaType javaType, Set dependencies) {
if (javaType instanceof JavaType.Class) {
extractFromClass((JavaType.Class) javaType, dependencies);
} else if (javaType instanceof JavaType.Parameterized) {
extractFromParameterized((JavaType.Parameterized) javaType, dependencies);
} else if (javaType instanceof JavaType.GenericTypeVariable) {
extractFromGenericTypeVariable((JavaType.GenericTypeVariable) javaType, dependencies);
} else if (javaType instanceof JavaType.Array) {
extractFromArray((JavaType.Array) javaType, dependencies);
}
}
private void extractFromClass(JavaType.Class classType, Set dependencies) {
log.debug("Class type FQN: {}", classType.getFullyQualifiedName());
dependencies.add(classType.getFullyQualifiedName());
extractAnnotations(classType, dependencies);
}
private void extractFromParameterized(JavaType.Parameterized parameterized, Set dependencies) {
log.debug("Parameterized type FQN: {}", parameterized.getFullyQualifiedName());
dependencies.add(parameterized.getFullyQualifiedName());
extractAnnotations(parameterized, dependencies);
log.debug("Nested Parameterized type parameters: {}", parameterized.getTypeParameters());
for (JavaType parameter : parameterized.getTypeParameters()) {
extractDependenciesRecursive(parameter, dependencies);
}
}
private void extractFromArray(JavaType.Array arrayType, Set dependencies) {
log.debug("Array Element type: {}", arrayType.getElemType());
extractDependenciesRecursive(arrayType.getElemType(), dependencies);
}
private void extractFromGenericTypeVariable(JavaType.GenericTypeVariable typeVariable, Set dependencies) {
log.debug("Type parameter type name: {}", typeVariable.getName());
for (JavaType bound : typeVariable.getBounds()) {
if (bound instanceof JavaType.Class) {
dependencies.add(((JavaType.Class) bound).getFullyQualifiedName());
} else if (bound instanceof JavaType.Parameterized) {
dependencies.add(((JavaType.Parameterized) bound).getFullyQualifiedName());
} else {
log.debug("Unknown type bound: {}", bound);
}
}
}
private void extractAnnotations(JavaType.FullyQualified fullyQualified, Set dependencies) {
if (!fullyQualified.getAnnotations().isEmpty()) {
for (JavaType.FullyQualified annotation : fullyQualified.getAnnotations()) {
String annotationFqn = annotation.getFullyQualifiedName();
log.debug("Annotation FQN: {}", annotationFqn);
dependencies.add(annotationFqn);
}
}
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java
================================================
package org.hjug.graphbuilder;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class JavaGraphBuilderTest {
JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder();
@DisplayName("When source directory input param is empty or null throw IllegalArgumentException.")
@Test
void parseSourceDirectoryEmptyTest() {
Assertions.assertThrows(
IllegalArgumentException.class, () -> javaGraphBuilder.getCodebaseGraphDTO("", false, ""));
Assertions.assertThrows(
IllegalArgumentException.class, () -> javaGraphBuilder.getCodebaseGraphDTO(null, false, ""));
}
@DisplayName("Given a valid source directory input parameter return a valid graph.")
@Test
void parseSourceDirectoryTest() throws IOException {
File srcDirectory = new File("src/test/resources/javaSrcDirectory");
CodebaseGraphDTO dto = javaGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, "");
Graph classReferencesGraph = dto.getClassReferencesGraph();
assertNotNull(classReferencesGraph);
assertEquals(5, classReferencesGraph.vertexSet().size());
assertEquals(7, classReferencesGraph.edgeSet().size());
assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.A"));
assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.B"));
assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.C"));
assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.D"));
assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.E"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.A", "com.ideacrest.parser.testclasses.B"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.B", "com.ideacrest.parser.testclasses.C"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.A"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.E"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.A"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.C"));
assertTrue(classReferencesGraph.containsEdge(
"com.ideacrest.parser.testclasses.E", "com.ideacrest.parser.testclasses.D"));
// confirm edge weight calculations
assertEquals(
1,
getEdgeWeight(
classReferencesGraph,
"com.ideacrest.parser.testclasses.A",
"com.ideacrest.parser.testclasses.B"));
assertEquals(
2,
getEdgeWeight(
classReferencesGraph,
"com.ideacrest.parser.testclasses.E",
"com.ideacrest.parser.testclasses.D"));
}
private static double getEdgeWeight(
Graph classReferencesGraph, String sourceVertex, String targetVertex) {
return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex));
}
@Test
void removeClassesNotInCodebase() throws IOException {
File srcDirectory = new File("src/test/resources/javaSrcDirectory");
CodebaseGraphDTO dto = javaGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, "");
Graph classReferencesGraph = dto.getClassReferencesGraph();
classReferencesGraph.addVertex("org.favioriteoss.FunClass");
classReferencesGraph.addVertex("org.favioriteoss.AnotherFunClass");
DefaultWeightedEdge edge1 =
classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.FunClass");
DefaultWeightedEdge edge2 =
classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.AnotherFunClass");
assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.FunClass"));
assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass"));
Set packagesInCodebase = new HashSet<>();
packagesInCodebase.add("com.ideacrest.parser.testclasses");
javaGraphBuilder.removeClassesNotInCodebase(packagesInCodebase, classReferencesGraph);
assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.FunClass"));
assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass"));
assertFalse(classReferencesGraph.containsEdge(edge1));
assertFalse(classReferencesGraph.containsEdge(edge2));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaClassDeclarationVisitorTest {
@Test
void visitClasses() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses");
org.openrewrite.java.JavaParser javaParser =
JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor javaVariableCapturingVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
javaVariableCapturingVisitor.visit(cu, ctx);
});
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.B"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.C"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.D"));
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation"));
Assertions.assertFalse(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.E"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.F"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.G"));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
@Disabled
class JavaFqnCapturingVisitorTest {
@Test
void visitClasses() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses");
org.openrewrite.java.JavaParser javaParser =
JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
JavaFqnCapturingVisitor javaFqnCapturingVisitor = new JavaFqnCapturingVisitor();
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
javaFqnCapturingVisitor.visit(cu, ctx);
});
Map> fqns = javaFqnCapturingVisitor.getFqnMap();
Map processed = fqns.get("org.hjug.graphbuilder.visitor.testclasses");
Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.A", processed.get("A"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass", processed.get("A.InnerClass"));
Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.A.InnerClass", processed.get("InnerClass"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner",
processed.get("A.InnerClass.InnerInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner",
processed.get("InnerClass.InnerInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner", processed.get("InnerInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner",
processed.get("A.InnerClass.InnerInner.MegaInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner",
processed.get("InnerClass.InnerInner.MegaInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner",
processed.get("InnerInner.MegaInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner",
processed.get("MegaInner"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass", processed.get("A.StaticInnerClass"));
Assertions.assertEquals(
"org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass", processed.get("StaticInnerClass"));
Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.NonPublic", processed.get("NonPublic"));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaInitializerBlockVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaInitializerBlockVisitorTest {
@Test
void visitInstanceInitializerBlocks() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
});
// Verify that the test class is in the graph
Assertions.assertTrue(
classReferencesGraph.containsVertex(
"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass"),
"InitializerBlockTestClass should be in the graph");
// Verify ArrayList is captured from instance initializer block: new ArrayList<>()
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.util.ArrayList"),
"ArrayList should be captured from instance initializer block");
// Verify edge from InitializerBlockTestClass to ArrayList exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass",
"java.util.ArrayList"),
"Should have edge from InitializerBlockTestClass to ArrayList from initializer block");
// Verify HashMap is captured from instance initializer block: new HashMap<>()
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.util.HashMap"),
"HashMap should be captured from instance initializer block");
// Verify edge from InitializerBlockTestClass to HashMap exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass",
"java.util.HashMap"),
"Should have edge from InitializerBlockTestClass to HashMap from initializer block");
// Verify StringBuilder is captured from instance initializer block: new StringBuilder()
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.lang.StringBuilder"),
"StringBuilder should be captured from instance initializer block");
// Verify edge from InitializerBlockTestClass to StringBuilder exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass",
"java.lang.StringBuilder"),
"Should have edge from InitializerBlockTestClass to StringBuilder from initializer block");
}
@Test
void visitStaticInitializerBlocks() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
});
// Verify that the complex test class is in the graph
Assertions.assertTrue(
classReferencesGraph.containsVertex(
"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass"),
"ComplexInitializerClass should be in the graph");
// Verify ConcurrentHashMap is captured from static initializer block
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.util.concurrent.ConcurrentHashMap"),
"ConcurrentHashMap should be captured from static initializer block");
// Verify edge from ComplexInitializerClass to ConcurrentHashMap exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass",
"java.util.concurrent.ConcurrentHashMap"),
"Should have edge from ComplexInitializerClass to ConcurrentHashMap from static initializer");
// Verify AtomicInteger is captured from static initializer block
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.util.concurrent.atomic.AtomicInteger"),
"AtomicInteger should be captured from static initializer block");
// Verify edge from ComplexInitializerClass to AtomicInteger exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass",
"java.util.concurrent.atomic.AtomicInteger"),
"Should have edge from ComplexInitializerClass to AtomicInteger from static initializer");
// Verify nested classes are captured from instance initializer
Assertions.assertTrue(
classReferencesGraph.containsVertex(
"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$DataProcessor"),
"DataProcessor nested class should be captured from instance initializer");
Assertions.assertTrue(
classReferencesGraph.containsVertex(
"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$HelperService"),
"HelperService nested class should be captured from instance initializer");
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaLambdaVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaLambdaVisitorTest {
@Test
void visitLambdaBodiesRecursively() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
});
// Verify that the main test class is in the graph
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"),
"LambdaTestClass should be in the graph");
// Verify that HelperClass is captured as a dependency
// This is from field declaration AND from lambda body: helper.process(item)
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"),
"HelperClass should be captured from lambda body method invocation");
// Verify edge from LambdaTestClass to HelperClass exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"),
"Should have edge from LambdaTestClass to HelperClass");
// Verify that DataProcessor is captured from lambda body: new DataProcessor()
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"),
"DataProcessor should be captured from new class instantiation in lambda body");
// Verify edge from LambdaTestClass to DataProcessor exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"),
"Should have edge from LambdaTestClass to DataProcessor from lambda body");
// Verify that StringBuilder is captured from lambda body: new StringBuilder(s)
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.lang.StringBuilder"),
"StringBuilder should be captured from new class instantiation in lambda body");
// Verify edge from LambdaTestClass to StringBuilder exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass", "java.lang.StringBuilder"),
"Should have edge from LambdaTestClass to StringBuilder from lambda body");
// Verify that String is captured (from method invocations like s.toUpperCase())
Assertions.assertTrue(
classReferencesGraph.containsVertex("java.lang.String"),
"String should be captured from method invocations in lambda body");
// Verify edge weight - multiple lambda usages should increase edge weight
DefaultWeightedEdge edge = classReferencesGraph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor");
// DataProcessor is used twice: once in processWithLambda() and once in lambdaWithLocalVariable()
Assertions.assertTrue(
classReferencesGraph.getEdgeWeight(edge) >= 2.0,
"Edge weight should reflect multiple uses of DataProcessor in lambda bodies");
}
@Test
void visitNestedLambdaBodiesRecursively() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
});
// Verify that the nested lambda test class is in the graph
Assertions.assertTrue(
classReferencesGraph.containsVertex(
"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"),
"NestedLambdaTestClass should be in the graph");
// Verify DataProcessor is captured from INNER lambda: new DataProcessor() inside nested lambda
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"),
"DataProcessor should be captured from inner nested lambda body");
// Verify edge from NestedLambdaTestClass to DataProcessor exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"),
"Should have edge from NestedLambdaTestClass to DataProcessor from nested lambda");
// Verify HelperClass is captured from nested lambda method invocation
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"),
"HelperClass should be captured from nested lambda method invocation");
// Verify edge from NestedLambdaTestClass to HelperClass exists
Assertions.assertTrue(
classReferencesGraph.containsEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"),
"Should have edge from NestedLambdaTestClass to HelperClass from nested lambda");
// Verify edge weight reflects multiple nested lambda usages
DefaultWeightedEdge dataProcessorEdge = classReferencesGraph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor");
// DataProcessor is used in multiple nested lambdas: processNestedLambdas() and deeplyNestedLambdaWithNewClass()
Assertions.assertTrue(
classReferencesGraph.getEdgeWeight(dataProcessorEdge) >= 2.0,
"Edge weight should reflect multiple uses of DataProcessor in nested lambda bodies");
// Verify that deeply nested instantiations are captured
DefaultWeightedEdge helperEdge = classReferencesGraph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass",
"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass");
// HelperClass is used in field declaration and in nested lambdas
Assertions.assertTrue(
classReferencesGraph.getEdgeWeight(helperEdge) >= 2.0,
"Edge weight should reflect HelperClass usage in nested lambda blocks");
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaMethodDeclarationVisitorTest {
@Test
void visitMethodDeclarations() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses");
org.openrewrite.java.JavaParser javaParser =
JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaMethodDeclarationVisitor methodDeclarationVisitor =
new JavaMethodDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
methodDeclarationVisitor.visit(cu, ctx);
});
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A"));
// TODO: Assert stuff
/* Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.A"));
Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.B"));
Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.C"));
Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.D"));
Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.MyAnnotation"));
Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.E"));
Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.F"));
Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.G"));*/
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaMethodInvocationVisitorTest {
@Test
void visitMethodInvocations() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
JavaVariableTypeVisitor variableTypeVisitor =
new JavaVariableTypeVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
variableTypeVisitor.visit(cu, ctx);
});
Graph graph = classReferencesGraph;
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C"));
Assertions.assertEquals(
3,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A",
"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B")));
Assertions.assertEquals(
3,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A",
"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C")));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
public class JavaNewClassVisitorFullTest {
@Test
void visitNewClass() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
final JavaVisitor javaVisitor = new JavaVisitor<>(dependencyCollector);
final JavaVariableTypeVisitor javaVariableTypeVisitor =
new JavaVariableTypeVisitor<>(dependencyCollector);
final JavaMethodDeclarationVisitor javaMethodDeclarationVisitor =
new JavaMethodDeclarationVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
javaVisitor.visit(cu, ctx);
javaVariableTypeVisitor.visit(cu, ctx);
javaMethodDeclarationVisitor.visit(cu, ctx);
});
Graph graph = classReferencesGraph;
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.A"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.B"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.C"));
// capturing counts of all types
Assertions.assertEquals(
6,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.newClass.A",
"org.hjug.graphbuilder.visitor.testclasses.newClass.B")));
Assertions.assertEquals(
3,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.newClass.A",
"org.hjug.graphbuilder.visitor.testclasses.newClass.C")));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
public class JavaNewClassVisitorTest {
@Test
void visitNewClass() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass");
JavaParser javaParser = JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaClassDeclarationVisitor classDeclarationVisitor =
new JavaClassDeclarationVisitor<>(dependencyCollector);
JavaVariableTypeVisitor variableTypeVisitor =
new JavaVariableTypeVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
classDeclarationVisitor.visit(cu, ctx);
variableTypeVisitor.visit(cu, ctx);
});
Graph graph = classReferencesGraph;
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.A"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.B"));
Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.C"));
// only looking for what was visited by classDeclarationVisitor and variableTypeVisitor
Assertions.assertEquals(
5,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.newClass.A",
"org.hjug.graphbuilder.visitor.testclasses.newClass.B")));
Assertions.assertEquals(
3,
graph.getEdgeWeight(graph.getEdge(
"org.hjug.graphbuilder.visitor.testclasses.newClass.A",
"org.hjug.graphbuilder.visitor.testclasses.newClass.C")));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaVariableTypeVisitorTest {
@Test
void visitClasses() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses");
org.openrewrite.java.JavaParser javaParser =
JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
Graph classReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
Graph packageReferencesGraph =
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
JavaVariableTypeVisitor javaVariableCapturingVisitor =
new JavaVariableTypeVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
javaVariableCapturingVisitor.visit(cu, ctx);
});
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.B"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.C"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.D"));
Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.E"));
Assertions.assertTrue(
classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation"));
Assertions.assertFalse(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.F"));
Assertions.assertFalse(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.G"));
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java
================================================
package org.hjug.graphbuilder.visitor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.hjug.graphbuilder.GraphDependencyCollector;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.junit.jupiter.api.Test;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
class JavaVisitorTest {
@Test
void visitClasses() throws IOException {
File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses");
org.openrewrite.java.JavaParser javaParser =
JavaParser.fromJavaVersion().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);
final Graph classReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
final Graph packageReferencesGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
final GraphDependencyCollector dependencyCollector =
new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);
final JavaVisitor javaVisitor = new JavaVisitor<>(dependencyCollector);
List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());
javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {
System.out.println(cu.getSourcePath());
javaVisitor.visit(cu, ctx);
});
assertEquals(5, dependencyCollector.getPackagesInCodebase().size());
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/A.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
import java.util.List;
import java.util.Map;
@MyAnnotation
public class A {
// public A(B cB, C cC){}
B extends C> crazyType;
@MyAnnotation
@MyOtherAnnotation
int intVar, intVar2;
@MyAnnotation
@MyOtherAnnotation
C rawC;
B b, b3;
C c;
D[] ds;
D d;
@MyAnnotation
B[] arrayOfGenericBsWithCTypeParam;
@MyAnnotation
B bWithArrayOfCs;
List> listWithNestedGenric;
Map map;
List extends List extends Number>> listOfListsOfNumbers;
@MyAnnotation
F doSomething(B paramB, C genericParam) {
List> list3;
A a2;
B b2;
C c2;
H h = new H();
B.invocationTest(h);
return new G();
}
class InnerClass {
class InnerInner {
class MegaInner {
D d;
}
}
}
static class StaticInnerClass {}
}
class NonPublic {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/B.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class B {
static D invocationTest(T type) {
return new D();
}
static class InnerB extends A {}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/C.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class C {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/D.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class D {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/E.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public interface E {
void foo(A a);
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/F.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class F {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/G.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class G extends F {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/H.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
public class H extends B {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyAnnotation.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE_USE)
@interface MyAnnotation {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyOtherAnnotation.java
================================================
package org.hjug.graphbuilder.visitor.testclasses;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE_USE)
@interface MyOtherAnnotation {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/ComplexInitializerClass.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.initializers;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ComplexInitializerClass {
private static ConcurrentHashMap staticCache;
private static AtomicInteger instanceCounter;
private DataProcessor processor;
private HelperService helper;
// Static initializer with new class instantiations
static {
staticCache = new ConcurrentHashMap<>();
instanceCounter = new AtomicInteger(0);
staticCache.put("initialized", "true");
}
// Instance initializer with dependencies
{
processor = new DataProcessor();
helper = new HelperService();
instanceCounter.incrementAndGet();
}
// Another static initializer
static {
staticCache.put("version", "1.0");
}
public void process() {
processor.execute();
}
static class DataProcessor {
public void execute() {}
}
static class HelperService {}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/InitializerBlockTestClass.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.initializers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InitializerBlockTestClass {
private List items;
private Map counters;
private StringBuilder builder;
// Instance initializer block
{
items = new ArrayList<>();
counters = new HashMap<>();
builder = new StringBuilder("Initialized");
}
// Static initializer block
static {
System.out.println("Static initializer");
}
// Another instance initializer block with method invocations
{
items.add("default");
counters.put("default", 0);
builder.append(" with defaults");
}
public InitializerBlockTestClass() {
// Constructor
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/DataProcessor.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.lambda;
public class DataProcessor {
public String transform(String data) {
return data;
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/HelperClass.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.lambda;
public class HelperClass {
public String process(String input) {
return input.toUpperCase();
}
public static String staticProcess(String input) {
return input.toLowerCase();
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/LambdaTestClass.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.lambda;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class LambdaTestClass {
private List items = new ArrayList<>();
private HelperClass helper = new HelperClass();
public void processWithLambda() {
// Lambda with method invocation on helper class
items.forEach(item -> helper.process(item));
// Lambda with multiple method invocations
items.stream().map(s -> s.toUpperCase()).filter(s -> s.length() > 5).collect(Collectors.toList());
// Lambda with new class instantiation - creates dependency on DataProcessor
items.stream().map(s -> new DataProcessor().transform(s)).collect(Collectors.toList());
// Lambda with new StringBuilder instantiation
items.stream().map(s -> new StringBuilder(s)).collect(Collectors.toList());
// Nested lambda
items.stream()
.map(s -> s.chars().mapToObj(c -> String.valueOf((char) c)).collect(Collectors.joining()))
.collect(Collectors.toList());
// Lambda with static method reference
items.stream().map(HelperClass::staticProcess).forEach(System.out::println);
// Lambda with type cast
items.stream().map(s -> (CharSequence) s).collect(Collectors.toList());
}
public void lambdaWithLocalVariable() {
items.forEach(item -> {
DataProcessor processor = new DataProcessor();
String processed = processor.transform(item);
System.out.println(processed);
});
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/NestedLambdaTestClass.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.lambda;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class NestedLambdaTestClass {
private List> nestedItems = new ArrayList<>();
private HelperClass helper = new HelperClass();
public void processNestedLambdas() {
// Nested lambda with DataProcessor instantiation in inner lambda
nestedItems.stream()
.map(innerList -> innerList.stream()
.map(s -> new DataProcessor().transform(s))
.collect(Collectors.toList()))
.collect(Collectors.toList());
// Nested lambda with HelperClass method invocation in inner lambda
nestedItems.stream()
.flatMap(innerList -> innerList.stream().map(s -> helper.process(s)))
.collect(Collectors.toList());
// Triple nested lambda with multiple dependencies
nestedItems.stream()
.map(outerList -> outerList.stream()
.map(middleItem -> middleItem
.chars()
.mapToObj(c -> new DataProcessor().transform(String.valueOf((char) c)))
.collect(Collectors.joining()))
.collect(Collectors.toList()))
.collect(Collectors.toList());
}
public void deeplyNestedLambdaWithNewClass() {
// Deeply nested lambda creating new instances at each level
nestedItems.stream()
.map(level1 -> {
DataProcessor processor1 = new DataProcessor();
return level1.stream()
.map(level2 -> {
HelperClass helper2 = new HelperClass();
return helper2.process(processor1.transform(level2));
})
.collect(Collectors.toList());
})
.collect(Collectors.toList());
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/A.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;
public class A {
A doSomething() {
B.invocationTest(new D());
A a = B.invocationTest(new D());
// TODO: add visitor for J.ReturnType
return B.invocationTest(new D());
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/B.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;
public class B {
static A invocationTest(T type) {
return new A();
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/C.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;
public class C extends B {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/D.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;
public class D extends C {}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/A.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.newClass;
import java.util.ArrayList;
import java.util.List;
public class A {
B newClassMethod() {
new C();
C c = new C();
// var treated like "B", counts as 2
var b = new B(null);
// <> treated like , counts as 2
List listB = new ArrayList<>();
// TODO: add visitor for J.ReturnType
return new B(c);
}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/B.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.newClass;
public class B {
public B(C c) {}
}
================================================
FILE: codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/C.java
================================================
package org.hjug.graphbuilder.visitor.testclasses.newClass;
public class C {}
================================================
FILE: codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java
================================================
package com.ideacrest.parser.testclasses;
public class A {
B b;
}
================================================
FILE: codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java
================================================
package com.ideacrest.parser.testclasses;
public class B {
C c;
}
================================================
FILE: codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java
================================================
package com.ideacrest.parser.testclasses;
public class C {
A a;
E e;
}
================================================
FILE: codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java
================================================
package com.ideacrest.parser.testclasses;
public class D {
A a;
C c;
}
================================================
FILE: codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java
================================================
package com.ideacrest.parser.testclasses;
public class E {
D d;
D d2;
}
================================================
FILE: cost-benefit-calculator/pom.xml
================================================
4.0.0
org.hjug.refactorfirst
refactor-first
0.8.1-SNAPSHOT
org.hjug.refactorfirst.costbenefitcalculator
cost-benefit-calculator
RefactorFirst Cost Benefit Calculator
org.slf4j
slf4j-api
org.hjug.refactorfirst.codebasegraphbuilder
codebase-graph-builder
org.hjug.refactorfirst.changepronenessranker
change-proneness-ranker
org.hjug.refactorfirst.effortranker
effort-ranker
org.hjug.refactorfirst.dsm
graph-algorithms
org.hjug.refactorfirst.testresources
test-resources
================================================
FILE: cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java
================================================
package org.hjug.cbc;
import static net.sourceforge.pmd.RuleViolation.CLASS_NAME;
import static net.sourceforge.pmd.RuleViolation.PACKAGE_NAME;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.pmd.*;
import net.sourceforge.pmd.lang.LanguageRegistry;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.hjug.git.ChangePronenessRanker;
import org.hjug.git.GitLogReader;
import org.hjug.git.ScmLogInfo;
import org.hjug.metrics.*;
import org.hjug.metrics.rules.CBORule;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
@Slf4j
public class CostBenefitCalculator implements AutoCloseable {
private Report report;
private final String repositoryPath;
private GitLogReader gitLogReader;
private final ChangePronenessRanker changePronenessRanker;
private final Map classToSourceFilePathMapping;
public CostBenefitCalculator(String repositoryPath, Map classToSourceFilePathMapping) {
this.repositoryPath = repositoryPath;
log.info("Initiating Cost Benefit calculation");
try {
gitLogReader = new GitLogReader(new File(repositoryPath));
} catch (IOException e) {
log.error("Failure to access Git repository", e);
}
changePronenessRanker = new ChangePronenessRanker(gitLogReader);
this.classToSourceFilePathMapping = classToSourceFilePathMapping;
}
@Override
public void close() throws Exception {
gitLogReader.close();
}
// copied from PMD's PmdTaskImpl.java and modified
public void runPmdAnalysis() throws IOException {
PMDConfiguration configuration = new PMDConfiguration();
try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
loadRules(pmd);
try (Stream files = Files.walk(Paths.get(repositoryPath))) {
files.filter(Files::isRegularFile).forEach(file -> pmd.files().addFile(file));
}
report = pmd.performAnalysisAndCollectReport();
}
}
public void runPmdAnalysis(boolean excludeTests, String testSourceDirectory) throws IOException {
PMDConfiguration configuration = new PMDConfiguration();
try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
loadRules(pmd);
try (Stream files = Files.walk(Paths.get(repositoryPath))) {
Stream pathStream;
if (excludeTests) {
pathStream = files.filter(Files::isRegularFile)
.filter(file -> !file.toString().contains(testSourceDirectory));
} else {
pathStream = files.filter(Files::isRegularFile);
}
pathStream.forEach(file -> pmd.files().addFile(file));
}
report = pmd.performAnalysisAndCollectReport();
}
}
private void loadRules(PmdAnalysis pmd) {
RuleSetLoader rulesetLoader = pmd.newRuleSetLoader();
pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(List.of("category/java/design.xml")));
Rule cboClassRule = new CBORule();
cboClassRule.setLanguage(LanguageRegistry.PMD.getLanguageByFullName("Java"));
pmd.addRuleSet(RuleSet.forSingleRule(cboClassRule));
log.info("files to be scanned: " + Paths.get(repositoryPath));
}
public List calculateGodClassCostBenefitValues() {
List godClasses = getGodClasses();
List scmLogInfos = getRankedChangeProneness(godClasses);
Map rankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);
List rankedDisharmonies = godClasses.stream()
.filter(godClass -> rankedLogInfosByPath.containsKey(godClass.getFileName()))
.map(godClass -> new RankedDisharmony(godClass, rankedLogInfosByPath.get(godClass.getFileName())))
.sorted(Comparator.comparing(RankedDisharmony::getRawPriority).reversed())
.collect(Collectors.toList());
int godClassPriority = 1;
for (RankedDisharmony rankedGodClassDisharmony : rankedDisharmonies) {
rankedGodClassDisharmony.setPriority(godClassPriority++);
}
return rankedDisharmonies;
}
private static Map getRankedLogInfosByPath(List scmLogInfos) {
return scmLogInfos.stream().collect(Collectors.toMap(ScmLogInfo::getPath, logInfo -> logInfo, (a, b) -> b));
}
private static Map getRankedLogInfosByClass(List scmLogInfos) {
return scmLogInfos.stream()
.collect(Collectors.toMap(ScmLogInfo::getClassName, logInfo -> logInfo, (a, b) -> b));
}
private List getGodClasses() {
List godClasses = new ArrayList<>();
for (RuleViolation violation : report.getViolations()) {
if (violation.getRule().getName().contains("GodClass")) {
GodClass godClass = new GodClass(
violation.getAdditionalInfo().get(CLASS_NAME),
getFileName(violation),
violation.getAdditionalInfo().get(PACKAGE_NAME),
violation.getDescription());
log.info("God Class identified: {}", godClass.getFileName());
godClasses.add(godClass);
}
}
GodClassRanker godClassRanker = new GodClassRanker();
godClassRanker.rankGodClasses(godClasses);
return godClasses;
}
public List getRankedChangeProneness(List disharmonies) {
log.info("Calculating Change Proneness");
Map innerClassPaths = new ConcurrentHashMap<>();
Map scmLogInfosByPath = new ConcurrentHashMap<>();
List> scmLogInfos = disharmonies.parallelStream()
.map(disharmony -> {
String className = disharmony.getClassName();
String path = null;
ScmLogInfo scmLogInfo = null;
try {
if (className.contains("$")
&& classToSourceFilePathMapping.containsKey(
className.substring(0, className.indexOf("$")))) {
path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf("$")));
log.debug("Found source file {} for nested class: {}", path, className);
innerClassPaths.put(className, path);
} else {
path = disharmony.getFileName();
try {
log.debug("Reading scmLogInfo for {}", path);
scmLogInfo = gitLogReader.fileLog(path);
scmLogInfo.setClassName(className);
log.debug("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath());
scmLogInfosByPath.put(path, scmLogInfo);
} catch (GitAPIException | IOException e) {
log.error("Error reading Git repository contents.", e);
}
}
} catch (NullPointerException e) {
// Should not be reached
log.error(
"Error looking up class SCM info. If this error is encountered, "
+ "please log a bug on the RefactorFirst project and describe if the class is a nested class, lambda, etc. \nClass: {}, Path: {}",
className,
path,
e);
}
Optional scmLogInfoOptional = Optional.ofNullable(scmLogInfo);
if (scmLogInfoOptional.isEmpty()) {
log.warn("No scmLogInfo found for class: {} at path: {}", className, path);
}
return scmLogInfoOptional;
})
.collect(Collectors.toList());
List> innerClassScmLogInfos = innerClassPaths.entrySet().parallelStream()
.map(innerClassPathEntry -> {
ScmLogInfo scmLogInfo = scmLogInfosByPath.get(innerClassPathEntry.getValue());
ScmLogInfo innerClassScmLogInfo = null;
if (scmLogInfo == null) {
String className = innerClassPathEntry.getKey();
String path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf("$")));
log.debug("Reading scmLogInfo for inner class {}", canonicaliseURIStringForRepoLookup(path));
try {
innerClassScmLogInfo = gitLogReader.fileLog(canonicaliseURIStringForRepoLookup(path));
innerClassScmLogInfo.setClassName(className);
log.debug(
"Successfully fetched scmLogInfo for inner class {} at {}",
innerClassScmLogInfo.getClassName(),
innerClassScmLogInfo.getPath());
scmLogInfosByPath.put(path, innerClassScmLogInfo);
} catch (GitAPIException | IOException e) {
log.error(
"Error reading Git repository contents for class {} with file path {}",
className,
path,
e);
}
} else {
innerClassScmLogInfo = new ScmLogInfo(
innerClassPathEntry.getValue(),
innerClassPathEntry.getKey(),
scmLogInfo.getEarliestCommit(),
scmLogInfo.getMostRecentCommit(),
scmLogInfo.getCommitCount());
String className = innerClassPathEntry.getKey();
innerClassScmLogInfo.setClassName(className);
String path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf("$")));
scmLogInfosByPath.put(path, innerClassScmLogInfo);
}
return Optional.ofNullable(innerClassScmLogInfo);
})
.collect(Collectors.toList());
scmLogInfos.addAll(innerClassScmLogInfos);
List sortedScmInfos = new ArrayList<>(scmLogInfos.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()));
changePronenessRanker.rankChangeProneness(sortedScmInfos);
return sortedScmInfos;
}
public List calculateCBOCostBenefitValues() {
List cboClasses = getCBOClasses();
List scmLogInfos = getRankedChangeProneness(cboClasses);
Map rankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);
for (Map.Entry stringScmLogInfoEntry : rankedLogInfosByPath.entrySet()) {
log.debug(
"ScmLogInfo entry: {} path: {}",
stringScmLogInfoEntry.getKey(),
stringScmLogInfoEntry.getValue().getPath());
}
List rankedDisharmonies = new ArrayList<>();
for (CBOClass cboClass : cboClasses) {
log.debug("CBO Class identified: {}", cboClass.getFileName());
log.debug(
"ScmLogInfo: {}",
rankedLogInfosByPath.get(cboClass.getFileName()).getPath());
rankedDisharmonies.add(new RankedDisharmony(cboClass, rankedLogInfosByPath.get(cboClass.getFileName())));
}
rankedDisharmonies.sort(
Comparator.comparing(RankedDisharmony::getRawPriority).reversed());
int cboPriority = 1;
for (RankedDisharmony rankedCBODisharmony : rankedDisharmonies) {
rankedCBODisharmony.setPriority(cboPriority++);
}
return rankedDisharmonies;
}
private List getCBOClasses() {
List cboClasses = new ArrayList<>();
for (RuleViolation violation : report.getViolations()) {
if (violation.getRule().getName().contains("CBORule")) {
log.info(violation.getDescription());
CBOClass godClass = new CBOClass(
violation.getAdditionalInfo().get(CLASS_NAME),
getFileName(violation),
violation.getAdditionalInfo().get(PACKAGE_NAME),
violation.getDescription());
log.debug("Highly Coupled class identified: {}", godClass.getFileName());
cboClasses.add(godClass);
}
}
return cboClasses;
}
public List calculateSourceNodeCostBenefitValues(
Graph classGraph,
Map edgeSourceNodeInfos,
Map edgeTargetNodeInfos,
Map edgeToRemoveCycleCounts,
Set vertexesToRemove) {
List sourceLogInfos = getRankedChangeProneness(new ArrayList<>(edgeSourceNodeInfos.values()));
List targetLogInfos = getRankedChangeProneness(new ArrayList<>(edgeTargetNodeInfos.values()));
List scmLogInfos = new ArrayList<>(sourceLogInfos.size() + targetLogInfos.size());
scmLogInfos.addAll(sourceLogInfos);
scmLogInfos.addAll(targetLogInfos);
Map sourceRankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);
List edgesThatNeedToBeRemoved = new ArrayList<>();
for (Map.Entry entry : edgeSourceNodeInfos.entrySet()) {
String edgeSource = classGraph.getEdgeSource(entry.getKey());
String edgeSourcePath;
if (edgeSource.contains("$")) {
edgeSourcePath = classToSourceFilePathMapping.get(edgeSource.substring(0, edgeSource.indexOf("$")));
} else {
edgeSourcePath = classToSourceFilePathMapping.get(edgeSource);
}
String edgeTarget = classGraph.getEdgeTarget(entry.getKey());
String edgeTargetPath;
if (edgeTarget.contains("$")) {
edgeTargetPath = classToSourceFilePathMapping.get(edgeTarget.substring(0, edgeTarget.indexOf("$")));
} else {
edgeTargetPath = classToSourceFilePathMapping.get(edgeTarget);
}
String sourceNodeFileName = canonicaliseURIStringForRepoLookup(edgeSourcePath);
String targetNodeFileName = canonicaliseURIStringForRepoLookup(edgeTargetPath);
boolean sourceNodeShouldBeRemoved = vertexesToRemove.contains(edgeSource);
boolean targetNodeShouldBeRemoved = vertexesToRemove.contains(edgeTarget);
ScmLogInfo sourceScmLogInfo = null;
if (sourceRankedLogInfosByPath.containsKey(sourceNodeFileName)) {
sourceScmLogInfo = sourceRankedLogInfosByPath.get(sourceNodeFileName);
}
ScmLogInfo targetScmLogInfo = null;
if (sourceRankedLogInfosByPath.containsKey(sourceNodeFileName)) {
targetScmLogInfo = sourceRankedLogInfosByPath.get(targetNodeFileName);
}
RankedDisharmony edgeThatNeedsToBeRemoved = new RankedDisharmony(
edgeSource,
entry.getKey(),
edgeToRemoveCycleCounts.get(entry.getKey()),
(int) classGraph.getEdgeWeight(entry.getKey()),
sourceNodeShouldBeRemoved,
targetNodeShouldBeRemoved,
sourceScmLogInfo,
targetScmLogInfo);
edgesThatNeedToBeRemoved.add(edgeThatNeedsToBeRemoved);
}
sortEdgesThatNeedToBeRemoved(edgesThatNeedToBeRemoved);
// Then subtract edge weight
int rawPriority = 1;
for (RankedDisharmony rankedDisharmony : edgesThatNeedToBeRemoved) {
rankedDisharmony.setRawPriority(rawPriority++);
}
// Push edges with higher weights down in the priority list
edgesThatNeedToBeRemoved.sort(Comparator.comparing(RankedDisharmony::getRawPriority));
// Then set priority
int sourceNodePriority = 1;
for (RankedDisharmony rankedSourceNodeDisharmony : edgesThatNeedToBeRemoved) {
rankedSourceNodeDisharmony.setPriority(sourceNodePriority++);
}
return edgesThatNeedToBeRemoved;
}
static void sortEdgesThatNeedToBeRemoved(List rankedDisharmonies) {
// Sort by impact value
// Order by cycle count reversed (highest count bubbles to the top)
rankedDisharmonies.sort(Comparator.comparingInt(RankedDisharmony::getCycleCount)
.reversed()
// then by weight, with lowest weight edges bubbling to the top
.thenComparingInt(RankedDisharmony::getEffortRank)
// then by change proneness
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getChangePronenessRank())
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getEdgeTargetChangePronenessRank())
// then if the source node is in the list of nodes to be removed
// multiplying by -1 reverses the sort order (reverse doesn't work in chained comparators)
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getSourceNodeShouldBeRemoved())
// then if the target node is in the list of nodes to be removed
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getTargetNodeShouldBeRemoved()));
}
private String getFileName(RuleViolation violation) {
String uriString = violation.getFileId().getUriString();
return canonicaliseURIStringForRepoLookup(uriString);
}
String canonicaliseURIStringForRepoLookup(String uriString) {
if (repositoryPath.startsWith("/") || repositoryPath.startsWith("\\")) {
return uriString.replace("file://" + repositoryPath.replace("\\", "/") + "/", "");
}
return uriString.replace("file:///" + repositoryPath.replace("\\", "/") + "/", "");
}
}
================================================
FILE: cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleNode.java
================================================
package org.hjug.cbc;
import java.time.Instant;
import lombok.Data;
import org.hjug.git.ScmLogInfo;
import org.hjug.metrics.Disharmony;
@Data
public class CycleNode implements Disharmony {
private final String className;
private String fileName;
private Integer changePronenessRank;
private Instant firstCommitTime;
private Instant mostRecentCommitTime;
private Integer commitCount;
public CycleNode(String className, String fileName) {
this.className = className;
this.fileName = fileName;
}
public String getPackageName() {
return className.substring(0, className.lastIndexOf('.'));
}
public void setScmLogInfo(ScmLogInfo scmLogInfo) {
firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());
mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());
commitCount = scmLogInfo.getCommitCount();
}
}
================================================
FILE: cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java
================================================
package org.hjug.cbc;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hjug.dsm.CircularReferenceChecker;
import org.hjug.graphbuilder.CodebaseGraphDTO;
import org.hjug.graphbuilder.JavaGraphBuilder;
import org.jgrapht.Graph;
import org.jgrapht.graph.AsSubgraph;
import org.jgrapht.graph.DefaultWeightedEdge;
@RequiredArgsConstructor
@Slf4j
public class CycleRanker {
private final String repositoryPath;
private final JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder();
@Getter
private Graph classReferencesGraph;
@Getter
private CodebaseGraphDTO codebaseGraphDTO;
@Getter
private Map classNamesAndPaths = new HashMap<>();
@Getter
private Map fqnsAndPaths = new HashMap<>();
public void generateClassReferencesGraph(boolean excludeTests, String testSourceDirectory) {
try {
codebaseGraphDTO = javaGraphBuilder.getCodebaseGraphDTO(repositoryPath, excludeTests, testSourceDirectory);
classReferencesGraph = codebaseGraphDTO.getClassReferencesGraph();
loadClassNamesAndPaths();
/*for (Map.Entry stringStringEntry : fqnsAndPaths.entrySet()) {
log.info(stringStringEntry.getKey() + " : " + stringStringEntry.getValue());
}*/
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public List performCycleAnalysis(boolean excludeTests, String testSourceDirectory) {
List rankedCycles = new ArrayList<>();
try {
boolean calculateCycleChurn = false;
generateClassReferencesGraph(excludeTests, testSourceDirectory);
identifyRankedCycles(rankedCycles);
sortRankedCycles(rankedCycles, calculateCycleChurn);
setPriorities(rankedCycles);
} catch (IOException e) {
throw new RuntimeException(e);
}
return rankedCycles;
}
private void identifyRankedCycles(List rankedCycles) throws IOException {
CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker();
Map> cycles =
circularReferenceChecker.getCycles(classReferencesGraph);
cycles.forEach((vertex, subGraph) -> {
// TODO: Calculate min cuts for smaller graphs - has a runtime of O(V^4) for a graph
/*Set minCutEdges;
GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree =
new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph));
double minCut = gusfieldGomoryHuCutTree.calculateMinCut();
minCutEdges = gusfieldGomoryHuCutTree.getCutEdges();*/
List cycleNodes = subGraph.vertexSet().stream()
.map(classInCycle -> new CycleNode(classInCycle, classNamesAndPaths.get(classInCycle)))
// .peek(cycleNode -> log.info(cycleNode.toString()))
.collect(Collectors.toList());
rankedCycles.add(createRankedCycle(vertex, subGraph, cycleNodes, 0.0, new HashSet<>()));
});
}
public CycleNode classToCycleNode(String fqnClass) {
return new CycleNode(fqnClass, fqnsAndPaths.get(fqnClass));
}
private RankedCycle createRankedCycle(
String vertex,
AsSubgraph subGraph,
List cycleNodes,
double minCut,
Set minCutEdges) {
return new RankedCycle(vertex, subGraph.vertexSet(), subGraph.edgeSet(), minCut, minCutEdges, cycleNodes);
}
private static void sortRankedCycles(List rankedCycles, boolean calculateChurnForCycles) {
if (calculateChurnForCycles) {
rankedCycles.sort(Comparator.comparing(RankedCycle::getAverageChangeProneness));
int cpr = 1;
for (RankedCycle rankedCycle : rankedCycles) {
rankedCycle.setChangePronenessRank(cpr++);
}
} else {
rankedCycles.sort(Comparator.comparing(RankedCycle::getRawPriority).reversed());
}
}
private static void setPriorities(List rankedCycles) {
int priority = 1;
for (RankedCycle rankedCycle : rankedCycles) {
rankedCycle.setPriority(priority++);
}
}
void loadClassNamesAndPaths() throws IOException {
try (Stream walk = Files.walk(Paths.get(repositoryPath))) {
walk.forEach(path -> {
String filename = path.getFileName().toString();
if (filename.endsWith(".java")) {
// extract package and class name
String packageName = getPackageName(path);
String uriString = path.toUri().toString();
String className = getClassName(filename);
String canonicalUri = canonicaliseURIStringForRepoLookup(uriString);
fqnsAndPaths.put(packageName + "." + className, canonicalUri);
classNamesAndPaths.put(className, canonicalUri);
}
});
}
}
private static String getPackageName(Path path) {
try {
return Files.readAllLines(path).stream()
.filter(line -> line.startsWith("package"))
.map(line -> line.replace("package", "").replace(";", "").trim())
.findFirst()
.orElse("");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String canonicaliseURIStringForRepoLookup(String uriString) {
if (repositoryPath.startsWith("/") || repositoryPath.startsWith("\\")) {
return uriString.replace("file://" + repositoryPath.replace("\\", "/") + "/", "");
}
return uriString.replace("file:///" + repositoryPath.replace("\\", "/") + "/", "");
}
/**
* Extract class name from java file name
* Example : MyJavaClass.java becomes MyJavaClass
*
* @param javaFileName
* @return
*/
private String getClassName(String javaFileName) {
return javaFileName.substring(0, javaFileName.indexOf('.'));
}
}
================================================
FILE: cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedCycle.java
================================================
package org.hjug.cbc;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.jgrapht.graph.DefaultWeightedEdge;
@Data
@Slf4j
public class RankedCycle {
private final String cycleName;
private Integer changePronenessRankSum = 0;
private final Set vertexSet;
private final Set edgeSet;
private final double minCutCount;
private final Set minCutEdges;
private final List cycleNodes;
private float rawPriority;
private Integer priority = 0;
private float averageChangeProneness;
private Integer changePronenessRank = 0;
private float impact;
public RankedCycle(
String cycleName,
Set vertexSet,
Set edgeSet,
double minCutCount,
Set minCutEdges,
List cycleNodes) {
this.cycleNodes = cycleNodes;
this.cycleName = cycleName;
this.vertexSet = vertexSet;
this.edgeSet = edgeSet;
this.minCutCount = minCutCount;
if (null == minCutEdges) {
this.minCutEdges = new HashSet<>();
} else {
this.minCutEdges = minCutEdges;
}
if (minCutCount == 0.0) {
this.impact = (float) (vertexSet.size());
} else {
this.impact = (float) (vertexSet.size() / minCutCount);
}
this.rawPriority = this.impact;
}
public RankedCycle(
String cycleName,
Integer changePronenessRankSum,
Set vertexSet,
Set edgeSet,
double minCutCount,
Set minCutEdges,
List cycleNodes) {
this.cycleNodes = cycleNodes;
this.cycleName = cycleName;
this.changePronenessRankSum = changePronenessRankSum;
this.vertexSet = vertexSet;
this.edgeSet = edgeSet;
this.minCutCount = minCutCount;
if (null == minCutEdges) {
this.minCutEdges = new HashSet<>();
} else {
this.minCutEdges = minCutEdges;
}
if (minCutCount == 0.0) {
this.impact = (float) (vertexSet.size());
} else {
this.impact = (float) (vertexSet.size() / minCutCount);
}
this.averageChangeProneness = (float) changePronenessRankSum / vertexSet.size();
this.rawPriority = this.impact + averageChangeProneness;
}
}
================================================
FILE: cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java
================================================
package org.hjug.cbc;
import java.nio.file.Paths;
import java.time.Instant;
import lombok.Data;
import org.hjug.git.ScmLogInfo;
import org.hjug.metrics.CBOClass;
import org.hjug.metrics.GodClass;
import org.jgrapht.graph.DefaultWeightedEdge;
@Data
public class RankedDisharmony {
private Instant firstCommitTime;
private Instant mostRecentCommitTime;
private Integer commitCount;
private String path;
private String fileName;
private final String className;
private final Integer effortRank;
private final Integer changePronenessRank;
private Integer rawPriority;
private Integer priority = 0;
private Integer wmc;
private Integer wmcRank;
private Integer atfd;
private Integer atfdRank;
private Float tcc;
private Integer tccRank;
private DefaultWeightedEdge edge;
private Integer cycleCount;
private int sourceNodeShouldBeRemoved;
private int targetNodeShouldBeRemoved;
private String edgeTargetClass;
private Integer edgeTargetChangePronenessRank;
public RankedDisharmony(GodClass godClass, ScmLogInfo scmLogInfo) {
path = scmLogInfo.getPath();
// from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java
fileName = Paths.get(path).getFileName().toString();
className = godClass.getClassName();
changePronenessRank = scmLogInfo.getChangePronenessRank();
effortRank = godClass.getOverallRank();
rawPriority = changePronenessRank - effortRank;
wmc = godClass.getWmc();
wmcRank = godClass.getWmcRank();
atfd = godClass.getAtfd();
atfdRank = godClass.getAtfdRank();
tcc = godClass.getTcc();
tccRank = godClass.getTccRank();
firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());
mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());
commitCount = scmLogInfo.getCommitCount();
}
public RankedDisharmony(CBOClass cboClass, ScmLogInfo scmLogInfo) {
path = scmLogInfo.getPath();
// from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java
fileName = Paths.get(path).getFileName().toString();
className = cboClass.getClassName();
changePronenessRank = scmLogInfo.getChangePronenessRank();
effortRank = cboClass.getCouplingCount();
rawPriority = changePronenessRank - effortRank;
firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());
mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());
commitCount = scmLogInfo.getCommitCount();
}
public RankedDisharmony(
String edgeSource,
DefaultWeightedEdge edge,
int cycleCount,
int weight,
boolean sourceNodeShouldBeRemoved,
boolean targetNodeShouldBeRemoved,
ScmLogInfo sourceScmLogInfo,
ScmLogInfo targetScmLogInfo) {
if (null != sourceScmLogInfo) {
path = sourceScmLogInfo.getPath();
// from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java
fileName = Paths.get(path).getFileName().toString();
firstCommitTime = Instant.ofEpochSecond(sourceScmLogInfo.getEarliestCommit());
mostRecentCommitTime = Instant.ofEpochSecond(sourceScmLogInfo.getMostRecentCommit());
commitCount = sourceScmLogInfo.getCommitCount();
}
className = edgeSource;
this.edge = edge;
this.cycleCount = cycleCount;
changePronenessRank = null == sourceScmLogInfo ? 0 : sourceScmLogInfo.getChangePronenessRank();
edgeTargetChangePronenessRank = null == targetScmLogInfo ? 0 : targetScmLogInfo.getChangePronenessRank();
effortRank = weight;
this.sourceNodeShouldBeRemoved = sourceNodeShouldBeRemoved ? 1 : 0;
this.targetNodeShouldBeRemoved = targetNodeShouldBeRemoved ? 1 : 0;
}
}
================================================
FILE: cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java
================================================
package org.hjug.cbc;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.*;
import java.util.*;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.hjug.git.ScmLogInfo;
import org.hjug.metrics.Disharmony;
import org.jetbrains.annotations.NotNull;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
class CostBenefitCalculatorTest {
@TempDir
public File tempFolder;
private String faceletsPath = "org/apache/myfaces/tobago/facelets/";
private String hudsonPath = "hudson/model/";
private Git git;
private Repository repository;
@BeforeEach
public void setUp() throws GitAPIException {
git = Git.init().setDirectory(tempFolder).call();
repository = git.getRepository();
new File(tempFolder.getPath() + "/" + faceletsPath).mkdirs();
new File(tempFolder.getPath() + "/" + hudsonPath).mkdirs();
}
@AfterEach
public void tearDown() {
repository.close();
}
@Test
void testCBOViolation() throws IOException, GitAPIException, InterruptedException {
// Has CBO violation
String user = "User.java";
InputStream userResourceAsStream = getClass().getClassLoader().getResourceAsStream(hudsonPath + user);
writeFile(hudsonPath + user, convertInputStreamToString(userResourceAsStream));
git.add().addFilepattern(".").call();
RevCommit firstCommit = git.commit().setMessage("message").call();
CostBenefitCalculator costBenefitCalculator =
new CostBenefitCalculator(git.getRepository().getDirectory().getParent(), new HashMap<>());
costBenefitCalculator.runPmdAnalysis();
List disharmonies = costBenefitCalculator.calculateCBOCostBenefitValues();
Assertions.assertFalse(disharmonies.isEmpty());
}
@Test
void testCostBenefitCalculation() throws IOException, GitAPIException, InterruptedException {
String attributeHandler = "AttributeHandler.java";
InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(faceletsPath + attributeHandler);
writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream));
git.add().addFilepattern(".").call();
RevCommit firstCommit = git.commit().setMessage("message").call();
// Sleeping for one second to guarantee commits have different time stamps
Thread.sleep(1000);
// write contents of updated file to original file
InputStream resourceAsStream2 =
getClass().getClassLoader().getResourceAsStream(faceletsPath + "AttributeHandler2.java");
writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream2));
InputStream resourceAsStream3 =
getClass().getClassLoader().getResourceAsStream(faceletsPath + "AttributeHandlerAndSorter.java");
writeFile(faceletsPath + "AttributeHandlerAndSorter.java", convertInputStreamToString(resourceAsStream3));
git.add().addFilepattern(".").call();
RevCommit secondCommit = git.commit().setMessage("message").call();
CostBenefitCalculator costBenefitCalculator =
new CostBenefitCalculator(git.getRepository().getDirectory().getParent(), new HashMap<>());
costBenefitCalculator.runPmdAnalysis();
List disharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues();
Assertions.assertEquals(1, disharmonies.get(0).getRawPriority().intValue());
Assertions.assertEquals(1, disharmonies.get(1).getRawPriority().intValue());
Assertions.assertEquals(1, disharmonies.get(0).getPriority().intValue());
Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue());
}
@Test
void calculateSourceNodeCostBenefitValues_filtersMissingLogInfoAndAssignsPriority() throws Exception {
writeFile(hudsonPath + "Dummy.java", "public class Dummy {}");
git.add().addFilepattern(".").call();
git.commit().setMessage("initial commit").call();
SimpleDirectedWeightedGraph classGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
classGraph.addVertex("ClassA");
classGraph.addVertex("ClassB");
classGraph.addVertex("ClassC");
classGraph.addVertex("ClassD");
classGraph.addVertex("ClassE");
classGraph.addVertex("ClassF");
DefaultWeightedEdge edge1 = classGraph.addEdge("ClassA", "ClassB");
classGraph.setEdgeWeight(edge1, 4);
DefaultWeightedEdge edge2 = classGraph.addEdge("ClassC", "ClassD");
classGraph.setEdgeWeight(edge2, 2);
Map edgeSourceNodeInfos = new HashMap<>();
edgeSourceNodeInfos.put(edge1, new CycleNode("ClassA", hudsonPath + "ClassA.java"));
edgeSourceNodeInfos.put(edge2, new CycleNode("ClassC", hudsonPath + "ClassC.java"));
Map edgeTargeteNodeInfos = new HashMap<>();
edgeTargeteNodeInfos.put(edge1, new CycleNode("ClassB", hudsonPath + "ClassB.java"));
edgeTargeteNodeInfos.put(edge2, new CycleNode("ClassD", hudsonPath + "ClassD.java"));
Map edgeToRemoveCycleCounts = new HashMap<>();
edgeToRemoveCycleCounts.put(edge1, 5);
edgeToRemoveCycleCounts.put(edge2, 3);
Set vertexesToRemove = new HashSet<>(Arrays.asList("ClassA", "ClassD"));
ScmLogInfo scmLogInfo1 = new ScmLogInfo(hudsonPath + "ClassA.java", null, 1, 2, 3);
scmLogInfo1.setChangePronenessRank(4);
ScmLogInfo scmLogInfo2 = new ScmLogInfo(hudsonPath + "ClassC.java", null, 1, 2, 5);
scmLogInfo2.setChangePronenessRank(7);
List scmLogInfos = Arrays.asList(scmLogInfo1, scmLogInfo2);
try (TestableCostBenefitCalculator costBenefitCalculator = new TestableCostBenefitCalculator(
git.getRepository().getDirectory().getParent(), scmLogInfos)) {
List disharmonies = costBenefitCalculator.calculateSourceNodeCostBenefitValues(
classGraph, edgeTargeteNodeInfos, edgeTargeteNodeInfos, edgeToRemoveCycleCounts, vertexesToRemove);
Assertions.assertEquals(2, disharmonies.size());
RankedDisharmony classA = disharmonies.get(0);
Assertions.assertEquals("ClassA", classA.getClassName());
Assertions.assertEquals(5, classA.getCycleCount().intValue());
Assertions.assertEquals(4, classA.getEffortRank().intValue());
Assertions.assertEquals(1, classA.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(0, classA.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(1, classA.getPriority().intValue());
RankedDisharmony classC = disharmonies.get(1);
Assertions.assertEquals("ClassC", classC.getClassName());
Assertions.assertEquals(3, classC.getCycleCount().intValue());
Assertions.assertEquals(2, classC.getEffortRank().intValue());
Assertions.assertEquals(0, classC.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(1, classC.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(2, classC.getPriority().intValue());
Assertions.assertEquals(7, classC.getChangePronenessRank());
}
}
@Test
void calculateSourceNodeCostBenefitValues_prefersHigherChangePronenessRank() throws Exception {
writeFile(faceletsPath + "Placeholder.java", "public class Placeholder {}");
git.add().addFilepattern(".").call();
git.commit().setMessage("initial commit").call();
SimpleDirectedWeightedGraph classGraph =
new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
classGraph.addVertex("Alpha");
classGraph.addVertex("Beta");
classGraph.addVertex("Gamma");
DefaultWeightedEdge edge1 = classGraph.addEdge("Alpha", "Beta");
classGraph.setEdgeWeight(edge1, 3);
DefaultWeightedEdge edge2 = classGraph.addEdge("Gamma", "Beta");
classGraph.setEdgeWeight(edge2, 3);
Map edgeSourceNodeInfos = new HashMap<>();
edgeSourceNodeInfos.put(edge1, new CycleNode("Alpha", faceletsPath + "Alpha.java"));
edgeSourceNodeInfos.put(edge2, new CycleNode("Gamma", faceletsPath + "Gamma.java"));
Map edgeTargetNodeInfos = new HashMap<>();
edgeTargetNodeInfos.put(edge1, new CycleNode("Beta", faceletsPath + "Beta.java"));
Map edgeToRemoveCycleCounts = new HashMap<>();
edgeToRemoveCycleCounts.put(edge1, 4);
edgeToRemoveCycleCounts.put(edge2, 4);
Set vertexesToRemove = new HashSet<>(Arrays.asList("Alpha", "Gamma"));
ScmLogInfo scmLogInfo1 = new ScmLogInfo(faceletsPath + "Alpha.java", null, 2, 3, 1);
scmLogInfo1.setChangePronenessRank(2);
ScmLogInfo scmLogInfo2 = new ScmLogInfo(faceletsPath + "Gamma.java", null, 2, 3, 1);
scmLogInfo2.setChangePronenessRank(8);
List scmLogInfos = Arrays.asList(scmLogInfo1, scmLogInfo2);
try (TestableCostBenefitCalculator costBenefitCalculator = new TestableCostBenefitCalculator(
git.getRepository().getDirectory().getParent(), scmLogInfos)) {
List disharmonies = costBenefitCalculator.calculateSourceNodeCostBenefitValues(
classGraph, edgeSourceNodeInfos, edgeTargetNodeInfos, edgeToRemoveCycleCounts, vertexesToRemove);
Assertions.assertEquals(2, disharmonies.size());
Assertions.assertEquals(8, disharmonies.get(0).getChangePronenessRank());
Assertions.assertEquals(1, disharmonies.get(0).getPriority().intValue());
Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue());
Assertions.assertEquals(2, disharmonies.get(1).getChangePronenessRank());
}
}
@Test
void sortEdgesThatNeedToBeRemoved_sortsByMultipleCriteria() {
// Create ScmLogInfo objects for testing
ScmLogInfo logInfo1 = new ScmLogInfo("path1.java", null, 1, 2, 3);
logInfo1.setChangePronenessRank(5);
ScmLogInfo logInfo2 = new ScmLogInfo("path2.java", null, 1, 2, 3);
logInfo2.setChangePronenessRank(3);
ScmLogInfo logInfo3 = new ScmLogInfo("path3.java", null, 1, 2, 3);
logInfo3.setChangePronenessRank(8);
ScmLogInfo logInfo4 = new ScmLogInfo("path4.java", null, 1, 2, 3);
logInfo4.setChangePronenessRank(2);
ScmLogInfo logInfo5 = new ScmLogInfo("path4.java", null, 1, 2, 3);
logInfo5.setChangePronenessRank(5);
// Create RankedDisharmony objects with different combinations
// Expected order after sorting: cycleCount desc, then sourceRemoved desc, then targetRemoved desc, then
// changeProneness desc
// cycle=5, source=0, target=0, change=5
RankedDisharmony disharmony1 = new RankedDisharmony(
"Class1", new org.jgrapht.graph.DefaultWeightedEdge(), 5, 1, false, false, logInfo1, null);
// cycle=5, source=1, target=0, change=3
RankedDisharmony disharmony2 = new RankedDisharmony(
"Class2", new org.jgrapht.graph.DefaultWeightedEdge(), 5, 1, true, false, logInfo2, null);
// cycle=3, source=0, target=1, change=8
RankedDisharmony disharmony3 = new RankedDisharmony(
"Class3", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, true, logInfo3, null);
// cycle=3, source=0, target=0, change=2
RankedDisharmony disharmony4 = new RankedDisharmony(
"Class4", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, false, logInfo4, null);
// cycle=3, source=0, target=0, change=5
RankedDisharmony disharmony5 = new RankedDisharmony(
"Class5", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, false, logInfo5, null);
List disharmonies =
Arrays.asList(disharmony4, disharmony2, disharmony1, disharmony3, disharmony5);
// Sort the list
CostBenefitCalculator.sortEdgesThatNeedToBeRemoved(disharmonies);
// Verify the order
// Order by cycle count reversed (highest count bubbles to the top)
// then Order by source node removed (source nodes needing to be removed bubble to the top)
// then Order by target node removed (target nodes needing to be removed bubble to the top)\
// then Order by change proneness (highest change proneness bubbles to the top)
for (RankedDisharmony disharmony : disharmonies) {
System.out.println(disharmony.getClassName() + " "
+ disharmony.getCycleCount() + " "
+ disharmony.getEffortRank() + " "
+ disharmony.getSourceNodeShouldBeRemoved() + " "
+ disharmony.getTargetNodeShouldBeRemoved() + " "
+ disharmony.getChangePronenessRank());
}
RankedDisharmony orderedDisharmony0 = disharmonies.get(0);
Assertions.assertEquals("Class1", orderedDisharmony0.getClassName());
Assertions.assertEquals(5, orderedDisharmony0.getCycleCount().intValue());
Assertions.assertEquals(1, orderedDisharmony0.getEffortRank().intValue());
Assertions.assertEquals(0, orderedDisharmony0.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(0, orderedDisharmony0.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(5, orderedDisharmony0.getChangePronenessRank());
RankedDisharmony orderedDisharmony1 = disharmonies.get(1);
Assertions.assertEquals("Class2", orderedDisharmony1.getClassName());
Assertions.assertEquals(5, orderedDisharmony1.getCycleCount().intValue());
Assertions.assertEquals(1, orderedDisharmony1.getEffortRank().intValue());
Assertions.assertEquals(1, orderedDisharmony1.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(0, orderedDisharmony1.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(3, orderedDisharmony1.getChangePronenessRank());
RankedDisharmony orderedDisharmony2 = disharmonies.get(2);
Assertions.assertEquals("Class3", orderedDisharmony2.getClassName());
Assertions.assertEquals(3, orderedDisharmony2.getCycleCount().intValue());
Assertions.assertEquals(1, orderedDisharmony2.getEffortRank().intValue());
Assertions.assertEquals(0, orderedDisharmony2.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(1, orderedDisharmony2.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(8, orderedDisharmony2.getChangePronenessRank());
RankedDisharmony orderedDisharmony3 = disharmonies.get(3);
Assertions.assertEquals("Class5", orderedDisharmony3.getClassName());
Assertions.assertEquals(3, orderedDisharmony3.getCycleCount().intValue());
Assertions.assertEquals(1, orderedDisharmony3.getEffortRank().intValue());
Assertions.assertEquals(0, orderedDisharmony3.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(0, orderedDisharmony3.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(5, orderedDisharmony3.getChangePronenessRank());
RankedDisharmony orderedDisharmony4 = disharmonies.get(4);
Assertions.assertEquals("Class4", orderedDisharmony4.getClassName());
Assertions.assertEquals(1, orderedDisharmony4.getEffortRank().intValue());
Assertions.assertEquals(3, orderedDisharmony4.getCycleCount().intValue());
Assertions.assertEquals(0, orderedDisharmony4.getSourceNodeShouldBeRemoved());
Assertions.assertEquals(0, orderedDisharmony4.getTargetNodeShouldBeRemoved());
Assertions.assertEquals(2, orderedDisharmony4.getChangePronenessRank());
}
private void writeFile(String name, String content) throws IOException {
// Files.writeString(Path.of(git.getRepository().getWorkTree().getPath()), content);
File file = new File(git.getRepository().getWorkTree(), name);
try (FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(content.getBytes(UTF_8));
}
}
private String convertInputStreamToString(InputStream inputStream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
return result.toString("UTF-8");
}
private static class TestableCostBenefitCalculator extends CostBenefitCalculator {
private final List scmLogInfos;
TestableCostBenefitCalculator(String repositoryPath, List scmLogInfos) {
super(repositoryPath, getClassToSourceFilePathMapping());
this.scmLogInfos = scmLogInfos;
}
private static @NotNull Map getClassToSourceFilePathMapping() {
Map classToSourceFilePathMapping = new HashMap<>();
classToSourceFilePathMapping.put("Alpha", "org/apache/myfaces/tobago/facelets/Alpha.java");
classToSourceFilePathMapping.put("Beta", "org/apache/myfaces/tobago/facelets/Beta.java");
classToSourceFilePathMapping.put("Gamma", "org/apache/myfaces/tobago/facelets/Gamma.java");
classToSourceFilePathMapping.put("ClassA", "hudson/model/ClassA.java");
classToSourceFilePathMapping.put("ClassB", "hudson/model/ClassB.java");
classToSourceFilePathMapping.put("ClassC", "hudson/model/ClassC.java");
classToSourceFilePathMapping.put("ClassD", "hudson/model/ClassD.java");
return classToSourceFilePathMapping;
}
@Override
public List getRankedChangeProneness(List disharmonies) {
return new ArrayList<>(scmLogInfos);
}
}
}
================================================
FILE: cost-benefit-calculator/src/test/resources/hudson/model/User.java
================================================
/*
* The MIT License
*
* Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
* Tom Huybrechts, Vincent Latombe, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.SecurityRealm;
import hudson.security.UserMayOrMayNotExistException2;
import hudson.util.FormApply;
import hudson.util.FormValidation;
import hudson.util.RunList;
import hudson.util.XStream2;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.IdStrategy;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import jenkins.scm.RunWithSCM;
import jenkins.security.ImpersonatingUserDetailsService2;
import jenkins.security.LastGrantedAuthoritiesProperty;
import jenkins.security.UserDetailsCache;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.POST;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* Represents a user.
*
*