diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/DependencyCollector.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/DependencyCollector.java new file mode 100644 index 00000000..bb48d0c9 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/DependencyCollector.java @@ -0,0 +1,35 @@ +package org.hjug.graphbuilder; + +public interface DependencyCollector { + + /** + * Records a dependency from one class to another + * + * @param fromClassFqn The fully qualified name of the class that depends on another + * @param toClassFqn The fully qualified name of the class being depended upon + */ + void addClassDependency(String fromClassFqn, String toClassFqn); + + /** + * Records a dependency from one package to another + * + * @param fromPackageName The package that depends on another + * @param toPackageName The package being depended upon + */ + void addPackageDependency(String fromPackageName, String toPackageName); + + /** + * Records the source file location for a class + * + * @param classFqn The fully qualified name of the class + * @param sourceFilePath The path to the source file containing the class + */ + void recordClassLocation(String classFqn, String sourceFilePath); + + /** + * Registers a package as being part of the codebase + * + * @param packageName The package name to register + */ + void registerPackage(String packageName); +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphBuilderConfig.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphBuilderConfig.java new file mode 100644 index 00000000..272e6416 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphBuilderConfig.java @@ -0,0 +1,19 @@ +package org.hjug.graphbuilder; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class GraphBuilderConfig { + + @Builder.Default + boolean excludeTests = true; + + @Builder.Default + String testSourceDirectory = "src/test"; + + public static GraphBuilderConfig defaultConfig() { + return GraphBuilderConfig.builder().build(); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java new file mode 100644 index 00000000..ca15faf0 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java @@ -0,0 +1,70 @@ +package org.hjug.graphbuilder; + +import java.util.HashSet; +import java.util.Set; +import lombok.Getter; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; + +public class GraphDependencyCollector implements DependencyCollector { + + @Getter + private final Graph classReferencesGraph; + + @Getter + private final Graph packageReferencesGraph; + + @Getter + private final Set packagesInCodebase = new HashSet<>(); + + public GraphDependencyCollector( + Graph classReferencesGraph, + Graph packageReferencesGraph) { + this.classReferencesGraph = classReferencesGraph; + this.packageReferencesGraph = packageReferencesGraph; + } + + @Override + public void addClassDependency(String fromClassFqn, String toClassFqn) { + if (fromClassFqn.equals(toClassFqn)) { + return; + } + + classReferencesGraph.addVertex(fromClassFqn); + classReferencesGraph.addVertex(toClassFqn); + + if (!classReferencesGraph.containsEdge(fromClassFqn, toClassFqn)) { + classReferencesGraph.addEdge(fromClassFqn, toClassFqn); + } else { + DefaultWeightedEdge edge = classReferencesGraph.getEdge(fromClassFqn, toClassFqn); + classReferencesGraph.setEdgeWeight(edge, classReferencesGraph.getEdgeWeight(edge) + 1); + } + } + + @Override + public void addPackageDependency(String fromPackageName, String toPackageName) { + if (fromPackageName.equals(toPackageName)) { + return; + } + + packageReferencesGraph.addVertex(fromPackageName); + packageReferencesGraph.addVertex(toPackageName); + + if (!packageReferencesGraph.containsEdge(fromPackageName, toPackageName)) { + packageReferencesGraph.addEdge(fromPackageName, toPackageName); + } else { + DefaultWeightedEdge edge = packageReferencesGraph.getEdge(fromPackageName, toPackageName); + packageReferencesGraph.setEdgeWeight(edge, packageReferencesGraph.getEdgeWeight(edge) + 1); + } + } + + @Override + public void recordClassLocation(String classFqn, String sourceFilePath) { + // This will be handled by JavaVisitor which maintains the mapping + } + + @Override + public void registerPackage(String packageName) { + packagesInCodebase.add(packageName); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java index 1ef24ed5..33724c49 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java @@ -23,26 +23,39 @@ public class JavaGraphBuilder { /** - * Given a java source directory, return a CodebaseGraphDTO + * Given a java source directory, return a CodebaseGraphDTO using default configuration * - * @param srcDirectory + * @param srcDirectory The source directory to analyze + * @param excludeTests Whether to exclude test files + * @param testSourceDirectory The test source directory pattern to exclude * @return CodebaseGraphDTO * @throws IOException */ public CodebaseGraphDTO getCodebaseGraphDTO(String srcDirectory, boolean excludeTests, String testSourceDirectory) throws IOException { - CodebaseGraphDTO codebaseGraphDTO; + GraphBuilderConfig config = GraphBuilderConfig.builder() + .excludeTests(excludeTests) + .testSourceDirectory(testSourceDirectory) + .build(); + return getCodebaseGraphDTO(srcDirectory, config); + } + + /** + * Given a java source directory and configuration, return a CodebaseGraphDTO + * + * @param srcDirectory The source directory to analyze + * @param config The configuration for the graph builder + * @return CodebaseGraphDTO + * @throws IOException + */ + public CodebaseGraphDTO getCodebaseGraphDTO(String srcDirectory, GraphBuilderConfig config) throws IOException { if (srcDirectory == null || srcDirectory.isEmpty()) { - throw new IllegalArgumentException(); - } else { - codebaseGraphDTO = processWithOpenRewrite(srcDirectory, excludeTests, testSourceDirectory); + throw new IllegalArgumentException("Source directory cannot be null or empty"); } - - return codebaseGraphDTO; + return processWithOpenRewrite(srcDirectory, config); } - private CodebaseGraphDTO processWithOpenRewrite(String srcDir, boolean excludeTests, String testSourceDirectory) - throws IOException { + private CodebaseGraphDTO processWithOpenRewrite(String srcDir, GraphBuilderConfig config) throws IOException { File srcDirectory = new File(srcDir); JavaParser javaParser = JavaParser.fromJavaVersion().build(); @@ -50,22 +63,23 @@ private CodebaseGraphDTO processWithOpenRewrite(String srcDir, boolean excludeTe final Graph classReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final Graph packageReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final JavaVisitor javaVisitor = - new JavaVisitor<>(classReferencesGraph, packageReferencesGraph); + final GraphDependencyCollector dependencyCollector = + new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph); + + final JavaVisitor javaVisitor = new JavaVisitor<>(dependencyCollector); final JavaVariableTypeVisitor javaVariableTypeVisitor = - new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaVariableTypeVisitor<>(dependencyCollector); final JavaMethodDeclarationVisitor javaMethodDeclarationVisitor = - new JavaMethodDeclarationVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaMethodDeclarationVisitor<>(dependencyCollector); try (Stream pathStream = Files.walk(Paths.get(srcDirectory.getAbsolutePath()))) { List list; - if (excludeTests) { + if (config.isExcludeTests()) { list = pathStream - .filter(file -> !file.toString().contains(testSourceDirectory)) + .filter(file -> !file.toString().contains(config.getTestSourceDirectory())) .collect(Collectors.toList()); } else { list = pathStream.collect(Collectors.toList()); @@ -80,7 +94,7 @@ private CodebaseGraphDTO processWithOpenRewrite(String srcDir, boolean excludeTe }); } - removeClassesNotInCodebase(javaVisitor.getPackagesInCodebase(), classReferencesGraph); + removeClassesNotInCodebase(dependencyCollector.getPackagesInCodebase(), classReferencesGraph); return new CodebaseGraphDTO( classReferencesGraph, packageReferencesGraph, javaVisitor.getClassToSourceFilePathMapping()); diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseCodebaseVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseCodebaseVisitor.java new file mode 100644 index 00000000..4e5d676b --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseCodebaseVisitor.java @@ -0,0 +1,17 @@ +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(); +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseTypeProcessor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseTypeProcessor.java new file mode 100644 index 00000000..f4affd0e --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseTypeProcessor.java @@ -0,0 +1,76 @@ +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); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java index a87c393d..c6b07c3b 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java @@ -1,118 +1,198 @@ package org.hjug.graphbuilder.visitor; import java.util.List; -import lombok.Getter; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.SimpleDirectedWeightedGraph; -import org.openrewrite.java.JavaIsoVisitor; +import lombok.extern.slf4j.Slf4j; +import org.hjug.graphbuilder.DependencyCollector; import org.openrewrite.java.tree.*; -public class JavaClassDeclarationVisitor

extends JavaIsoVisitor

implements TypeProcessor { +@Slf4j +public class JavaClassDeclarationVisitor

extends BaseCodebaseVisitor

{ - private final JavaMethodInvocationVisitor methodInvocationVisitor; - private final JavaNewClassVisitor newClassVisitor; + private final BaseTypeProcessor typeProcessor; + private String currentOwnerFqn; - @Getter - private Graph classReferencesGraph = - new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - - public JavaClassDeclarationVisitor() { - methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); - newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); - } - - public JavaClassDeclarationVisitor(Graph classReferencesGraph) { - this.classReferencesGraph = classReferencesGraph; - methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); - newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); + 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) { - J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, p); + JavaType.FullyQualified type = classDecl.getType(); + if (type == null) { + log.warn("ClassDeclaration has null type, skipping: {}", classDecl.getSimpleName()); + return classDecl; + } - JavaType.FullyQualified type = classDeclaration.getType(); String owningFqn = type.getFullyQualifiedName(); + String previousOwner = currentOwnerFqn; + currentOwnerFqn = owningFqn; - processType(owningFqn, type); + try { + typeProcessor.processType(owningFqn, type); - TypeTree extendsTypeTree = classDeclaration.getExtends(); - if (null != extendsTypeTree) { - processType(owningFqn, extendsTypeTree.getType()); - } + 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()); + } - List implementsTypeTree = classDeclaration.getImplements(); - if (null != implementsTypeTree) { - for (TypeTree typeTree : implementsTypeTree) { - processType(owningFqn, typeTree.getType()); + if (null != classDecl.getTypeParameters()) { + for (J.TypeParameter typeParameter : classDecl.getTypeParameters()) { + typeProcessor.processTypeParameter(owningFqn, typeParameter, getCursor()); + } } + + return super.visitClassDeclaration(classDecl, p); + } finally { + currentOwnerFqn = previousOwner; } + } - for (J.Annotation leadingAnnotation : classDeclaration.getLeadingAnnotations()) { - processAnnotation(owningFqn, leadingAnnotation); + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P p) { + J.MethodInvocation methodInvocation = super.visitMethodInvocation(method, p); + if (currentOwnerFqn == null) { + return methodInvocation; } - if (null != classDeclaration.getTypeParameters()) { - for (J.TypeParameter typeParameter : classDeclaration.getTypeParameters()) { - processTypeParameter(owningFqn, typeParameter); - } + JavaType.Method methodType = methodInvocation.getMethodType(); + if (null != methodType && null != methodType.getDeclaringType()) { + typeProcessor.processType(currentOwnerFqn, methodType.getDeclaringType()); } - // process method invocations and lambda invocations - processInvocations(classDeclaration); + if (null != methodInvocation.getTypeParameters() + && !methodInvocation.getTypeParameters().isEmpty()) { + for (Expression typeParameter : methodInvocation.getTypeParameters()) { + typeProcessor.processType(currentOwnerFqn, typeParameter.getType()); + } + } - return classDeclaration; + return methodInvocation; } - private void processInvocations(J.ClassDeclaration classDeclaration) { - JavaType.FullyQualified type = classDeclaration.getType(); - String owningFqn = type.getFullyQualifiedName(); + @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; + } - for (Statement statement : classDeclaration.getBody().getStatements()) { - if (statement instanceof J.Block) { - processBlock((J.Block) statement, owningFqn); - } - if (statement instanceof J.MethodDeclaration) { - J.MethodDeclaration methodDeclaration = (J.MethodDeclaration) statement; - processBlock(methodDeclaration.getBody(), owningFqn); - } + @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); } - private void processBlock(J.Block block, String owningFqn) { - if (null != block && null != block.getStatements()) { - for (Statement statementInBlock : block.getStatements()) { - if (statementInBlock instanceof J.MethodInvocation) { - J.MethodInvocation methodInvocation = (J.MethodInvocation) statementInBlock; - methodInvocationVisitor.visitMethodInvocation(owningFqn, methodInvocation); - } else if (statementInBlock instanceof J.Lambda) { - J.Lambda lambda = (J.Lambda) statementInBlock; - processType(owningFqn, lambda.getType()); - } else if (statementInBlock instanceof J.NewClass) { - J.NewClass newClass = (J.NewClass) statementInBlock; - newClassVisitor.visitNewClass(owningFqn, newClass); - } else if (statementInBlock instanceof J.Return) { - J.Return returnStmt = (J.Return) statementInBlock; - visitReturn(owningFqn, returnStmt); + @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; } - public J.Return visitReturn(String owningFqn, J.Return visitedReturn) { - Expression expression = visitedReturn.getExpression(); - if (expression instanceof J.MethodInvocation) { - J.MethodInvocation methodInvocation = (J.MethodInvocation) expression; - methodInvocationVisitor.visitMethodInvocation(owningFqn, methodInvocation); - } else if (expression instanceof J.NewClass) { - J.NewClass newClass = (J.NewClass) expression; - newClassVisitor.visitNewClass(owningFqn, newClass); - } else if (expression instanceof J.Lambda) { - J.Lambda lambda = (J.Lambda) expression; - processType(owningFqn, lambda.getType()); + @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; + } - return visitedReturn; + @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; } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java index 7bb536fe..004c7208 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java @@ -26,26 +26,8 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P } J.ClassDeclaration captureClassDeclarations(J.ClassDeclaration classDecl, Map> fqnMap) { - // get class fqn (including "$") String fqn = classDecl.getType().getFullyQualifiedName(); fqns.add(fqn); - - /* String currentPackage = getPackage(fqn); - String className = getClassName(fqn); - Map classesInPackage = fqnMap.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); - } - - fqnMap.put(currentPackage, classesInPackage);*/ return classDecl; } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java index 1775473b..5b681277 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java @@ -1,33 +1,26 @@ package org.hjug.graphbuilder.visitor; import java.util.List; -import lombok.Getter; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.SimpleDirectedWeightedGraph; -import org.openrewrite.java.JavaIsoVisitor; +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; -public class JavaMethodDeclarationVisitor

extends JavaIsoVisitor

implements TypeProcessor { +@Slf4j +public class JavaMethodDeclarationVisitor

extends BaseCodebaseVisitor

{ - @Getter - private Graph classReferencesGraph = - new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + private final BaseTypeProcessor typeProcessor; - @Getter - private Graph packageReferencesGraph = - new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - - public JavaMethodDeclarationVisitor() {} - - public JavaMethodDeclarationVisitor( - Graph classReferencesGraph, - Graph packageReferencesGraph) { - this.classReferencesGraph = classReferencesGraph; - this.packageReferencesGraph = packageReferencesGraph; + public JavaMethodDeclarationVisitor(DependencyCollector dependencyCollector) { + super(dependencyCollector); + this.typeProcessor = new BaseTypeProcessor() { + @Override + protected DependencyCollector getDependencyCollector() { + return dependencyCollector; + } + }; } @Override @@ -36,43 +29,48 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P JavaType.Method methodType = methodDeclaration.getMethodType(); if (null == methodType) { - // sometimes methodType is null, not sure why... + 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(); - // if returnTypeExpression is null, a constructor declaration is being processed TypeTree returnTypeExpression = methodDeclaration.getReturnTypeExpression(); if (returnTypeExpression != null) { JavaType returnType = returnTypeExpression.getType(); - // skip primitive variable declarations if (!(returnType instanceof JavaType.Primitive)) { - processType(owner, returnType); + typeProcessor.processType(owner, returnType); } } for (J.Annotation leadingAnnotation : methodDeclaration.getLeadingAnnotations()) { - processType(owner, leadingAnnotation.getType()); + typeProcessor.processAnnotation(owner, leadingAnnotation, getCursor()); } if (null != methodDeclaration.getTypeParameters()) { for (J.TypeParameter typeParameter : methodDeclaration.getTypeParameters()) { - processTypeParameter(owner, typeParameter); + typeProcessor.processTypeParameter(owner, typeParameter, getCursor()); } } - // don't need to capture parameter declarations - // they are captured in JavaVariableTypeVisitor - List throwz = methodDeclaration.getThrows(); if (null != throwz && !throwz.isEmpty()) { for (NameTree thrown : throwz) { - processType(owner, thrown.getType()); + typeProcessor.processType(owner, thrown.getType()); } } return methodDeclaration; } + + @Override + protected String getCurrentOwnerFqn() { + return null; + } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java deleted file mode 100644 index 70f81833..00000000 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.hjug.graphbuilder.visitor; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -// See RemoveMethodInvocationsVisitor for other visitor methods to override -// Custom visitor - not extending IsoVisitor on purpose since it does not provide caller information - -@RequiredArgsConstructor -@Getter -public class JavaMethodInvocationVisitor implements TypeProcessor { - - private final Graph classReferencesGraph; - - public J.MethodInvocation visitMethodInvocation(String invokingFqn, J.MethodInvocation methodInvocation) { - // getDeclaringType() returns the type that declared the method being invoked - JavaType.Method methodType = methodInvocation.getMethodType(); - // sometimes methodType is null - not sure why - if (null != methodType && null != methodType.getDeclaringType()) { - processType(invokingFqn, methodType.getDeclaringType()); - } - - if (null != methodInvocation.getTypeParameters() - && !methodInvocation.getTypeParameters().isEmpty()) { - for (Expression typeParameter : methodInvocation.getTypeParameters()) { - processType(invokingFqn, typeParameter.getType()); - } - } - - return methodInvocation; - } -} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java deleted file mode 100644 index 1be95649..00000000 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.hjug.graphbuilder.visitor; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.openrewrite.java.tree.J; - -@RequiredArgsConstructor -@Getter -public class JavaNewClassVisitor implements TypeProcessor { - - private final Graph classReferencesGraph; - - public J.NewClass visitNewClass(String invokingFqn, J.NewClass newClass) { - processType(invokingFqn, newClass.getType()); - // TASK: process initializer block??? - return newClass; - } -} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java index 117681cb..5ad5ccb2 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java @@ -1,54 +1,32 @@ package org.hjug.graphbuilder.visitor; import java.util.List; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.SimpleDirectedWeightedGraph; -import org.openrewrite.java.JavaIsoVisitor; +import org.hjug.graphbuilder.DependencyCollector; import org.openrewrite.java.tree.*; @Slf4j -public class JavaVariableTypeVisitor

extends JavaIsoVisitor

implements TypeProcessor { +public class JavaVariableTypeVisitor

extends BaseCodebaseVisitor

{ - @Getter - private Graph classReferencesGraph = - new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + private final BaseTypeProcessor typeProcessor; - @Getter - private Graph packageReferencesGraph = - new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - - private final JavaNewClassVisitor newClassVisitor; - private final JavaMethodInvocationVisitor methodInvocationVisitor; - - public JavaVariableTypeVisitor() { - newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); - methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); - } - - public JavaVariableTypeVisitor( - Graph classReferencesGraph, - Graph packageReferencesGraph) { - this.classReferencesGraph = classReferencesGraph; - this.packageReferencesGraph = packageReferencesGraph; - newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); - methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); + 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); - /* - * Handles - * java.lang.NullPointerException: Cannot invoke "org.openrewrite.java.tree.JavaType$Variable.getOwner()" - * because the return value of - * "org.openrewrite.java.tree.J$VariableDeclarations$NamedVariable.getVariableType()" is null - */ 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; } @@ -57,17 +35,20 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m if (owner instanceof JavaType.Method) { JavaType.Method m = (JavaType.Method) owner; - // log.debug("Method owner: " + m.getDeclaringType().getFullyQualifiedName()); + 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; - // log.debug("Method owner: " + c.getFullyQualifiedName()); ownerFqn = c.getFullyQualifiedName(); + } else { + log.debug("Unknown owner type: {}", owner != null ? owner.getClass() : "null"); + return variableDeclarations; } - log.debug("*************************"); - log.debug("Processing " + ownerFqn + ":" + variableDeclarations); - log.debug("*************************"); + log.debug("Processing variable declaration in: {}", ownerFqn); TypeTree typeTree = variableDeclarations.getTypeExpression(); @@ -78,33 +59,19 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return variableDeclarations; } - // TODO: getAllAnnotations() is deprecated - need to call - // AnnotationService.getAllAnnotations() but not sure which one yet - // but I'm not sure how to get a cursor - // All types, including primitives can be annotated - for (J.Annotation annotation : variableDeclarations.getAllAnnotations()) { - processAnnotation(ownerFqn, annotation); - } + typeProcessor.processAnnotations(ownerFqn, getCursor()); - // skip primitive variable declarations if (javaType instanceof JavaType.Primitive) { return variableDeclarations; } - processType(ownerFqn, javaType); - - // process variable instantiation if present - for (J.VariableDeclarations.NamedVariable variable : variables) { - Expression initializer = variable.getInitializer(); - if (null != initializer && null != initializer.getType() && initializer instanceof J.MethodInvocation) { - J.MethodInvocation methodInvocation = (J.MethodInvocation) initializer; - methodInvocationVisitor.visitMethodInvocation(ownerFqn, methodInvocation); - } else if (null != initializer && null != initializer.getType() && initializer instanceof J.NewClass) { - J.NewClass newClassType = (J.NewClass) initializer; - newClassVisitor.visitNewClass(ownerFqn, newClassType); - } - } + typeProcessor.processType(ownerFqn, javaType); return variableDeclarations; } + + @Override + protected String getCurrentOwnerFqn() { + return null; + } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java index a37ce8c4..d0b1fcf9 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java @@ -3,37 +3,20 @@ import java.util.*; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.openrewrite.java.JavaIsoVisitor; +import org.hjug.graphbuilder.DependencyCollector; import org.openrewrite.java.tree.*; @Slf4j -public class JavaVisitor

extends JavaIsoVisitor

implements TypeProcessor { +public class JavaVisitor

extends BaseCodebaseVisitor

{ - // used to keep track of what packages are in the codebase - // used to remove the nodes that are not in the codebase - @Getter - private final Set packagesInCodebase = new HashSet<>(); - - // used for looking up files where classes reside @Getter private final Map classToSourceFilePathMapping = new HashMap<>(); - @Getter - private final Graph classReferencesGraph; - - @Getter - private final Graph packageReferencesGraph; - private final JavaClassDeclarationVisitor

javaClassDeclarationVisitor; - public JavaVisitor( - Graph classReferencesGraph, - Graph packageReferencesGraph) { - this.classReferencesGraph = classReferencesGraph; - this.packageReferencesGraph = packageReferencesGraph; - javaClassDeclarationVisitor = new JavaClassDeclarationVisitor<>(classReferencesGraph); + public JavaVisitor(DependencyCollector dependencyCollector) { + super(dependencyCollector); + javaClassDeclarationVisitor = new JavaClassDeclarationVisitor<>(dependencyCollector); } @Override @@ -51,14 +34,20 @@ public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, P p) { return compilationUnit; } - packagesInCodebase.add(packageDeclaration.getPackageName()); + dependencyCollector.registerPackage(packageDeclaration.getPackageName()); for (J.ClassDeclaration aClass : compilationUnit.getClasses()) { - classToSourceFilePathMapping.put( - aClass.getType().getFullyQualifiedName(), - compilationUnit.getSourcePath().toUri().toString()); + 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; + } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeDependencyExtractor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeDependencyExtractor.java new file mode 100644 index 00000000..2b55f45f --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeDependencyExtractor.java @@ -0,0 +1,84 @@ +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); + } + } + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java deleted file mode 100644 index ca379dba..00000000 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.hjug.graphbuilder.visitor; - -import lombok.extern.slf4j.Slf4j; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.TypeTree; - -public interface TypeProcessor { - - @Slf4j - final class LogHolder {} - - /** - * @param ownerFqn The FQN that is the source of the relationship - * @param javaType The type that is used/referenced by the source of the relationship - */ - default void processType(String ownerFqn, JavaType javaType) { - if (javaType instanceof JavaType.Class) { - processType(ownerFqn, (JavaType.Class) javaType); - } else if (javaType instanceof JavaType.Parameterized) { - // A --> A - processType(ownerFqn, (JavaType.Parameterized) javaType); - } else if (javaType instanceof JavaType.GenericTypeVariable) { - // T t; - processType(ownerFqn, (JavaType.GenericTypeVariable) javaType); - } else if (javaType instanceof JavaType.Array) { - processType(ownerFqn, (JavaType.Array) javaType); - } - } - - private void processType(String ownerFqn, JavaType.Parameterized parameterized) { - // List> --> A - processAnnotations(ownerFqn, parameterized); - LogHolder.log.debug("Parameterized type FQN : " + parameterized.getFullyQualifiedName()); - addType(ownerFqn, parameterized.getFullyQualifiedName()); - - LogHolder.log.debug("Nested Parameterized type parameters: " + parameterized.getTypeParameters()); - for (JavaType parameter : parameterized.getTypeParameters()) { - processType(ownerFqn, parameter); - } - } - - private void processType(String ownerFqn, JavaType.Array arrayType) { - // D[] --> D - LogHolder.log.debug("Array Element type: " + arrayType.getElemType()); - processType(ownerFqn, arrayType.getElemType()); - } - - private void processType(String ownerFqn, JavaType.GenericTypeVariable typeVariable) { - LogHolder.log.debug("Type parameter type name: {}", typeVariable.getName()); - - for (JavaType bound : typeVariable.getBounds()) { - if (bound instanceof JavaType.Class) { - addType(ownerFqn, ((JavaType.Class) bound).getFullyQualifiedName()); - } else if (bound instanceof JavaType.Parameterized) { - addType(ownerFqn, ((JavaType.Parameterized) bound).getFullyQualifiedName()); - } else { - LogHolder.log.debug("Unknown type bound: {}", bound); - } - } - } - - private void processType(String ownerFqn, JavaType.Class classType) { - processAnnotations(ownerFqn, classType); - LogHolder.log.debug("Class type FQN: " + classType.getFullyQualifiedName()); - addType(ownerFqn, classType.getFullyQualifiedName()); - } - - private void processAnnotations(String ownerFqn, JavaType.FullyQualified fullyQualified) { - if (!fullyQualified.getAnnotations().isEmpty()) { - for (JavaType.FullyQualified annotation : fullyQualified.getAnnotations()) { - String annotationFqn = annotation.getFullyQualifiedName(); - LogHolder.log.debug("Extra Annotation FQN: " + annotationFqn); - addType(ownerFqn, annotationFqn); - } - } - } - - default void processAnnotation(String ownerFqn, J.Annotation annotation) { - if (annotation.getType() instanceof JavaType.Unknown) { - return; - } - - JavaType.Class type = (JavaType.Class) annotation.getType(); - if (null != type) { - String annotationFqn = type.getFullyQualifiedName(); - LogHolder.log.debug("Variable Annotation FQN: " + annotationFqn); - addType(ownerFqn, annotationFqn); - - if (null != annotation.getArguments()) { - for (Expression argument : annotation.getArguments()) { - processType(ownerFqn, argument.getType()); - } - } - } - } - - default void processTypeParameter(String ownerFqn, J.TypeParameter typeParameter) { - - 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); - } - } - } - - default Graph getPackageReferencesGraph() { - return null; - } - - Graph getClassReferencesGraph(); - - /** - * - * @param ownerFqn The FQN that is the source of the relationship - * @param typeFqn The FQN of the type that is being used by the source - */ - default void addType(String ownerFqn, String typeFqn) { - if (ownerFqn.equals(typeFqn)) return; - - Graph classReferencesGraph = getClassReferencesGraph(); - - classReferencesGraph.addVertex(ownerFqn); - classReferencesGraph.addVertex(typeFqn); - - if (!classReferencesGraph.containsEdge(ownerFqn, typeFqn)) { - classReferencesGraph.addEdge(ownerFqn, typeFqn); - } else { - DefaultWeightedEdge edge = classReferencesGraph.getEdge(ownerFqn, typeFqn); - classReferencesGraph.setEdgeWeight(edge, classReferencesGraph.getEdgeWeight(edge) + 1); - } - } - - // TODO: process packages -} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java index 1cf523a1..719381a9 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java @@ -7,6 +7,10 @@ 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; @@ -24,39 +28,29 @@ void visitClasses() throws IOException { 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<>(); + 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(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.B")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.C")); - // false because it doesn't reference any other classes - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.D")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation")); - // false because the class declaration doesn't reference any other classes - Assertions.assertFalse(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.E")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.F")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.G")); + 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")); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaInitializerBlockVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaInitializerBlockVisitorTest.java new file mode 100644 index 00000000..723b56bb --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaInitializerBlockVisitorTest.java @@ -0,0 +1,154 @@ +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"); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaLambdaVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaLambdaVisitorTest.java new file mode 100644 index 00000000..41105d19 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaLambdaVisitorTest.java @@ -0,0 +1,177 @@ +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"); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java index 3e20d16f..c249a148 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java @@ -7,6 +7,10 @@ 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; @@ -24,18 +28,22 @@ void visitMethodDeclarations() throws IOException { JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - JavaMethodDeclarationVisitor methodDeclarationVisitor = new JavaMethodDeclarationVisitor<>(); + 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); }); - methodDeclarationVisitor.getClassReferencesGraph(); - - Assertions.assertTrue(methodDeclarationVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); + Assertions.assertTrue(classReferencesGraph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); // TODO: Assert stuff /* Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.A")); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java index 1f92a05b..d84ff7b0 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java @@ -7,6 +7,7 @@ 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; @@ -28,14 +29,16 @@ void visitMethodInvocations() throws IOException { Graph classReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph packageReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + GraphDependencyCollector dependencyCollector = + new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph); + JavaClassDeclarationVisitor classDeclarationVisitor = - new JavaClassDeclarationVisitor<>(classReferencesGraph); + new JavaClassDeclarationVisitor<>(dependencyCollector); JavaVariableTypeVisitor variableTypeVisitor = - new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaVariableTypeVisitor<>(dependencyCollector); List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { @@ -43,7 +46,7 @@ void visitMethodInvocations() throws IOException { variableTypeVisitor.visit(cu, ctx); }); - Graph graph = classDeclarationVisitor.getClassReferencesGraph(); + 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")); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java index 5701e9ff..1a3efd2c 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java @@ -7,6 +7,7 @@ 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; @@ -28,27 +29,27 @@ void visitNewClass() throws IOException { Graph classReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph packageReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final JavaVisitor javaVisitor = - new JavaVisitor<>(classReferencesGraph, packageReferencesGraph); + GraphDependencyCollector dependencyCollector = + new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph); + + final JavaVisitor javaVisitor = new JavaVisitor<>(dependencyCollector); final JavaVariableTypeVisitor javaVariableTypeVisitor = - new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaVariableTypeVisitor<>(dependencyCollector); final JavaMethodDeclarationVisitor javaMethodDeclarationVisitor = - new JavaMethodDeclarationVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaMethodDeclarationVisitor<>(dependencyCollector); List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); - // Parse sources with all visitors, not only javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { javaVisitor.visit(cu, ctx); javaVariableTypeVisitor.visit(cu, ctx); javaMethodDeclarationVisitor.visit(cu, ctx); }); - Graph graph = javaVisitor.getClassReferencesGraph(); + 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")); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java index 504dacb9..28c2a942 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java @@ -7,6 +7,7 @@ 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; @@ -28,14 +29,16 @@ void visitNewClass() throws IOException { Graph classReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph packageReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + GraphDependencyCollector dependencyCollector = + new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph); + JavaClassDeclarationVisitor classDeclarationVisitor = - new JavaClassDeclarationVisitor<>(classReferencesGraph); + new JavaClassDeclarationVisitor<>(dependencyCollector); JavaVariableTypeVisitor variableTypeVisitor = - new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + new JavaVariableTypeVisitor<>(dependencyCollector); List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { @@ -43,7 +46,7 @@ void visitNewClass() throws IOException { variableTypeVisitor.visit(cu, ctx); }); - Graph graph = variableTypeVisitor.getClassReferencesGraph(); + 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")); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java index fbaf7523..066ba9f3 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java @@ -7,6 +7,10 @@ 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; @@ -24,36 +28,29 @@ void visitClasses() throws IOException { JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - JavaVariableTypeVisitor javaVariableCapturingVisitor = new JavaVariableTypeVisitor<>(); + 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(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.B")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.C")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.D")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.E")); - Assertions.assertTrue(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation")); - Assertions.assertFalse(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.F")); - Assertions.assertFalse(javaVariableCapturingVisitor - .getClassReferencesGraph() - .containsVertex("org.hjug.graphbuilder.visitor.testclasses.G")); + 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")); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java index b1260b29..d8d04858 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java @@ -9,6 +9,7 @@ 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; @@ -30,12 +31,13 @@ void visitClasses() throws IOException { final Graph classReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final Graph packageReferencesGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final JavaVisitor javaVisitor = - new JavaVisitor<>(classReferencesGraph, packageReferencesGraph); + 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 -> { @@ -43,6 +45,6 @@ void visitClasses() throws IOException { javaVisitor.visit(cu, ctx); }); - assertEquals(3, javaVisitor.getPackagesInCodebase().size()); + assertEquals(5, dependencyCollector.getPackagesInCodebase().size()); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/ComplexInitializerClass.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/ComplexInitializerClass.java new file mode 100644 index 00000000..b39e2869 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/ComplexInitializerClass.java @@ -0,0 +1,42 @@ +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 {} +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/InitializerBlockTestClass.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/InitializerBlockTestClass.java new file mode 100644 index 00000000..9bb94dd7 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/InitializerBlockTestClass.java @@ -0,0 +1,36 @@ +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 + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/DataProcessor.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/DataProcessor.java new file mode 100644 index 00000000..5123b5f4 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/DataProcessor.java @@ -0,0 +1,8 @@ +package org.hjug.graphbuilder.visitor.testclasses.lambda; + +public class DataProcessor { + + public String transform(String data) { + return data; + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/HelperClass.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/HelperClass.java new file mode 100644 index 00000000..64a7eeaa --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/HelperClass.java @@ -0,0 +1,12 @@ +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(); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/LambdaTestClass.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/LambdaTestClass.java new file mode 100644 index 00000000..35362026 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/LambdaTestClass.java @@ -0,0 +1,44 @@ +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); + }); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/NestedLambdaTestClass.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/NestedLambdaTestClass.java new file mode 100644 index 00000000..873160df --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/NestedLambdaTestClass.java @@ -0,0 +1,50 @@ +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()); + } +} diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java index e17a50ef..37ea56d9 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java @@ -580,7 +580,17 @@ String buildClassGraphDot(Graph classGraph) { // render vertices for (String vertex : vertexesToRender) { - dot.append(getClassName(vertex).replace("$", "_")); + String className = getClassName(vertex); + + // if the vertex is a nested class and has no outgoing edges, skip it + if (className.contains("$") + && className.split("\\$")[className.split("\\$").length - 1].matches("\\d+") + && classGraph.outDegreeOf(vertex) == 0) { + log.info("Skipping vertex: {}", className); + continue; + } + + dot.append(className.replace("$", "_")); if (vertexesToRemove.contains(vertex)) { dot.append(" [color=red style=filled]\n"); @@ -598,9 +608,30 @@ private void renderEdge( // render edge String[] vertexes = extractVertexes(edge); - String start = getClassName(vertexes[0].trim()).replace("$", "_"); - String end = getClassName(vertexes[1].trim()).replace("$", "_"); + // String start = getClassName(vertexes[0].trim()).replace("$", "_"); + // String end = getClassName(vertexes[1].trim()).replace("$", "_"); + + String startVertex = vertexes[0].trim(); + String start = getClassName(startVertex.trim()).replace("$", "_"); + String endVertex = vertexes[1].trim(); + String end = getClassName(endVertex.trim()).replace("$", "_"); + + // if the vertex is a nested class and has no outgoing edges, skip it + if (start.contains("$") + && start.split("\\$")[startVertex.split("\\$").length - 1].matches("\\d+") + && classGraph.outDegreeOf(startVertex) == 0) { + log.info("Skipping edge: {} -> {}", startVertex, endVertex); + return; + } + + if (endVertex.contains("$") + && endVertex.split("\\$")[endVertex.split("\\$").length - 1].matches("\\d+") + && classGraph.outDegreeOf(endVertex) == 0) { + log.info("Skipping edge: {} -> {}", startVertex, endVertex); + return; + } + log.info("Rendering edge: {} -> {}", startVertex, endVertex); dot.append(start); dot.append(" -> "); dot.append(end);