1 /// Author: Aziz Köksal 2 /// License: GPL3 3 /// $(Maturity average) 4 module cmd.ImportGraph; 5 6 import cmd.Command; 7 import dil.ast.Node, 8 dil.ast.Declarations; 9 import dil.semantic.Module, 10 dil.semantic.Package; 11 import dil.parser.ImportParser; 12 import dil.SourceText, 13 dil.Compilation, 14 dil.ModuleManager, 15 dil.Diagnostics, 16 dil.String; 17 import util.Path; 18 import Settings; 19 import common; 20 21 import std.regex; 22 23 /// The importgraph command. 24 class IGraphCommand : Command 25 { 26 /// Options for the command. 27 enum Option 28 { 29 None, 30 IncludeUnlocatableModules = 1, 31 PrintDot = 1<<1, 32 HighlightCyclicEdges = 1<<2, 33 HighlightCyclicVertices = 1<<3, 34 GroupByPackageNames = 1<<4, 35 GroupByFullPackageName = 1<<5, 36 PrintPaths = 1<<6, 37 PrintList = 1<<7, 38 MarkCyclicModules = 1<<8, 39 } 40 alias Options = Option; 41 42 Options options; /// Command options. 43 cstring filePath; /// File path to the root module. 44 cstring[] regexps; /// Regular expressions. 45 cstring siStyle = "dashed"; /// Static import style. 46 cstring piStyle = "bold"; /// Public import style. 47 uint levels; /// How many levels to print. 48 49 CompilationContext context; 50 51 /// Adds o to the options. 52 void add(Option o) 53 { 54 options |= o; 55 } 56 57 /// Executes the command. 58 override void run() 59 { 60 // Init regular expressions. 61 Regex!char[] regexps; 62 foreach (strRegexp; this.regexps) 63 regexps ~= regex(strRegexp); 64 65 // Add the directory of the file to the import paths. 66 auto filePath = Path(this.filePath); 67 context.importPaths ~= filePath.folder(); 68 69 auto gbuilder = new GraphBuilder(context); 70 71 gbuilder.options = options; 72 gbuilder.filterPredicate = (cstring moduleFQNPath) { 73 foreach (rx; regexps) 74 // Replace slashes: dil/ast/Node -> dil.ast.Node 75 if (!matchFirst(moduleFQNPath.replace(dirSep, '.'), rx).empty) 76 return true; 77 return false; 78 }; 79 80 auto graph = gbuilder.start(filePath.name()); 81 82 if (options & (Option.PrintList | Option.PrintPaths)) 83 { 84 if (options & Option.MarkCyclicModules) 85 graph.detectCycles(); 86 87 if (options & Option.PrintPaths) 88 printModulePaths(graph.vertices, levels+1, ""); 89 else 90 printModuleList(graph.vertices, levels+1, ""); 91 } 92 else 93 printDotDocument(context, graph, siStyle, piStyle, options); 94 } 95 } 96 97 /// Represents a module dependency graph. 98 class Graph 99 { 100 Vertex[] vertices; /// The vertices or modules. 101 Edge[] edges; /// The edges or import statements. 102 103 /// Adds a vertex to the graph and sets its ID. 104 void addVertex(Vertex vertex) 105 { 106 vertex.id = vertices.length; 107 vertices ~= vertex; 108 } 109 110 /// Adds an edge between two vertices to the graph. 111 Edge addEdge(Vertex from, Vertex to) 112 { 113 auto edge = new Edge(from, to); 114 edges ~= edge; 115 from.outgoing ~= to; 116 return edge; 117 } 118 119 /// ditto 120 Edge addEdge(size_t id1, size_t id2) 121 { 122 return addEdge(vertices[id1], vertices[id2]); 123 } 124 125 /// Walks the graph and marks cyclic vertices and edges. 126 void detectCycles() 127 { 128 bool[Vertex][] vsets; // List of Set(sccs) 129 130 bool isReflexive(Vertex v) 131 { 132 foreach (w; v.outgoing) 133 if (w is v) 134 return true; 135 return false; 136 } 137 138 foreach (sccs; findTarjansSCCs()) 139 { // Ignore a component if it's alone and not connecting to itself. 140 if (sccs.length == 1 && !isReflexive(sccs[0])) 141 continue; 142 143 bool[Vertex] vset; 144 foreach (v; sccs) 145 vset[v] = v.isCyclic = true; 146 vsets ~= vset; 147 } 148 149 foreach (e; edges) 150 foreach (vset; vsets) 151 if (auto v = e.from in vset && e.to in vset) 152 { 153 e.isCyclic = true; 154 break; 155 } 156 } 157 158 /// Returns a list of strongly connected components using Tarjan's algorithm. 159 Vertex[][] findTarjansSCCs() 160 { 161 size_t i = 1; // Start with 1, as the default of Vertex.index is 0. 162 Vertex[] stack; 163 Vertex[][] sccs; 164 165 void recurse(Vertex v) 166 { 167 v.index = v.llink = i++; 168 v.instack = true; 169 stack ~= v; 170 171 foreach (z; v.outgoing) 172 if (z.index == 0) 173 { 174 recurse(z); // Depth-first search. 175 if (z.llink < v.llink) 176 v.llink = z.llink; 177 } 178 else if (z.instack && z.index < v.llink) 179 v.llink = z.index; 180 181 if (v.index == v.llink) 182 { 183 assert(stack.length); 184 auto end = stack.ptr + stack.length; 185 auto p = end; // Reverse iterator. 186 Vertex z; 187 do 188 { // Search backwards until the root vertex is found. 189 assert(p > stack.ptr); 190 z = *--p; 191 z.instack = false; 192 } while (z !is v); 193 assert(*p is v && v is z); 194 sccs ~= p[0 .. end - p].dup; 195 stack.length = p - stack.ptr; 196 } 197 } 198 199 // Walk through the graph. 200 foreach (v; vertices) 201 if (v.index == 0) 202 recurse(v); 203 204 return sccs; 205 } 206 } 207 208 void testGraph() 209 { 210 scope msg = new UnittestMsg("Testing class Graph."); 211 212 // 1st test. 213 { 214 auto g = new Graph(); 215 auto v = new Vertex(); 216 g.addVertex(v); 217 g.addEdge(v, v); // Connect the only vertex to itself. 218 g.detectCycles(); 219 assert(g.vertices[0].isCyclic && g.edges[0].isCyclic); 220 } 221 222 // 2nd test. 223 auto g = new Graph(); 224 225 auto connect = (size_t x, size_t y) => g.addEdge(x, y); 226 227 foreach (i; 0..7) 228 g.addVertex(new Vertex()); 229 230 connect(0, 1); 231 connect(1, 3); 232 connect(3, 2); 233 connect(2, 0); 234 235 connect(6, 5); 236 connect(5, 3); 237 connect(3, 4); 238 connect(4, 6); 239 240 g.detectCycles(); 241 242 foreach (v; g.vertices) 243 assert(v.isCyclic); 244 foreach (e; g.edges) 245 assert(e.isCyclic); 246 247 // 3rd test. 248 g = new Graph(); 249 250 foreach (i; 0..6) 251 g.addVertex(new Vertex()); 252 253 // First "triangle". 254 connect(0, 1); 255 connect(1, 2); 256 connect(2, 0); 257 // Second "triangle". 258 connect(3, 4); 259 connect(4, 5); 260 connect(5, 3); 261 // Connect the two triangles. 262 connect(3, 0); 263 264 g.detectCycles(); 265 266 foreach (e; g.edges[0..$-1]) 267 assert(e.isCyclic); // Edges of the triangles must be marked as cyclic. 268 // The last edge, connecting both triangles, is not cyclic. 269 assert(!g.edges[$-1].isCyclic); 270 } 271 272 /// Represents a directed connection between two vertices. 273 class Edge 274 { 275 Vertex from; /// Coming from vertex. 276 Vertex to; /// Going to vertex. 277 bool isCyclic; /// Edge connects cyclic vertices. 278 bool isPublic; /// Public import. 279 bool isStatic; /// Static import. 280 281 /// Constructs an Edge object between two vertices. 282 this(Vertex from, Vertex to) 283 { 284 this.from = from; 285 this.to = to; 286 } 287 288 override string toString() 289 { 290 return cast(string)Format("E({}, {})", from, to); 291 } 292 } 293 294 /// Represents a module in the graph. 295 class Vertex 296 { 297 Module modul; /// The module represented by this vertex. 298 size_t id; /// The nth vertex in the graph. 299 Vertex[] outgoing; /// Also called successors. 300 bool isCyclic; /// Whether this vertex is in a cyclic relationship 301 /// with other vertices. 302 // Members specific to Tarjan's algorithm. 303 size_t index; /// Greater than 0 means that this vertex has been visited. 304 size_t llink; /// Aka. lowlink. 305 bool instack; /// Is this in the Stack? 306 307 override string toString() 308 { 309 return cast(string)Format("V({})", id); 310 } 311 } 312 313 /// Builds a module dependency graph. 314 class GraphBuilder 315 { 316 Graph graph; /// The graph object. 317 IGraphCommand.Options options; /// The options. 318 Vertex[hash_t] loadedModulesTable; /// Maps FQN paths to modules. 319 bool delegate(cstring) filterPredicate; 320 CompilationContext cc; /// The context. 321 322 /// Constructs a GraphBuilder object. 323 this(CompilationContext cc) 324 { 325 this.graph = new Graph; 326 this.cc = cc; 327 } 328 329 /// Start building the graph and return that. 330 /// Params: 331 /// fileName = The file name of the root module. 332 Graph start(cstring fileName) 333 { 334 loadModule(fileName); 335 return graph; 336 } 337 338 /// Loads all modules recursively and builds the graph at the same time. 339 /// Params: 340 /// moduleFQNPath = The path version of the module FQN.$(BR) 341 /// E.g.: FQN = dil.ast.Node -> FQNPath = dil/ast/Node 342 Vertex loadModule(cstring moduleFQNPath) 343 { 344 // Look up in table if the module is already loaded. 345 auto hash = hashOf(moduleFQNPath); 346 auto pVertex = hash in loadedModulesTable; 347 if (pVertex !is null) 348 return *pVertex; // Returns null for filtered or unlocatable modules. 349 350 // Filter out modules. 351 if (filterPredicate && filterPredicate(moduleFQNPath)) 352 { // Store null for filtered modules. 353 loadedModulesTable[hash] = null; 354 return null; 355 } 356 357 // Locate the module in the file system. 358 auto moduleFilePath = ModuleManager.findModuleFile( 359 moduleFQNPath, cc.importPaths); 360 361 Vertex vertex; 362 363 if (moduleFilePath is null) 364 { // Module not found. 365 if (options & IGraphCommand.Option.IncludeUnlocatableModules) 366 { // Include module nevertheless. 367 vertex = new Vertex; 368 vertex.modul = new Module("", cc); 369 auto fqn = moduleFQNPath.replace(dirSep, '.'); 370 vertex.modul.setFQN(fqn); 371 graph.addVertex(vertex); 372 } 373 // Store vertex in the table (vertex may be null.) 374 loadedModulesTable[hash] = vertex; 375 } 376 else 377 { 378 auto modul = new Module(moduleFilePath, cc); 379 // Use lightweight ImportParser. 380 modul.setParser(new ImportParser(modul.sourceText, cc.tables.lxtables)); 381 modul.parse(); 382 383 vertex = new Vertex; 384 vertex.modul = modul; 385 386 graph.addVertex(vertex); 387 loadedModulesTable[hashOf(modul.getFQNPath())] = vertex; 388 389 // Load the modules which this module depends on. 390 foreach (importDecl; modul.imports) 391 { 392 foreach (moduleFQNPath2; importDecl.getModuleFQNs(dirSep)) 393 { 394 auto loaded = loadModule(moduleFQNPath2); 395 if (loaded !is null) 396 { 397 auto edge = graph.addEdge(vertex, loaded); 398 edge.isPublic = importDecl.isPublic(); 399 edge.isStatic = importDecl.isStatic(); 400 } 401 } 402 } 403 } 404 return vertex; 405 } 406 } 407 408 /// Prints the file paths to the modules. 409 void printModulePaths(Vertex[] vertices, uint level, cstring indent) 410 { 411 if (level == 0) 412 return; 413 foreach (vertex; vertices) 414 { 415 Stdout(indent)((vertex.isCyclic?"*":"")~vertex.modul.filePath).newline; 416 if (vertex.outgoing.length) 417 printModulePaths(vertex.outgoing, level-1, indent~" "); 418 } 419 } 420 421 /// Prints a list of module FQNs. 422 void printModuleList(Vertex[] vertices, uint level, cstring indent) 423 { 424 if (level == 0) 425 return; 426 foreach (vertex; vertices) 427 { 428 Stdout(indent)((vertex.isCyclic?"*":"")~vertex.modul.getFQN()).newline; 429 if (vertex.outgoing.length) 430 printModuleList(vertex.outgoing, level-1, indent~" "); 431 } 432 } 433 434 /// Prints the graph as a graphviz dot document. 435 void printDotDocument(CompilationContext cc, Graph graph, 436 cstring siStyle, cstring piStyle, IGraphCommand.Options options) 437 { 438 // Needed for grouping by package names. 439 ModuleManager mm; 440 uint groupModules = options & (IGraphCommand.Option.GroupByFullPackageName | 441 IGraphCommand.Option.GroupByPackageNames); 442 443 if (groupModules) 444 { 445 mm = new ModuleManager(cc); 446 foreach (vertex; graph.vertices) 447 mm.addModule(vertex.modul); 448 } 449 450 if (options & (IGraphCommand.Option.HighlightCyclicVertices | 451 IGraphCommand.Option.HighlightCyclicEdges)) 452 graph.detectCycles(); 453 454 // Output header of the dot document. 455 Stdout("Digraph ImportGraph\n{\n"); 456 Stdout(" fontname = Verdana; /*fontsize = 10;*/\n"); 457 // Output nodes. 458 // 'i' and vertex.id should be the same. 459 foreach (i, vertex; graph.vertices) 460 Stdout.formatln(` n{} [label="{}"{}];`, i, //, URL="{}.html" 461 groupModules ? vertex.modul.moduleName : 462 vertex.modul.getFQN(), 463 (vertex.isCyclic ? ",style=filled,fillcolor=tomato" : "")); 464 465 // Output edges. 466 foreach (edge; graph.edges) 467 { 468 cstring edgeStyles = ""; 469 if (edge.isStatic || edge.isPublic) 470 { 471 edgeStyles = `[style="`; 472 edge.isStatic && (edgeStyles ~= siStyle ~ ","); 473 edge.isPublic && (edgeStyles ~= piStyle); 474 if (edgeStyles[$-1] == ',') 475 edgeStyles = edgeStyles[0..$-1]; // Remove last comma. 476 edgeStyles ~= `"]`; 477 } 478 edge.isCyclic && (edgeStyles ~= "[color=red]"); 479 Stdout.formatln(` n{} -> n{} {};`, edge.from.id, edge.to.id, edgeStyles); 480 } 481 482 if (options & IGraphCommand.Option.GroupByFullPackageName) 483 { 484 Vertex[][cstring] verticesByPckgName; 485 foreach (vertex; graph.vertices) 486 verticesByPckgName[vertex.modul.packageName] ~= vertex; 487 foreach (packageFQN, vertices; verticesByPckgName) 488 { // Output nodes in a cluster. 489 Stdout.format(` subgraph "cluster_{0}" {{`"\n" 490 ` label="{0}";color=blue;`"\n ", 491 packageFQN); 492 foreach (vertex; vertices) 493 Stdout.format(`n{};`, vertex.id); 494 Stdout("\n }\n"); 495 } 496 } 497 else if (options & IGraphCommand.Option.GroupByPackageNames) 498 { 499 Stdout(" // Warning: some nested clusters may crash dot.\n"); 500 size_t[Module] idTable; 501 foreach (vertex; graph.vertices) 502 idTable[vertex.modul] = vertex.id; 503 void printSubgraph(Package pckg, cstring indent) 504 { // Output nodes in a cluster. 505 foreach (p; pckg.packages) 506 { 507 Stdout.format(`{0}subgraph "cluster_{1}" {{`"\n" 508 `{0} label="{2}";color=blue;`"\n" 509 "{0} ", 510 indent, p.getFQN(), p.pckgName); 511 foreach (modul; p.modules) 512 Stdout.format(`n{};`, idTable[modul]); 513 if (p.packages) { 514 Stdout.newline; 515 printSubgraph(p, indent~" "); // Output nested clusters. 516 } 517 Stdout("\n }\n"); 518 } 519 } 520 printSubgraph(mm.rootPackage, " "); 521 } 522 523 Stdout("}\n"); 524 }