# -*- coding: utf-8 -*- import os import re import networkx as nx # pip install networkx plotly numpy scipy def build_class_graph(directory): """Builds a directed acyclic graph of class dependencies.""" # Create a graph to store the dependencies graph = nx.DiGraph() dependencies = {} # Find all .gd files in the directory for root, dirs, files in os.walk(directory): for file in files: if not file.endswith(".gd"): continue file_path = os.path.join(root, file) # Find the class name in the file with open(file_path, "r", encoding="utf-8") as f: contents = f.read() match = re.search(r"^class_name\s+(\w+)", contents, re.MULTILINE) if not match: continue class_name = match.group(1) graph.add_node(class_name) # Find the dependencies of the class for dep_match in re.finditer( r"^\s*extends\s+(\w+)", contents, re.MULTILINE ): dep_name = dep_match.group(1) graph.add_edge(class_name, dep_name) # Find all uses of the class name in other files for root2, dirs2, files2 in os.walk(directory): for file2 in files2: if not file2.endswith(".gd"): continue if file_path == os.path.join(root2, file2): continue with open(os.path.join(root2, file2), "r", encoding="utf-8") as f2: contents2 = f2.read() if re.search(rf"\b{class_name}\b", contents2): print( file2.ljust(30, " "), f"--> class_name {class_name}".ljust(50, " "), f" from {file}", ) graph.add_edge(class_name, file2) dependencies[file2] = file pos = nx.planar_layout(graph) # Set the node positions in the graph nx.set_node_attributes(graph, pos, "pos") return graph, dependencies def debug_graph(graph): """Builds and visualizes a graph of class dependencies in the directory.""" import plotly.graph_objects as go # Create the plotly figure fig = go.Figure() # Add the nodes to the figure for node in graph.nodes(): x, y = graph.nodes[node]["pos"] fig.add_trace( go.Scatter( x=[x], y=[y], text=[node], hovertext=[f"Class: {node}"], mode="markers", marker=dict( symbol="circle", size=20, line=dict(width=1, color="black"), ), name=node, ) ) # Add the edges to the figure for edge in graph.edges(): x0, y0 = graph.nodes[edge[0]]["pos"] x1, y1 = graph.nodes[edge[1]]["pos"] fig.add_trace( go.Scatter( x=[x0, x1], y=[y0, y1], text=[f"{edge[0]} -> {edge[1]}", ""], hovertext=[f"Depends on: {edge[1]}", ""], mode="lines+markers+text", line=dict(width=2, color="black"), marker=dict(size=0), textposition="middle right", showlegend=True, ) ) # Customize the layout and style of the figure fig.update_layout( title="Class Dependency Graph", title_font_size=24, margin=dict(l=20, r=20, t=50, b=20), hovermode="closest", plot_bgcolor="white", showlegend=True, ) # Show the figure fig.show() def find_circular_dependencies(dependencies): """Finds circular dependencies between classes in the directory.""" circular_dependencies = [] for file, dependency in dependencies.items(): visited = set() path = [file] while dependency and dependency not in visited: visited.add(dependency) path.append(dependency) dependency = dependencies.get(dependency) # Circular check if dependency in path: start_index = path.index(dependency) circular_dependency = path[start_index:] if circular_dependency not in circular_dependencies: circular_dependencies.append(circular_dependency) break # Convert circular_dependencies to dependency paths dependency_paths = [] for circular_dependency in circular_dependencies: dependency_path = " -> ".join(circular_dependency) dependency_paths.append(dependency_path) return dependency_paths # Example usage graph, dependencies = build_class_graph("./") debug_graph(graph) circular_deps = find_circular_dependencies(dependencies) print("----------------------------------") if circular_deps: print(" Circular dependencies found!") for dep in circular_deps: print("\t", dep) else: print(" No circular dependencies found.") print("----------------------------------")