Detecting Circular References In A Java Project
23 Jul 2018
#Java #code maintenance #source code analysisAs Java projects get bigger, with several developers working on them, it may lead to code having circular references. It is difficult to serialize objects with circular references. I have created a tool to detect circular references in a Java project : Circular Reference Detector Gitlab Source Code. Below are the steps taken to detect circular references in a Java project.
Program Goal
- Input : Path to a Java project’s source files.
- Output : Folder containing the image files of cyclic graphs for each class having circular references.
Steps
- Using Java Parser, parse every Java file in the given source directory and collect the references of a class in a directed graph using JgraphT
- Detect cycles for each vertex in the graph using JgraphT’s CycleDetector
- Store graphs as PNG files using jgrapht-ext library
Code Snippets
Parse Java files in the source directory and add the class references to a graph.
Graph<String, DefaultEdge> classReferencesGraph = new DefaultDirectedGraph<>(DefaultEdge.class);
try (Stream<Path> filesStream = Files.walk(Paths.get(srcDirectory))) {
filesStream
.filter(path -> path.getFileName().toString().endsWith(".java"))
.forEach(path ->{
Set<String> instanceVarTypes = getInstanceVarTypes(path.toFile());
if( ! instanceVarTypes.isEmpty()) {
String className = getClassName(path.getFileName().toString());
classReferencesGraph.addVertex(className);
instanceVarTypes.forEach(classReferencesGraph::addVertex);
instanceVarTypes.forEach( var -> classReferencesGraph.addEdge(className, var));
}
});
}
Collecting the Instance Variables of a Class using Java Parser
CompilationUnit compilationUnit = StaticJavaParser.parse(javaSrcFile);
List<Node> instanceVarTypes = compilationUnit.findAll(FieldDeclaration.class)
.stream()
.map(f -> f.getVariables().get(0).getType())
.filter(v -> !v.isPrimitiveType())
.map( Object::toString)
.collect(Collectors.toSet());
Create Directed Graph using Map
Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);
//add vertices
classNametoInstanceVarTypesMap.keySet().forEach(className ->{
graph.addVertex(className);
});
//add edges
classNametoInstanceVarTypesMap
.forEach((className , instanceVariableTypes) -> {
instanceVariableTypes.forEach( instVar -> {
if (classNametoInstanceVarTypesMap.containsKey(instVar)){
graph.addEdge(className, instVar);
}
});
});
Detect cycles for each vertex in the graph using JGraphT CycleDetector and add the cycles to a map.
Map<String, AsSubgraph<String, DefaultEdge>> cyclesForEveryVertexMap = new HashMap<>();
CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(classReferencesGraph);
cycleDetector.findCycles().forEach(v -> {
AsSubgraph<String, DefaultEdge> subGraph = new AsSubgraph<>(classReferencesGraph,
cycleDetector.findCyclesContainingVertex(v));
cyclesForEveryVertexMap.put(v,subGraph);
});
findCyclesContainingVertex(vertex)
returns set of vertices participating in the cycle for a vertex. Create a subgraph of the main graph using these vertices.
Create PNG Image of a graph using jgrapht-ext library.
new File(outputDirectoryPath).mkdirs();
File imgFile = new File(outputDirectoryPath+"/graph" + imageName + ".png");
if(imgFile.createNewFile()) {
JGraphXAdapter<String, DefaultEdge> graphAdapter = new JGraphXAdapter<>(subGraph);
mxIGraphLayout layout = new mxCircleLayout(graphAdapter);
layout.execute(graphAdapter.getDefaultParent());
BufferedImage image = mxCellRenderer.createBufferedImage(graphAdapter, null, 2, Color.WHITE, true, null);
if (image != null) {
ImageIO.write(image, "PNG", imgFile);
}
}