├── .gitignore ├── .travis.yml ├── LICENSE_1_0.txt ├── README.md ├── dub.sdl └── src ├── check.d ├── deps.d ├── graph.d ├── imports.d ├── main.d ├── model.d ├── settings.d └── uml.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | build 3 | dub.selections.json 4 | depend 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 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 | moved to [funkwerk-mobility/depend](https://github.com/funkwerk-mobility/depend) 2 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "depend" 2 | description "Dependency Tool for D" 3 | authors "Mario Kröplin" 4 | copyright "Copyright © 2018, Mario Kröplin" 5 | license "BSL-1.0" 6 | targetType "executable" 7 | buildRequirements "disallowDeprecations" 8 | 9 | configuration "debug" { 10 | targetName "depend" 11 | targetPath "build" 12 | } 13 | configuration "unittest" { 14 | dependency "unit-threaded" version="*" 15 | mainSourceFile "build/ut.d" 16 | excludedSourceFiles "src/main.d" 17 | targetType "executable" 18 | targetName "TestRunner" 19 | targetPath "build" 20 | preBuildCommands "$DUB run --compiler=$$DC unit-threaded -c gen_ut_main -- -f build/ut.d" 21 | } 22 | -------------------------------------------------------------------------------- /src/check.d: -------------------------------------------------------------------------------- 1 | module check; 2 | 3 | import model; 4 | import std.typecons; 5 | version (unittest) import unit_threaded; 6 | 7 | struct Checker 8 | { 9 | private Dependency[] explicitDependencies; 10 | 11 | private Dependency[] implicitDependencies; 12 | 13 | this(Dependency[] targetDependencies, bool simplify) nothrow 14 | { 15 | import std.algorithm : partition; 16 | 17 | bool implict(Dependency dependency) 18 | { 19 | import std.algorithm : any, filter; 20 | 21 | return targetDependencies 22 | .filter!(targetDependency => targetDependency != dependency) 23 | .any!(targetDependency => targetDependency.implies(dependency)); 24 | } 25 | 26 | auto dependencies = targetDependencies.dup; 27 | 28 | if (simplify) 29 | { 30 | this.explicitDependencies = dependencies.partition!implict; 31 | this.implicitDependencies = dependencies[0 .. $ - this.explicitDependencies.length]; 32 | return; 33 | } 34 | this.implicitDependencies = dependencies; 35 | } 36 | 37 | bool allows(Dependency actualDependency)@nogc nothrow 38 | { 39 | import std.algorithm : any; 40 | 41 | return this.explicitDependencies.any!(dependency => actualDependency.implies(dependency)) 42 | || this.implicitDependencies.any!(dependency => actualDependency == dependency); 43 | } 44 | } 45 | 46 | @("check for allowed dependencies") 47 | unittest 48 | { 49 | auto dependencies = [ 50 | Dependency("a", "b"), 51 | Dependency("a.x", "b.y"), 52 | Dependency("b", "c"), 53 | ]; 54 | 55 | with (Checker(dependencies, Yes.simplify)) 56 | { 57 | allows(Dependency("a", "b")).shouldBeTrue; 58 | allows(Dependency("a.x", "b.y")).shouldBeTrue; 59 | allows(Dependency("b", "c")).shouldBeTrue; 60 | allows(Dependency("b.x", "c")).shouldBeTrue; // implies explicit dependency 61 | 62 | allows(Dependency("a.x", "b")).shouldBeFalse; // implies implicit dependency 63 | } 64 | } 65 | 66 | bool implies(Dependency lhs, Dependency rhs) @nogc nothrow 67 | { 68 | import std.algorithm : startsWith; 69 | 70 | return lhs.client.names.startsWith(rhs.client.names) 71 | && lhs.supplier.names.startsWith(rhs.supplier.names); 72 | } 73 | 74 | @("check for implied dependencies") 75 | unittest 76 | { 77 | Dependency("a", "b").implies(Dependency("a", "b")).shouldBeTrue; 78 | Dependency("a.x", "b.y").implies(Dependency("a", "b")).shouldBeTrue; 79 | 80 | Dependency("a.x", "b").implies(Dependency("a", "b.y")).shouldBeFalse; 81 | Dependency("aa", "bb").implies(Dependency("a", "b")).shouldBeFalse; 82 | } 83 | -------------------------------------------------------------------------------- /src/deps.d: -------------------------------------------------------------------------------- 1 | module deps; 2 | 3 | import model; 4 | import std.range; 5 | import std.regex; 6 | import std.stdio; 7 | import std.typecons; 8 | version (unittest) import unit_threaded; 9 | 10 | auto moduleDependencies(alias predicate)(File file) 11 | { 12 | import std.algorithm : filter, map; 13 | 14 | return reader(file.byLine) 15 | .filter!predicate 16 | .map!(dependency => Dependency(dependency.client.name, dependency.supplier.name)); 17 | } 18 | 19 | auto reader(R)(R input) 20 | { 21 | return Reader!R(input); 22 | } 23 | 24 | struct Reader(R) 25 | if (isInputRange!R) 26 | { 27 | alias Module = Tuple!(string, "name", string, "path"); 28 | alias Dependency = Tuple!(Module, "client", Module, "supplier"); 29 | 30 | private R input; 31 | 32 | public bool empty = false; 33 | 34 | public Dependency front; 35 | 36 | private this(R input) 37 | { 38 | this.input = input; 39 | popFront; 40 | } 41 | 42 | public void popFront() 43 | { 44 | import std.conv : to; 45 | import std.regex : matchFirst, regex; 46 | 47 | enum pattern = regex(`^(depsImport\s)?` 48 | ~ `(?P[\w.]+)\s\((?P.*)\)` 49 | ~ `\s:[^:]*:\s` 50 | ~ `(?P[\w.]+)\s\((?P.*)\)`); 51 | 52 | while (!this.input.empty) 53 | { 54 | auto captures = this.input.front.matchFirst(pattern); 55 | 56 | scope (exit) 57 | this.input.popFront; 58 | 59 | if (captures) 60 | { 61 | with (this.front.client) 62 | { 63 | name = captures["clientName"].to!string; 64 | path = captures["clientPath"].to!string; 65 | } 66 | with (this.front.supplier) 67 | { 68 | name = captures["supplierName"].to!string; 69 | path = captures["supplierPath"].to!string; 70 | } 71 | return; 72 | } 73 | } 74 | this.empty = true; 75 | } 76 | } 77 | 78 | @("read module dependencies") 79 | unittest 80 | { 81 | const line = "depend (src/depend.d) : private : object (/usr/include/dmd/druntime/import/object.di)"; 82 | const client = tuple("depend", "src/depend.d"); 83 | const supplier = tuple("object", "/usr/include/dmd/druntime/import/object.di"); 84 | 85 | reader(only(line)).should.be == only(tuple(client, supplier)); 86 | } 87 | -------------------------------------------------------------------------------- /src/graph.d: -------------------------------------------------------------------------------- 1 | module graph; 2 | 3 | import model; 4 | import std.algorithm; 5 | import std.range; 6 | import std.typecons; 7 | version (unittest) import unit_threaded; 8 | 9 | void write(Output)(auto ref Output output, Dependency[] dependencies) 10 | { 11 | import std.format : formattedWrite; 12 | 13 | output.put("digraph Dependencies {\n"); 14 | output.put("node [shape=box];\n"); 15 | foreach (element; dependencies.elements) 16 | { 17 | output.formattedWrite!(`"%s"`)(element); 18 | output.put('\n'); 19 | } 20 | foreach (dependency; dependencies) 21 | { 22 | output.formattedWrite!(`"%s" -> "%s"`)(dependency.client, dependency.supplier); 23 | output.put('\n'); 24 | } 25 | output.put("}\n"); 26 | } 27 | 28 | @("write dependency graph in the DOT language") 29 | unittest 30 | { 31 | import std.array : appender; 32 | import std.string : outdent, stripLeft; 33 | 34 | auto output = appender!string; 35 | auto dependencies = [Dependency("a", "b")]; 36 | 37 | output.write(dependencies); 38 | 39 | const expected = ` 40 | digraph Dependencies { 41 | node [shape=box]; 42 | "a" 43 | "b" 44 | "a" -> "b" 45 | } 46 | `; 47 | 48 | output.data.should.be == outdent(expected).stripLeft; 49 | } 50 | 51 | void transitiveClosure(ref Dependency[] dependencies) nothrow 52 | { 53 | FullyQualifiedName[] elements = dependencies.elements; 54 | 55 | foreach (element; elements) 56 | foreach (client; elements) 57 | if (dependencies.canFind(Dependency(client, element))) 58 | foreach (supplier; elements) 59 | if (dependencies.canFind(Dependency(element, supplier))) 60 | dependencies.add(Dependency(client, supplier)); 61 | } 62 | 63 | Dependency[] transitiveReduction(ref Dependency[] dependencies) 64 | { 65 | bool[FullyQualifiedName] mark = null; 66 | Dependency[] cyclicDependencies = null; 67 | 68 | void traverse(FullyQualifiedName node) 69 | { 70 | import std.array : array; 71 | 72 | mark[node] = true; 73 | foreach (outEdge; dependencies.filter!(a => a.client == node).array) 74 | { 75 | if (!dependencies.canFind(outEdge)) 76 | continue; 77 | if (mark.get(outEdge.supplier, false)) 78 | { 79 | cyclicDependencies.add(outEdge); 80 | continue; 81 | } 82 | foreach (inEdge; dependencies.filter!(a => a.supplier == outEdge.supplier).array) 83 | { 84 | if (inEdge == outEdge) 85 | continue; 86 | if (mark.get(inEdge.client, false)) 87 | dependencies = dependencies.remove!(a => a == inEdge); 88 | } 89 | traverse(outEdge.supplier); 90 | } 91 | mark[node] = false; 92 | } 93 | 94 | foreach (element; dependencies.elements) 95 | traverse(element); 96 | 97 | return cyclicDependencies; 98 | } 99 | 100 | @("apply transitive reduction") 101 | unittest 102 | { 103 | auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("a", "c")]; 104 | auto cyclicDependencies = transitiveReduction(dependencies); 105 | 106 | dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c")]; 107 | cyclicDependencies.shouldBeEmpty; 108 | } 109 | 110 | @("apply transitive reduction to cyclic dependencies") 111 | unittest 112 | { 113 | auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")]; 114 | auto cyclicDependencies = transitiveReduction(dependencies); 115 | 116 | dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")]; 117 | cyclicDependencies.sort.should.be == dependencies; 118 | } 119 | 120 | FullyQualifiedName[] elements(Dependency[] dependencies) nothrow 121 | { 122 | FullyQualifiedName[] elements = null; 123 | 124 | foreach (dependency; dependencies) 125 | { 126 | elements.add(dependency.client); 127 | elements.add(dependency.supplier); 128 | } 129 | return elements; 130 | } 131 | 132 | void add(Element)(ref Element[] elements, Element element) 133 | { 134 | if (!elements.canFind(element)) 135 | elements ~= element; 136 | } 137 | -------------------------------------------------------------------------------- /src/imports.d: -------------------------------------------------------------------------------- 1 | module imports; 2 | 3 | import model; 4 | import std.algorithm; 5 | import std.array; 6 | import std.range; 7 | import std.typecons; 8 | 9 | version (unittest) import unit_threaded; 10 | 11 | auto mutualDependencies(const string[] args) 12 | { 13 | string[][string] importedModules; 14 | 15 | foreach (arg; args) 16 | with (readImports(arg)) 17 | importedModules[client] ~= suppliers; 18 | return importedModules.byKeyValue 19 | .map!(pair => pair.value.map!(supplier => Dependency(pair.key, supplier))) 20 | .joiner 21 | .filter!(dependency => dependency.supplier.toString in importedModules); 22 | } 23 | 24 | auto readImports(string file) 25 | { 26 | import std.file : readText; 27 | import std.path : baseName, stripExtension; 28 | 29 | const input = file.readText; 30 | auto captures = moduleDeclaration(input); 31 | const client = captures 32 | ? captures["fullyQualifiedName"].toFullyQualifiedName 33 | : file.baseName.stripExtension; 34 | const suppliers = importDeclarations(input) 35 | .map!(captures => captures["fullyQualifiedName"].toFullyQualifiedName) 36 | .array; 37 | 38 | return tuple!("client", "suppliers")(client, suppliers); 39 | } 40 | 41 | auto moduleDeclaration(R)(R input) 42 | { 43 | import std.regex : matchFirst, regex; 44 | 45 | // TODO: skip comments, string literals 46 | enum pattern = regex(`\bmodule\s+` ~ fullyQualifiedName ~ `\s*;`); 47 | 48 | return input.matchFirst(pattern); 49 | } 50 | 51 | @("match module declaration") 52 | unittest 53 | { 54 | auto captures = moduleDeclaration("module bar.baz;"); 55 | 56 | captures.shouldBeTrue; 57 | captures["fullyQualifiedName"].should.be == "bar.baz"; 58 | } 59 | 60 | @("match module declaration with white space") 61 | unittest 62 | { 63 | auto captures = moduleDeclaration("module bar . baz\n;"); 64 | 65 | captures.shouldBeTrue; 66 | captures["fullyQualifiedName"].should.be == "bar . baz"; 67 | } 68 | 69 | auto importDeclarations(R)(R input) 70 | { 71 | import std.regex : matchAll, regex; 72 | 73 | // TODO: skip comments, string literals 74 | enum pattern = regex(`\bimport\s+(\w+\s*=\s*)?` ~ fullyQualifiedName ~ `[^;]*;`); 75 | 76 | return input.matchAll(pattern); 77 | } 78 | 79 | @("match import declaration") 80 | unittest 81 | { 82 | auto match = importDeclarations("import bar.baz;"); 83 | 84 | match.shouldBeTrue; 85 | match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar.baz"]); 86 | } 87 | 88 | @("match import declaration with white space") 89 | unittest 90 | { 91 | auto match = importDeclarations("import bar . baz\n;"); 92 | 93 | match.shouldBeTrue; 94 | match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar . baz"]); 95 | } 96 | 97 | @("match renamed import") 98 | unittest 99 | { 100 | auto match = importDeclarations("import foo = bar.baz;"); 101 | 102 | match.shouldBeTrue; 103 | match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar.baz"]); 104 | } 105 | 106 | enum fullyQualifiedName = `(?P\w+(\s*\.\s*\w+)*)`; 107 | 108 | string toFullyQualifiedName(string text) 109 | { 110 | import std.string : join, strip; 111 | 112 | return text.splitter('.').map!strip.join('.'); 113 | } 114 | 115 | @("convert text to fully-qualified name") 116 | unittest 117 | { 118 | "bar . baz".toFullyQualifiedName.should.be == "bar.baz"; 119 | } 120 | -------------------------------------------------------------------------------- /src/main.d: -------------------------------------------------------------------------------- 1 | // Copyright Mario Kröplin 2020. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | module main; 6 | 7 | import core.stdc.stdlib; 8 | import deps; 9 | import graph; 10 | import imports; 11 | import model; 12 | import settings : readSettings = read; 13 | import std.algorithm; 14 | import std.array; 15 | import std.exception; 16 | import std.range; 17 | import std.stdio; 18 | import std.typecons; 19 | import uml; 20 | 21 | void main(string[] args) 22 | { 23 | const settings = readSettings(args); 24 | 25 | with (settings) 26 | { 27 | auto readDependencies(File file) 28 | { 29 | import std.regex : matchFirst, Regex; 30 | 31 | if (pattern != Regex!char()) 32 | { 33 | bool matches(T)(T dependency) 34 | { 35 | with (dependency) 36 | return client.path.matchFirst(pattern) 37 | && supplier.path.matchFirst(pattern); 38 | } 39 | 40 | return moduleDependencies!(dependency => matches(dependency))(file).array; 41 | } 42 | else 43 | { 44 | bool matches(T)(T dependency) 45 | { 46 | with (dependency) 47 | return unrecognizedArgs.canFind(client.path) 48 | && unrecognizedArgs.canFind(supplier.path); 49 | } 50 | 51 | return moduleDependencies!(dependency => matches(dependency))(file).array; 52 | } 53 | } 54 | 55 | Dependency[] actualDependencies; 56 | 57 | if (compiler.empty) 58 | { 59 | actualDependencies = mutualDependencies(unrecognizedArgs).array; 60 | } 61 | else 62 | { 63 | import std.process : pipeProcess, Redirect, wait; 64 | 65 | const args_ = [compiler, "-deps", "-o-"] ~ unrecognizedArgs; 66 | auto pipes = pipeProcess(args_, Redirect.stdout); 67 | 68 | scope (exit) 69 | { 70 | auto status = wait(pipes.pid); 71 | 72 | if (status != 0) 73 | exit(EXIT_FAILURE); 74 | } 75 | 76 | actualDependencies = readDependencies(pipes.stdout); 77 | } 78 | actualDependencies ~= depsFiles 79 | .map!(depsFile => readDependencies(File(depsFile))) 80 | .join; 81 | actualDependencies ~= umlFiles 82 | .map!(umlFile => read(File(umlFile).byLine)) 83 | .join; 84 | actualDependencies = actualDependencies.sort.uniq.array; 85 | if (!targetFiles.empty) 86 | { 87 | import check : Checker; 88 | import uml : read; 89 | 90 | bool success = true; 91 | Dependency[] targetDependencies = null; 92 | 93 | foreach (targetFile; targetFiles) 94 | targetDependencies ~= read(File(targetFile).byLine); 95 | 96 | if (!transitive) 97 | targetDependencies.transitiveClosure; 98 | 99 | auto checker = Checker(targetDependencies, simplify); 100 | 101 | foreach (dependency; actualDependencies) 102 | { 103 | auto client = dependency.client; 104 | auto supplier = dependency.supplier; 105 | 106 | if (detail) 107 | dependency = Dependency(client, supplier); 108 | else 109 | { 110 | dependency = Dependency(client.packages, supplier.packages); 111 | if (dependency.client.names.empty || dependency.supplier.names.empty 112 | || dependency.client == dependency.supplier) 113 | continue; 114 | } 115 | if (!checker.allows(dependency)) 116 | { 117 | stderr.writefln("error: unintended dependency %s -> %s", client, supplier); 118 | success = false; 119 | } 120 | } 121 | if (!success) 122 | exit(EXIT_FAILURE); 123 | } 124 | if (dot || targetFiles.empty) 125 | { 126 | Dependency[] dependencies_ = null; 127 | 128 | if (detail) 129 | dependencies_ = actualDependencies; 130 | else 131 | { 132 | foreach (dependency; actualDependencies) 133 | { 134 | const client = dependency.client.packages; 135 | const supplier = dependency.supplier.packages; 136 | 137 | if (!client.empty && !supplier.empty && client != supplier) 138 | dependencies_.add(Dependency(client, supplier)); 139 | } 140 | } 141 | if (!transitive) 142 | { 143 | auto cyclicDependencies = transitiveReduction(dependencies_); 144 | 145 | if (!cyclicDependencies.empty) 146 | { 147 | stderr.writeln("warning: cyclic dependencies"); 148 | foreach (dependency; cyclicDependencies.sort) 149 | stderr.writefln!"%s -> %s"(dependency.client, dependency.supplier); 150 | } 151 | } 152 | if (dot) 153 | { 154 | import graph : write; 155 | import std.stdio : stdout; 156 | 157 | stdout.lockingTextWriter.write(dependencies_); 158 | } 159 | else 160 | { 161 | import uml : write; 162 | import std.stdio : stdout; 163 | 164 | stdout.lockingTextWriter.write(dependencies_); 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/model.d: -------------------------------------------------------------------------------- 1 | module model; 2 | 3 | version (unittest) import unit_threaded; 4 | 5 | struct Dependency 6 | { 7 | FullyQualifiedName client; 8 | 9 | FullyQualifiedName supplier; 10 | 11 | this(string client, string supplier) 12 | { 13 | this(FullyQualifiedName(client), FullyQualifiedName(supplier)); 14 | } 15 | 16 | this(FullyQualifiedName client, FullyQualifiedName supplier) @nogc nothrow 17 | { 18 | this.client = client; 19 | this.supplier = supplier; 20 | } 21 | 22 | int opCmp(ref const Dependency that) const @nogc nothrow pure @safe 23 | { 24 | import std.algorithm : cmp; 25 | 26 | int result = cmp(this.client.names, that.client.names); 27 | 28 | if (result == 0) 29 | { 30 | result = cmp(this.supplier.names, that.supplier.names); 31 | } 32 | return result; 33 | } 34 | } 35 | 36 | struct FullyQualifiedName 37 | { 38 | string[] names; 39 | 40 | this(string name) 41 | { 42 | import std.string : split; 43 | 44 | this(name.split('.')); 45 | } 46 | 47 | this(string[] names) nothrow 48 | { 49 | this.names = names.dup; 50 | } 51 | 52 | string toString() const pure @safe 53 | { 54 | import std.string : join; 55 | 56 | return this.names.join('.'); 57 | } 58 | } 59 | 60 | string packages(FullyQualifiedName fullyQualifiedName) pure @safe 61 | { 62 | import std.algorithm : splitter; 63 | import std.array : join; 64 | import std.range : dropBackOne; 65 | 66 | return fullyQualifiedName.names 67 | .dropBackOne 68 | .join('.'); 69 | } 70 | 71 | @("split packages from a fully-qualified module name") 72 | unittest 73 | { 74 | packages(FullyQualifiedName("bar.baz.foo")).should.be == "bar.baz"; 75 | packages(FullyQualifiedName("foo")).shouldBeEmpty; 76 | } 77 | -------------------------------------------------------------------------------- /src/settings.d: -------------------------------------------------------------------------------- 1 | module settings; 2 | 3 | import core.stdc.stdlib; 4 | import model; 5 | import std.range; 6 | import std.regex; 7 | import std.stdio; 8 | version (unittest) import unit_threaded; 9 | 10 | struct Settings 11 | { 12 | string compiler; 13 | string[] depsFiles; 14 | bool scan; 15 | string[] umlFiles; 16 | Regex!char pattern; 17 | bool detail; 18 | bool transitive; 19 | bool dot; 20 | string[] targetFiles; 21 | bool simplify; 22 | string[] unrecognizedArgs; 23 | } 24 | 25 | Settings read(string[] args) 26 | in (!args.empty) 27 | { 28 | import std.exception : enforce; 29 | import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult; 30 | 31 | Settings settings; 32 | 33 | with (settings) 34 | { 35 | string filter; 36 | GetoptResult result; 37 | 38 | try 39 | { 40 | result = getopt(args, 41 | config.passThrough, 42 | "compiler|c", "Specify the compiler to use", &compiler, 43 | "deps", "Read module dependencies from file", &depsFiles, 44 | "uml", "Read dependencies from PlantUML file", ¨Files, 45 | "filter", "Filter source files matching the regular expression", &filter, 46 | "detail", "Inspect dependencies between modules instead of packages", &detail, 47 | "transitive|t", "Keep transitive dependencies", &transitive, 48 | "dot", "Write dependency graph in the DOT language", &dot, 49 | "check", "Check against the PlantUML target dependencies", &targetFiles, 50 | "simplify", "Use simplifying assumptions for the check (experimental)", &simplify, 51 | ); 52 | if (!filter.empty) 53 | { 54 | enforce(!compiler.empty || !depsFiles.empty, 55 | "filter can only be applied to dependencies collected by a compiler"); 56 | 57 | pattern = regex(filter); 58 | } 59 | } 60 | catch (Exception exception) 61 | { 62 | stderr.writeln("error: ", exception.msg); 63 | exit(EXIT_FAILURE); 64 | } 65 | if (result.helpWanted) 66 | { 67 | import std.path : baseName; 68 | 69 | writefln("Usage: %s [options] files", args.front.baseName); 70 | writeln("Process import dependencies as created by dmd with the --deps switch."); 71 | writeln("If no compiler is specified, source files are scanned for (simple) import declarations."); 72 | defaultGetoptPrinter("Options:", result.options); 73 | exit(EXIT_SUCCESS); 74 | } 75 | unrecognizedArgs = args.dropOne; 76 | } 77 | return settings; 78 | } 79 | 80 | @("read settings") 81 | unittest 82 | { 83 | const settings = read(["depend", "--deps", "dependencies", "--check", "target"]); 84 | 85 | with (settings) 86 | { 87 | depsFiles.should.be == ["dependencies"]; 88 | targetFiles.should.be == ["target"]; 89 | } 90 | } 91 | 92 | @("read settings with unrecognized arguments") 93 | unittest 94 | { 95 | const settings = read(["depend", "main.d", "--detail"]); 96 | 97 | with (settings) 98 | { 99 | unrecognizedArgs.should.be == ["main.d"]; 100 | detail.should.be == true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/uml.d: -------------------------------------------------------------------------------- 1 | module uml; 2 | 3 | import model; 4 | import std.algorithm; 5 | import std.range; 6 | import std.stdio; 7 | import std.typecons; 8 | version (unittest) import unit_threaded; 9 | 10 | Dependency[] read(R)(R input) 11 | { 12 | import std.array : appender; 13 | 14 | auto output = appender!(Dependency[]); 15 | 16 | read(input, output); 17 | return output.data; 18 | } 19 | 20 | private void read(Input, Output)(Input input, auto ref Output output) 21 | { 22 | import std.conv : to; 23 | import std.regex : matchFirst, regex; 24 | 25 | enum arrow = `(?P?)`; 26 | enum pattern = regex(`(?P\w+(.\w+)*)\s*` ~ arrow ~ `\s*(?P\w+(.\w+)*)`); 27 | 28 | foreach (line; input) 29 | { 30 | auto captures = line.matchFirst(pattern); 31 | 32 | if (captures) 33 | { 34 | const lhs = captures["lhs"].to!string; 35 | const rhs = captures["rhs"].to!string; 36 | 37 | if (captures["arrow"].endsWith(">")) 38 | output.put(Dependency(lhs, rhs)); 39 | if (captures["arrow"].startsWith("<")) 40 | output.put(Dependency(rhs, lhs)); 41 | } 42 | } 43 | } 44 | 45 | @("read PlantUML dependencies") 46 | unittest 47 | { 48 | read(only("a .> b")).should.be == [Dependency("a", "b")]; 49 | read(only("a <. b")).should.be == [Dependency("b", "a")]; 50 | read(only("a <.> b")).should.be == [Dependency("a", "b"), Dependency("b", "a")]; 51 | read(only("a.[#red]>b")).should.be == [Dependency("a", "b")]; 52 | read(only("a.[#red]le>b")).should.be == [Dependency("a", "b")]; 53 | } 54 | 55 | void write(Output)(auto ref Output output, Dependency[] dependencies) 56 | { 57 | Package hierarchy; 58 | 59 | dependencies.each!(dependency => hierarchy.add(dependency)); 60 | 61 | output.put("@startuml\n"); 62 | hierarchy.write(output); 63 | output.put("@enduml\n"); 64 | } 65 | 66 | @("write PlantUML package diagram") 67 | unittest 68 | { 69 | import std.array : appender; 70 | import std.string : outdent, stripLeft; 71 | 72 | auto output = appender!string; 73 | auto dependencies = [Dependency("a", "b")]; 74 | 75 | output.write(dependencies); 76 | 77 | const expected = ` 78 | @startuml 79 | package a {} 80 | package b {} 81 | 82 | a ..> b 83 | @enduml 84 | `; 85 | 86 | output.data.should.be == outdent(expected).stripLeft; 87 | } 88 | 89 | @("place internal dependencies inside the package") 90 | unittest 91 | { 92 | import std.array : appender; 93 | import std.string : outdent, stripLeft; 94 | 95 | auto output = appender!string; 96 | auto dependencies = [Dependency("a", "a.b"), Dependency("a.b", "a.c")]; 97 | 98 | output.write(dependencies); 99 | 100 | const expected = ` 101 | @startuml 102 | package a { 103 | package b as a.b {} 104 | package c as a.c {} 105 | 106 | a.b ..> a.c 107 | } 108 | 109 | a ..> a.b 110 | @enduml 111 | `; 112 | 113 | output.data.should.be == outdent(expected).stripLeft; 114 | } 115 | 116 | private struct Package 117 | { 118 | string[] path; 119 | 120 | Package[string] subpackages; 121 | 122 | Dependency[] dependencies; 123 | 124 | void add(Dependency dependency) nothrow 125 | { 126 | const clientPath = dependency.client.names; 127 | const supplierPath = dependency.supplier.names; 128 | const path = commonPrefix(clientPath.dropBackOne, supplierPath.dropBackOne); 129 | 130 | addPackage(clientPath); 131 | addPackage(supplierPath); 132 | addDependency(path, dependency); 133 | } 134 | 135 | void addPackage(const string[] path, size_t index = 0) nothrow 136 | { 137 | if (path[index] !in subpackages) 138 | subpackages[path[index]] = Package(path[0 .. index + 1].dup); 139 | if (index + 1 < path.length) 140 | subpackages[path[index]].addPackage(path, index + 1); 141 | } 142 | 143 | void addDependency(const string[] path, Dependency dependency) nothrow 144 | { 145 | if (path.empty) 146 | dependencies ~= dependency; 147 | else 148 | subpackages[path.front].addDependency(path.dropOne, dependency); 149 | } 150 | 151 | void write(Output)(auto ref Output output, size_t level = 0) 152 | { 153 | import std.format : formattedWrite; 154 | 155 | void indent() 156 | { 157 | foreach (_; 0 .. level) 158 | output.put(" "); 159 | } 160 | 161 | foreach (subpackage; subpackages.keys.sort.map!(key => subpackages[key])) 162 | { 163 | indent; 164 | if (subpackage.path.length == 1) 165 | output.formattedWrite!"package %s {"(subpackage.path.join('.')); 166 | else 167 | output.formattedWrite!"package %s as %s {"(subpackage.path.back, subpackage.path.join('.')); 168 | 169 | if (!subpackage.subpackages.empty || !subpackage.dependencies.empty) 170 | { 171 | output.put('\n'); 172 | subpackage.write(output, level + 1); 173 | indent; 174 | } 175 | output.put("}\n"); 176 | } 177 | if (!dependencies.empty) 178 | output.put('\n'); 179 | foreach (dependency; dependencies.sort) 180 | { 181 | indent; 182 | output.formattedWrite!"%s ..> %s\n"(dependency.client, dependency.supplier); 183 | } 184 | } 185 | } 186 | --------------------------------------------------------------------------------