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 }