1 module dgraphviz; 2 3 import std.format : format; 4 5 6 struct Option { 7 string[string] option; 8 alias option this; 9 10 auto toString() { 11 if (option.length == 0) return ""; 12 auto s = " [ "; 13 foreach (k, v; option) { 14 s ~= "%s = \"%s\", ".format(k, v); 15 } 16 s = s[0 .. $-2] ~ " ]"; 17 return s; 18 } 19 } 20 21 private struct Edge { 22 string ark; 23 Node src, dst; 24 Option option; 25 26 auto toString() { 27 return "\"%s\" %s \"%s\" %s;\n".format(src.label, ark, dst.label, option); 28 } 29 } 30 31 private class Node { 32 string label; 33 Option option; 34 size_t nIn = 0, nOut = 0; 35 36 this(string label) { 37 import std.string : replace; 38 this.label = label.replace("\"", "\\\""); 39 } 40 41 this(string label, Option option) { 42 this(label); 43 this.option = option; 44 } 45 46 auto info() { 47 if (option.length == 0) return ""; 48 auto s = "\"%s\" %s;\n".format(label, option); 49 return s; 50 } 51 } 52 53 54 abstract class Graph { 55 import std.conv : to; 56 57 // TODO use Set 58 Node[string] nodes; 59 Edge[string] edges; 60 Option graphOpt, nodeOpt, edgeOpt; 61 62 ref auto node(ref Node d) { return d; } 63 64 ref auto node(T)(T t) { 65 string[string] opt; 66 return node(t, opt); 67 } 68 69 ref auto node(T)(T t, string[string] option) { 70 auto key = t.to!string; 71 if (key !in this.nodes) { 72 this.nodes[key] = new Node(t.to!string, Option(option)); 73 } 74 return this.nodes[key]; 75 } 76 77 auto edge(S, D)(S src, D dst,) { 78 string[string] opt; 79 return edge(src, dst, opt); 80 } 81 82 auto edge(S, D)(S src, D dst, string[string] option) { 83 auto s = node(src); 84 auto d = node(dst); 85 auto e = Edge(this.ark, s, d, Option(option)); 86 ++s.nOut; 87 ++d.nIn; 88 this.edges[e.to!string] = e; 89 return e; 90 } 91 92 abstract string typename(); 93 abstract string ark(); 94 95 override string toString() { 96 import std.array : array; 97 import std.algorithm : uniq, map, sort; 98 auto s = this.typename ~ " g{\n"; 99 100 if (graphOpt.length > 0) s ~= "graph %s;\n".format(graphOpt); 101 if (nodeOpt.length > 0) s ~= "node %s;\n".format(nodeOpt); 102 if (edgeOpt.length > 0) s ~= "edge %s;\n".format(edgeOpt); 103 104 foreach (k, n; this.nodes) { 105 s ~= n.info; 106 } 107 foreach (k, e; this.edges) { 108 s ~= k; 109 } 110 s ~= "}\n"; 111 return s; 112 } 113 114 void save(string path) { 115 import std.stdio : File; 116 auto f = File(path, "w"); 117 f.write(this.toString()); 118 f.detach(); 119 } 120 } 121 122 class Undirected : Graph { 123 override string typename() { return "graph"; } 124 override string ark() { return "--"; } 125 } 126 127 class Directed : Graph { 128 override string typename() { return "digraph"; } 129 override string ark() { return "->"; } 130 } 131 132 133 /// 134 unittest { 135 import std.stdio; 136 import std.format; 137 import dgraphviz; 138 139 struct A { 140 auto toString() { 141 return "A\n\"struct\""; 142 } 143 } 144 145 auto g = new Directed; 146 A a; 147 with (g) { 148 node(a, ["shape": "box", "color": "#ff0000"]); 149 edge(a, true); 150 edge(a, 1, ["style": "dashed", "label": "a-to-1"]); 151 edge(true, "foo"); 152 } 153 g.save("simple.dot"); 154 } 155 156 Directed libraryDependency(string root, string prefix="", 157 bool verbose=false, size_t maxDepth=3) { 158 import std.file : dirEntries, SpanMode, readText; 159 import std.format : formattedRead; 160 import std.string : split, strip, join, endsWith, replace, startsWith; 161 import std.algorithm : map, canFind, min, any, filter; 162 import std.stdio : writefln; 163 164 auto g = new Directed; 165 166 with (g) { 167 enum invalidTokens = ["\"", "$", "/", "\\"]; 168 auto removeSub(string s) { 169 return s.split(".")[0..min($, maxDepth)].join("."); 170 } 171 172 void registerEdge(string src, string dst) { 173 dst = dst.strip; 174 // FIXME follow import expr spec. 175 if (invalidTokens.map!(i => dst.canFind(i)).any) { 176 return; 177 } else if (dst.canFind(":")) { 178 registerEdge(src, dst.split(":")[0]); 179 } else if (dst.canFind(",")) { 180 foreach (d; split(dst, ",")) { 181 registerEdge(src, d); 182 } 183 } else if (dst.canFind(" ")) { 184 return; 185 } else if (dst.canFind("std.")) { 186 if (verbose) writefln("%s -> %s", src, dst); 187 edge(removeSub(src), removeSub(dst)); 188 } 189 } 190 191 auto dfiles = dirEntries(root, SpanMode.depth) 192 .filter!(f => f.name.startsWith(root ~ prefix) && f.name.endsWith(".d")); 193 foreach (dpath; dfiles) { 194 auto src = dpath[root.length .. $].replace("/", ".")[0 .. $-2]; 195 try { 196 foreach (txt; dpath.readText.split("import")[1..$]) { 197 txt = "import " ~ txt; 198 string dst, rest; 199 txt.formattedRead!"import %s;%s"(dst, rest); 200 if (verbose) writefln("%s ---------> %s", src, dst); 201 registerEdge(src, dst); 202 } 203 } catch (Exception e) { 204 // FIXME display warnings 205 } 206 } 207 } 208 return g; 209 } 210 211 /// 212 unittest { 213 import std.path; 214 import std.process; 215 216 auto dc = environment.get("DC"); 217 assert(dc != "", "use DUB or set DC enviroment variable"); 218 auto which = executeShell("which " ~ dc); 219 assert(which.status == 0); 220 version(DigitalMars) { 221 auto root = which.output.dirName ~ "/../../src/phobos/"; 222 } 223 version(LDC) { 224 auto root = which.output.dirName ~ "/../import/"; 225 } 226 227 auto g = libraryDependency(root, "std/range", true); 228 g.save("range.dot"); 229 }