Detecting Circular References In A Java Project

23 Jul 2018

#Java #code maintenance #source code analysis

As 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);
	}
}

Sample Output Graph Image

Sample output graph image.


References