├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dub.json ├── range.dot ├── range.dot.png ├── simple.dot ├── simple.dot.png └── source └── dgraphviz.d /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | !*.d 4 | !*.png 5 | !*.dot 6 | !.gitignore 7 | !.travis.yml 8 | !LICENSE 9 | 10 | .dub 11 | docs.json 12 | __dummy.html 13 | docs/ 14 | d-graphviz.so 15 | d-graphviz.dylib 16 | d-graphviz.dll 17 | d-graphviz.a 18 | d-graphviz.lib 19 | d-graphviz-test-* 20 | *.exe 21 | *.o 22 | *.obj 23 | *.lst 24 | 25 | d-graphviz 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | os: 4 | - linux 5 | 6 | language: d 7 | 8 | d: 9 | - ldc 10 | - dmd 11 | 12 | env: 13 | - ARCH="x86_64" 14 | 15 | before_install: 16 | - sudo apt-get install graphviz 17 | 18 | script: 19 | - dub test --build=unittest-cov 20 | - ls *.dot | xargs dot -Tpng -O 21 | 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d-graphviz 2 | 3 | [![Build Status](https://travis-ci.org/ShigekiKarita/d-graphviz.svg?branch=master)](https://travis-ci.org/ShigekiKarita/d-graphviz) 4 | [![codecov](https://codecov.io/gh/ShigekiKarita/d-graphviz/branch/master/graph/badge.svg)](https://codecov.io/gh/ShigekiKarita/d-graphviz) 5 | [![Dub version](https://img.shields.io/dub/v/d-graphviz.svg)](https://code.dlang.org/packages/d-graphviz) 6 | 7 | Graphviz utility for D 8 | 9 | ![dot](simple.dot.png) 10 | 11 | created by 12 | 13 | ```d 14 | import std.stdio; 15 | import std.format; 16 | 17 | import dgraphviz; 18 | 19 | struct A { 20 | auto toString() { 21 | return "A\n\"struct\""; 22 | } 23 | } 24 | 25 | void main() 26 | { 27 | auto g = new Directed; 28 | A a; 29 | with (g) { 30 | node(a, ["shape": "box", "color": "#ff0000"]); 31 | edge(a, true); 32 | edge(a, 1, ["style": "dashed", "label": "a-to-1"]); 33 | edge(true, "foo"); 34 | } 35 | g.save("simple.dot"); 36 | } 37 | ``` 38 | 39 | and `$ dot simple.dot -Tpng -O` 40 | 41 | 42 | ## practical example 43 | 44 | library dependency graph 45 | 46 | ```d 47 | import std.path; 48 | import std.process; 49 | import dgraphviz; 50 | 51 | void main() { 52 | // set phobos root 53 | auto dc = environment.get("DC"); 54 | assert(dc != "", "use DUB or set $DC enviroment variable"); 55 | auto which = executeShell("which " ~ dc); 56 | assert(which.status == 0); 57 | version(DigitalMars) { 58 | auto root = which.output.dirName ~ "/../../src/phobos/"; 59 | } 60 | version(LDC) { 61 | auto root = which.output.dirName ~ "/../import/"; 62 | } 63 | 64 | // plot std.range dependency graph 65 | auto g = libraryDependency(root, "std/range", true); 66 | g.save("range.dot"); 67 | } 68 | ``` 69 | 70 | result `$ dot range.dot -Tpng -O` 71 | 72 | ![range](range.dot.png) 73 | 74 | ## TODO 75 | 76 | - use set ops to remove duplicated nodes and edges 77 | - do string sanity check 78 | - support subgraph 79 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-graphviz", 3 | "authors": [ 4 | "Shigeki Karita" 5 | ], 6 | "description": "Graphviz utility for D", 7 | "copyright": "Copyright © 2018, Shigeki Karita", 8 | "license": "BSL-1.0", 9 | } 10 | -------------------------------------------------------------------------------- /range.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | "std.range.primitives" -> "std.algorithm.comparison" ; 3 | "std.range.interfaces" -> "std.meta" ; 4 | "std.range.package" -> "std.uuid" ; 5 | "std.range.package" -> "std.conv" ; 6 | "std.range.interfaces" -> "std.range" ; 7 | "std.range.package" -> "std.stdio" ; 8 | "std.range.interfaces" -> "std.algorithm.iteration" ; 9 | "std.range.primitives" -> "std.stdio" ; 10 | "std.range.package" -> "std.range.interfaces" ; 11 | "std.range.package" -> "std.algorithm.setops" ; 12 | "std.range.package" -> "std.algorithm.comparison" ; 13 | "std.range.interfaces" -> "std.array" ; 14 | "std.range.package" -> "std.file" ; 15 | "std.range.package" -> "std.random" ; 16 | "std.range.package" -> "std.algorithm.searching" ; 17 | "std.range.package" -> "std.container.dlist" ; 18 | "std.range.primitives" -> "std.meta" ; 19 | "std.range.interfaces" -> "std.range.primitives" ; 20 | "std.range.primitives" -> "std.typecons" ; 21 | "std.range.package" -> "std.array" ; 22 | "std.range.interfaces" -> "std.traits" ; 23 | "std.range.package" -> "std.algorithm.mutation" ; 24 | "std.range.package" -> "std.range" ; 25 | "std.range.package" -> "std.uni" ; 26 | "std.range.interfaces" -> "std.algorithm.comparison" ; 27 | "std.range.package" -> "std.range.primitives" ; 28 | "std.range.primitives" -> "std.algorithm.iteration" ; 29 | "std.range.primitives" -> "std.array" ; 30 | "std.range.package" -> "std.path" ; 31 | "std.range.package" -> "std.string" ; 32 | "std.range.package" -> "std.meta" ; 33 | "std.range.package" -> "std.format" ; 34 | "std.range.primitives" -> "std.algorithm.mutation" ; 35 | "std.range.package" -> "std.math" ; 36 | "std.range.primitives" -> "std.utf" ; 37 | "std.range.primitives" -> "std.format" ; 38 | "std.range.package" -> "std.exception" ; 39 | "std.range.interfaces" -> "std.internal.test" ; 40 | "std.range.primitives" -> "std.conv" ; 41 | "std.range.package" -> "std.typecons" ; 42 | "std.range.package" -> "std.algorithm.sorting" ; 43 | "std.range.package" -> "std.functional" ; 44 | "std.range.primitives" -> "std.range" ; 45 | "std.range.package" -> "std.internal.test" ; 46 | "std.range.primitives" -> "std.internal.test" ; 47 | "std.range.primitives" -> "std.traits" ; 48 | "std.range.primitives" -> "std.encoding" ; 49 | "std.range.interfaces" -> "std.conv" ; 50 | "std.range.package" -> "std.algorithm.iteration" ; 51 | "std.range.package" -> "std.traits" ; 52 | } 53 | -------------------------------------------------------------------------------- /range.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShigekiKarita/d-graphviz/48babeb4a95938a51e575af987ca9bdc2d128099/range.dot.png -------------------------------------------------------------------------------- /simple.dot: -------------------------------------------------------------------------------- 1 | digraph g{ 2 | "A 3 | \"struct\"" [ color = "#ff0000", shape = "box" ]; 4 | "A 5 | \"struct\"" -> "1" [ style = "dashed", label = "a-to-1" ]; 6 | "A 7 | \"struct\"" -> "true" ; 8 | "true" -> "foo" ; 9 | } 10 | -------------------------------------------------------------------------------- /simple.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShigekiKarita/d-graphviz/48babeb4a95938a51e575af987ca9bdc2d128099/simple.dot.png -------------------------------------------------------------------------------- /source/dgraphviz.d: -------------------------------------------------------------------------------- 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 | } 235 | --------------------------------------------------------------------------------