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