├── .gitignore
├── LICENSE
├── README.md
├── deps-lock.txt
├── deps.dot
├── deps.png
├── deps.svg
├── deps.txt
├── dub.json
├── dub.selections.json
├── screenshot
├── README.md
├── rx-deps-nostd.png
├── rx-deps-nostd.svg
├── rx-deps-nosubject.png
├── rx-deps-nosubject.svg
├── rx-deps.png
└── rx-deps.svg
└── source
└── app.d
/.gitignore:
--------------------------------------------------------------------------------
1 | .dub
2 | docs.json
3 | __dummy.html
4 | docs/
5 | /graphviz
6 | graphviz.so
7 | graphviz.dylib
8 | graphviz.dll
9 | graphviz.a
10 | graphviz.lib
11 | graphviz-test-*
12 | *.exe
13 | *.o
14 | *.obj
15 | *.lst
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DDeps
2 |
3 | Source review support tool
4 |
5 | A tool to create a module dependency graph for the D language.
6 | The feature is that you can record snapshots in two versions and compare them to visualize the differences.
7 |
8 | # Screenshot (Example)
9 |
10 | **basic**
11 |
12 | 
13 |
14 | **no core, no std**
15 |
16 | 
17 |
18 | **exclude rx.subject**
19 |
20 | 
21 |
22 | # Requirements
23 | 1. dub
24 | 2. Graphviz (for visualize)
25 |
26 | # Settings
27 |
28 | ## For library (example)
29 | ```json
30 | "configurations": [
31 | {
32 | "name": "default"
33 | },
34 | {
35 | "name": "diff",
36 | "postGenerateCommands": [
37 | "dub build -c makedeps",
38 | "dub fetch ddeps",
39 | "dub run ddeps -- --focus=rx -o deps.dot",
40 | "dot -Tsvg -odeps.svg deps.dot"
41 | ]
42 | },
43 | {
44 | "name": "diff-update",
45 | "postGenerateCommands": [
46 | "dub fetch ddeps",
47 | "dub run ddeps -- --update"
48 | ]
49 | },
50 | {
51 | "name": "makedeps",
52 | "dflags": ["-deps=deps.txt"]
53 | }
54 | ]
55 | ```
56 |
57 | ## For executable
58 | ```json
59 | "configurations": [
60 | {
61 | "name": "default"
62 | },
63 | {
64 | "name": "diff",
65 | "postGenerateCommands": [
66 | "dub build -c makedeps",
67 | "dub fetch ddeps",
68 | "dub run ddeps -- -o deps.dot",
69 | "dot -Tsvg -odeps.svg deps.dot"
70 | ]
71 | },
72 | {
73 | "name": "diff-update",
74 | "postGenerateCommands": [
75 | "dub fetch ddeps",
76 | "dub run ddeps -- --update"
77 | ]
78 | },
79 | {
80 | "name": "makedeps",
81 | "dflags": ["-deps=deps.txt"]
82 | }
83 | ]
84 | ```
85 |
86 | # Usage
87 |
88 | ## At first
89 | create lock file
90 |
91 | ```bash
92 | dub build -c makedeps
93 | dub build -c diff-update
94 | ```
95 |
96 | ## Basic
97 | 1. Modify source
98 | 2. Update diff
99 | - `dub build -c diff`
100 | 3. Do review with the dependency graph diff
101 | - Open the `deps.svg` in browser
102 |
103 | ## Compare 2 versions
104 |
105 | 1. checkout a target version
106 | - `git reset --hard XXX` or `git checkout XXXXX`
107 | 2. reset to source version
108 | - `git reset --hard HEAD~10` (e.g. 10 versions ago)
109 | 3. create `deps-lock.txt`
110 | - `dub build -c makedeps`
111 | - `dub build -c diff-update`
112 | - if `dub.json` / `dub.sdl` has not configure then add these.
113 | 4. reset to target version
114 | - `git reset --hard ORIG_HEAD`
115 | 5. make diff
116 | - `dub build -c diff`
117 | 6. open `deps.svg`
118 |
119 |
120 | # Arguments
121 |
122 | | name | Usage | description | default |
123 | |:-----|:------------|:--|:--|
124 | | input | `-i XXX` or `--input=XXX` | deps file name | `deps.txt` |
125 | | output | `-o XXX` or `--output=XXX` | destination file name | write to stdout |
126 | | update | `-u` or `--update` | update lock file | false |
127 | | lock | `-l XXX` or `--lock=XXX` | lock file name | `deps-lock.txt` |
128 | | focus | `-f XXX` or `--focus=XXX` | filtering target by name | `app` |
129 | | depth | `-d N` or `--depth=N` | search depth | 1 |
130 | | exclude | `-e XXX [-e YYY]` or `--exclude=XXX [--exclude=YYY]` | exclude module names | `object` |
131 | | help | `--help` | show help | |
132 |
--------------------------------------------------------------------------------
/deps.dot:
--------------------------------------------------------------------------------
1 | digraph {
2 | {
3 | "app"
4 | "core.bitop"
5 | "core.checkedint"
6 | "core.exception"
7 | "core.internal.traits"
8 | "core.memory"
9 | "core.stdc.string"
10 | "std.algorithm"
11 | "std.algorithm.comparison"
12 | "std.algorithm.iteration"
13 | "std.algorithm.mutation"
14 | "std.algorithm.searching"
15 | "std.array"
16 | "std.ascii"
17 | "std.container.rbtree"
18 | "std.conv"
19 | "std.encoding"
20 | "std.exception"
21 | "std.file"
22 | "std.format"
23 | "std.functional"
24 | "std.getopt"
25 | "std.internal.memory"
26 | "std.internal.unicode_tables"
27 | "std.meta"
28 | "std.range"
29 | "std.range.primitives"
30 | "std.stdio"
31 | "std.system"
32 | "std.traits"
33 | "std.typecons"
34 | "std.uni"
35 | "std.utf"
36 | }
37 | "app" -> "core.bitop";
38 | "app" -> "core.checkedint";
39 | "app" -> "core.exception";
40 | "app" -> "core.internal.traits";
41 | "app" -> "core.memory";
42 | "app" -> "core.stdc.string";
43 | "app" -> "std.algorithm";
44 | "app" -> "std.algorithm.comparison";
45 | "app" -> "std.algorithm.iteration";
46 | "app" -> "std.algorithm.mutation";
47 | "app" -> "std.algorithm.searching";
48 | "app" -> "std.array";
49 | "app" -> "std.ascii";
50 | "app" -> "std.container.rbtree";
51 | "app" -> "std.conv";
52 | "app" -> "std.encoding";
53 | "app" -> "std.exception";
54 | "app" -> "std.file";
55 | "app" -> "std.format";
56 | "app" -> "std.functional";
57 | "app" -> "std.getopt";
58 | "app" -> "std.internal.memory";
59 | "app" -> "std.internal.unicode_tables";
60 | "app" -> "std.meta";
61 | "app" -> "std.range";
62 | "app" -> "std.range.primitives";
63 | "app" -> "std.stdio";
64 | "app" -> "std.system";
65 | "app" -> "std.traits";
66 | "app" -> "std.typecons";
67 | "app" -> "std.uni";
68 | "app" -> "std.utf";
69 | }
70 |
--------------------------------------------------------------------------------
/deps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lempiji/ddeps/abe8c4a5d9c56889e923dff7abd1c0a0064beb6a/deps.png
--------------------------------------------------------------------------------
/deps.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
339 |
--------------------------------------------------------------------------------
/dub.json:
--------------------------------------------------------------------------------
1 | {
2 | "authors": [
3 | "lempiji"
4 | ],
5 | "description": "Source review support tool.",
6 | "license": "MIT",
7 | "name": "ddeps",
8 | "configurations": [
9 | {
10 | "name": "default",
11 | "targetType": "executable"
12 | },
13 | {
14 | "name": "diff",
15 | "postGenerateCommands": [
16 | "dub build -c makedeps",
17 | "dub fetch ddeps",
18 | "dub run ddeps -- --output=deps.dot",
19 | "dot -Tsvg -odeps.svg deps.dot",
20 | "dot -Tpng -odeps.png deps.dot"
21 | ]
22 | },
23 | {
24 | "name": "diff-update",
25 | "postGenerateCommands": [
26 | "dub fetch ddeps",
27 | "dub run ddeps -- --update"
28 | ]
29 | },
30 | {
31 | "name": "makedeps",
32 | "dflags": ["-deps=deps.txt"]
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/dub.selections.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileVersion": 1,
3 | "versions": {
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/screenshot/README.md:
--------------------------------------------------------------------------------
1 | # Settings
2 |
3 | | name | command |
4 | |:--------------------|:------|
5 | | `rx-deps` | `dub run ddeps -- -f rx -o deps.dot` |
6 | | `rx-deps-nostd` | `dub run ddeps -- -f rx -o deps.dot -e std -e core` |
7 | | `rx-deps-nosubject` | `dub run ddeps -- -f rx -o deps.dot -e std -e core -e rx.subject` |
--------------------------------------------------------------------------------
/screenshot/rx-deps-nostd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lempiji/ddeps/abe8c4a5d9c56889e923dff7abd1c0a0064beb6a/screenshot/rx-deps-nostd.png
--------------------------------------------------------------------------------
/screenshot/rx-deps-nostd.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
649 |
--------------------------------------------------------------------------------
/screenshot/rx-deps-nosubject.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lempiji/ddeps/abe8c4a5d9c56889e923dff7abd1c0a0064beb6a/screenshot/rx-deps-nosubject.png
--------------------------------------------------------------------------------
/screenshot/rx-deps-nosubject.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
614 |
--------------------------------------------------------------------------------
/screenshot/rx-deps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lempiji/ddeps/abe8c4a5d9c56889e923dff7abd1c0a0064beb6a/screenshot/rx-deps.png
--------------------------------------------------------------------------------
/screenshot/rx-deps.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
984 |
--------------------------------------------------------------------------------
/source/app.d:
--------------------------------------------------------------------------------
1 | import std.stdio;
2 | import std.algorithm;
3 | import std.conv;
4 | import std.container.rbtree;
5 | import std.array;
6 | import std.file;
7 | import std.getopt;
8 | import std.range;
9 |
10 | version (unittest)
11 | {
12 | }
13 | else
14 | {
15 | int main(string[] args)
16 | {
17 | string outfile;
18 | string depsfile = "deps.txt";
19 | string lockfile = "deps-lock.txt";
20 | string focusName = "app";
21 | int filterCost = 1;
22 | bool forceUpdate;
23 | string[] excludeNames = ["object"]; // default exclude
24 |
25 | // dfmt off
26 | auto helpInformation = getopt(args,
27 | "i|input", "deps file name", &depsfile,
28 | "o|output", "graph file name.\n\tIf not specified, it is standard output.", &outfile,
29 | "u|update", "update lock file", &forceUpdate,
30 | "l|lock", "lock file name", &lockfile,
31 | "f|focus", "filtering target name", &focusName,
32 | "d|depth", "depth for dependency search", &filterCost,
33 | "e|exclude", "exclude module names", &excludeNames
34 | );
35 | // dfmt on
36 |
37 | if (helpInformation.helpWanted)
38 | {
39 | defaultGetoptPrinter("", helpInformation.options);
40 | return 0;
41 | }
42 |
43 | if (forceUpdate)
44 | {
45 | writefln!"copy lockfile (%s)"(lockfile);
46 | copy(depsfile, lockfile);
47 | return 0;
48 | }
49 |
50 | auto beforeGraph = readText(lockfile).toGraph();
51 | auto afterGraph = readText(depsfile).toGraph();
52 |
53 | auto diff = makeDiff(beforeGraph, afterGraph, DiffSettings(focusName,
54 | filterCost, excludeNames));
55 |
56 | auto f = outfile ? File(outfile, "w") : stdout;
57 | scope (exit)
58 | f.close();
59 |
60 | f.writeln("digraph {");
61 | if (diff.keptNodes.length > 0)
62 | {
63 | f.writeln(" {");
64 | foreach (m; diff.keptNodes)
65 | {
66 | f.writefln!" \"%s\""(m.name);
67 | }
68 | f.writeln(" }");
69 | }
70 | if (diff.removedNodes.length > 0)
71 | {
72 | f.writeln(" {");
73 | f.writeln(` node [style=filled color="#fdaeb7" fillcolor="#ffeef0"];`);
74 | foreach (m; diff.removedNodes)
75 | {
76 | f.writefln!" \"%s\""(m.name);
77 | }
78 | f.writeln(" }");
79 | }
80 | if (diff.addedNodes.length > 0)
81 | {
82 | f.writeln(" {");
83 | f.writeln(` node [style=filled color="#bef5cb" fillcolor="#e6ffed"];`);
84 | foreach (m; diff.addedNodes)
85 | {
86 | f.writefln!" \"%s\""(m.name);
87 | }
88 | f.writeln(" }");
89 | }
90 |
91 | ////////////////////////////////////////////////////////////////////////////
92 | enum ModuleEditType
93 | {
94 | Keep,
95 | Remove,
96 | Add,
97 | }
98 |
99 | ModuleEditType[string] moduleSet;
100 | foreach (m; diff.keptNodes)
101 | moduleSet[m.name] = ModuleEditType.Keep;
102 | foreach (m; diff.removedNodes)
103 | moduleSet[m.name] = ModuleEditType.Remove;
104 | foreach (m; diff.addedNodes)
105 | moduleSet[m.name] = ModuleEditType.Add;
106 |
107 | if (diff.keptEdges.length > 0)
108 | {
109 | foreach (m; diff.keptEdges)
110 | {
111 | switch (moduleSet[m.import_.name]) with (ModuleEditType)
112 | {
113 | case Keep:
114 | f.writefln!` "%s" -> "%s";`(m.module_.name, m.import_.name);
115 | break;
116 | case Remove:
117 | f.writefln!` "%s" -> "%s" [color="#cb2431"];`(m.module_.name,
118 | m.import_.name);
119 | break;
120 | case Add:
121 | f.writefln!` "%s" -> "%s" [color="#2cbe4e"];`(m.module_.name,
122 | m.import_.name);
123 | break;
124 | default:
125 | writeln("// unknown edge: ", m);
126 | break;
127 | }
128 | }
129 | }
130 | if (diff.removedEdges.length > 0)
131 | {
132 | foreach (m; diff.removedEdges)
133 | f.writefln!` "%s" -> "%s" [color="#cb2431"];`(m.module_.name, m.import_.name);
134 | }
135 | if (diff.addedEdges.length > 0)
136 | {
137 | foreach (m; diff.addedEdges)
138 | f.writefln!` "%s" -> "%s" [color="#2cbe4e"];`(m.module_.name, m.import_.name);
139 | }
140 | f.writeln("}");
141 |
142 | return 0;
143 | }
144 | }
145 |
146 | DependenciesGraph toGraph(string value)
147 | {
148 | auto nodes = new RedBlackTree!Node();
149 | auto edges = new RedBlackTree!Edge();
150 |
151 | foreach (line; value.splitter("\n"))
152 | {
153 | if (line.length == 0)
154 | continue;
155 |
156 | auto tokens = splitter(line, " : ");
157 | auto sourceText = tokens.pop();
158 | auto importType = tokens.pop();
159 | auto targetText = tokens.pop();
160 |
161 | auto sourceName = until(sourceText, " ").to!string();
162 | auto sourceModule = Node(sourceName);
163 | nodes.insert(sourceModule);
164 | auto targetName = targetText.until(" ").to!string();
165 | auto targetModule = Node(targetName);
166 | nodes.insert(targetModule);
167 |
168 | edges.insert(Edge("module", sourceModule, targetModule, importType));
169 | }
170 |
171 | // TODO 省略できる気がする
172 | auto tempNodes = nodes[].array();
173 | tempNodes.sort();
174 | auto tempEdges = edges[].array();
175 | tempEdges.sort();
176 | return new DependenciesGraph(tempNodes, tempEdges);
177 | }
178 |
179 | unittest
180 | {
181 | auto graph = toGraph(`
182 | app (/app.d) : private : object (/object.d)
183 | app (/app.d) : private : std.stdio (/std/stdio.d)
184 | `);
185 |
186 | assert(graph.modules.length == 3);
187 | assert(graph.modules[0].name == "app");
188 | assert(graph.modules[1].name == "object");
189 | assert(graph.modules[2].name == "std.stdio");
190 |
191 | assert(graph.dependencies.length == 2);
192 | assert(graph.dependencies[0].module_.name == "app");
193 | assert(graph.dependencies[0].import_.name == "object");
194 | assert(graph.dependencies[1].module_.name == "app");
195 | assert(graph.dependencies[1].import_.name == "std.stdio");
196 | }
197 |
198 | struct GraphDiff
199 | {
200 | Node[] keptNodes;
201 | Node[] removedNodes;
202 | Node[] addedNodes;
203 |
204 | Edge[] keptEdges;
205 | Edge[] removedEdges;
206 | Edge[] addedEdges;
207 | }
208 |
209 | struct DiffSettings
210 | {
211 | this(string filterName, int filterCost)
212 | {
213 | this(filterName, filterCost, null);
214 | }
215 |
216 | this(string filterName, int filterCost, string[] excludeNames)
217 | {
218 | this.filterName = filterName;
219 | this.filterCost = filterCost;
220 | this.excludeNames = excludeNames;
221 | }
222 |
223 | string filterName;
224 | int filterCost;
225 | string[] excludeNames;
226 | }
227 |
228 | GraphDiff makeDiff(DependenciesGraph before, DependenciesGraph after, DiffSettings settings)
229 | {
230 | GraphDiff result;
231 |
232 | auto beforeTempCost = makeNodeCostDic(before.dependencies,
233 | settings.filterName, settings.filterCost);
234 | auto afterTempCost = makeNodeCostDic(after.dependencies,
235 | settings.filterName, settings.filterCost);
236 |
237 | auto beforeEdges = before.dependencies.filter!(e => isOutputTarget(e,
238 | beforeTempCost, settings.filterCost, settings.excludeNames)).array();
239 | auto afterEdges = after.dependencies.filter!(e => isOutputTarget(e,
240 | afterTempCost, settings.filterCost, settings.excludeNames)).array();
241 |
242 | result.keptEdges = setIntersection(beforeEdges, afterEdges).array();
243 | result.removedEdges = setDifference(beforeEdges, afterEdges).array();
244 | result.addedEdges = setDifference(afterEdges, beforeEdges).array();
245 |
246 | auto beforeCost = makeNodeCostDic(beforeEdges, settings.filterName, settings.filterCost);
247 | auto afterCost = makeNodeCostDic(afterEdges, settings.filterName, settings.filterCost);
248 |
249 | auto beforeNodes = before.modules.filter!(m => isOutputTarget(m,
250 | beforeCost, settings.filterCost)).array();
251 | auto afterNodes = after.modules.filter!(m => isOutputTarget(m, afterCost,
252 | settings.filterCost)).array();
253 |
254 | result.keptNodes = setIntersection(beforeNodes, afterNodes).array();
255 | result.removedNodes = setDifference(beforeNodes, afterNodes).array();
256 | result.addedNodes = setDifference(afterNodes, beforeNodes).array();
257 |
258 | return result;
259 | }
260 |
261 | unittest
262 | {
263 | // Add node and edge
264 | auto before = toGraph(`
265 | app (/app.d) : private : object (/object.d)
266 | app (/app.d) : private : std.stdio (/std/stdio.d)
267 | `);
268 | auto after = toGraph(`
269 | app (/app.d) : private : object (/object.d)
270 | app (/app.d) : private : std.stdio (/std/stdio.d)
271 | app (/app.d) : private : std.algorithm (/std/algorithm.d)
272 | `);
273 |
274 | auto diff = makeDiff(before, after, DiffSettings("app", 1));
275 |
276 | assert(diff.keptNodes.length == 3);
277 | assert(diff.keptNodes[0].name == "app");
278 | assert(diff.keptNodes[1].name == "object");
279 | assert(diff.keptNodes[2].name == "std.stdio");
280 | assert(diff.removedNodes.length == 0);
281 | assert(diff.addedNodes.length == 1);
282 | assert(diff.addedNodes[0].name == "std.algorithm");
283 |
284 | assert(diff.keptEdges.length == 2);
285 | assert(diff.keptEdges[0].module_.name == "app");
286 | assert(diff.keptEdges[0].import_.name == "object");
287 | assert(diff.keptEdges[1].module_.name == "app");
288 | assert(diff.keptEdges[1].import_.name == "std.stdio");
289 | assert(diff.removedEdges.length == 0);
290 | assert(diff.addedEdges.length == 1);
291 | assert(diff.addedEdges[0].module_.name == "app");
292 | assert(diff.addedEdges[0].import_.name == "std.algorithm");
293 | }
294 |
295 | unittest
296 | {
297 | // Remove node and edge
298 | auto before = toGraph(`
299 | app (/app.d) : private : object (/object.d)
300 | app (/app.d) : private : std.stdio (/std/stdio.d)
301 | app (/app.d) : private : std.algorithm (/std/algorithm.d)
302 | std.stdio (/std/stdio.d) : private : object (/object.d)
303 | `);
304 | auto after = toGraph(`
305 | app (/app.d) : private : object (/object.d)
306 | app (/app.d) : private : std.stdio (/std/stdio.d)
307 | std.stdio (/std/stdio.d) : private : object (/object.d)
308 | `);
309 |
310 | auto diff = makeDiff(before, after, DiffSettings("app", 1));
311 |
312 | assert(diff.keptNodes.length == 3);
313 | assert(diff.keptNodes[0].name == "app");
314 | assert(diff.keptNodes[1].name == "object");
315 | assert(diff.keptNodes[2].name == "std.stdio");
316 | assert(diff.removedNodes.length == 1);
317 | assert(diff.removedNodes[0].name == "std.algorithm");
318 | assert(diff.addedNodes.length == 0);
319 |
320 | assert(diff.keptEdges.length == 2);
321 | assert(diff.keptEdges[0].module_.name == "app");
322 | assert(diff.keptEdges[0].import_.name == "object");
323 | assert(diff.keptEdges[1].module_.name == "app");
324 | assert(diff.keptEdges[1].import_.name == "std.stdio");
325 | assert(diff.removedEdges.length == 1);
326 | assert(diff.removedEdges[0].module_.name == "app");
327 | assert(diff.removedEdges[0].import_.name == "std.algorithm");
328 | assert(diff.addedEdges.length == 0);
329 | }
330 |
331 | unittest
332 | {
333 | // Add node and edge with excludeNames,
334 | // then omit about the std.conv
335 | auto before = toGraph(`
336 | app (/app.d) : private : object (/object.d)
337 | app (/app.d) : private : std.stdio (/std/stdio.d)
338 | std.stdio (/std/stdio.d) : private : object (/object.d)
339 | `);
340 | auto after = toGraph(`
341 | app (/app.d) : private : object (/object.d)
342 | app (/app.d) : private : std.conv (/std/conv.d)
343 | app (/app.d) : private : std.stdio (/std/stdio.d)
344 | std.stdio (/std/stdio.d) : private : object (/object.d)
345 | `);
346 |
347 | auto diff = makeDiff(before, after, DiffSettings("app", 2, ["std.conv"]));
348 |
349 | assert(diff.keptNodes.length == 3);
350 | assert(diff.keptNodes[0].name == "app");
351 | assert(diff.keptNodes[1].name == "object");
352 | assert(diff.keptNodes[2].name == "std.stdio");
353 | assert(diff.removedNodes.length == 0);
354 | assert(diff.addedNodes.length == 0);
355 |
356 | assert(diff.keptEdges.length == 3);
357 | assert(diff.keptEdges[0].module_.name == "app");
358 | assert(diff.keptEdges[0].import_.name == "object");
359 | assert(diff.keptEdges[1].module_.name == "app");
360 | assert(diff.keptEdges[1].import_.name == "std.stdio");
361 | assert(diff.keptEdges[2].module_.name == "std.stdio");
362 | assert(diff.keptEdges[2].import_.name == "object");
363 | assert(diff.removedEdges.length == 0);
364 | assert(diff.addedEdges.length == 0);
365 | }
366 |
367 | unittest
368 | {
369 | // Remove node and edge with excludeNames,
370 | // then omit about the std.conv
371 | auto before = toGraph(`
372 | app (/app.d) : private : object (/object.d)
373 | app (/app.d) : private : std.conv (/std/conv.d)
374 | app (/app.d) : private : std.stdio (/std/stdio.d)
375 | std.stdio (/std/stdio.d) : private : object (/object.d)
376 | `);
377 | auto after = toGraph(`
378 | app (/app.d) : private : object (/object.d)
379 | app (/app.d) : private : std.stdio (/std/stdio.d)
380 | std.stdio (/std/stdio.d) : private : object (/object.d)
381 | `);
382 |
383 | auto diff = makeDiff(before, after, DiffSettings("app", 2, ["std.conv"]));
384 |
385 | assert(diff.keptNodes.length == 3);
386 | assert(diff.keptNodes[0].name == "app");
387 | assert(diff.keptNodes[1].name == "object");
388 | assert(diff.keptNodes[2].name == "std.stdio");
389 |
390 | assert(diff.keptEdges.length == 3);
391 | assert(diff.keptEdges[0].module_.name == "app");
392 | assert(diff.keptEdges[0].import_.name == "object");
393 | assert(diff.keptEdges[1].module_.name == "app");
394 | assert(diff.keptEdges[1].import_.name == "std.stdio");
395 | assert(diff.keptEdges[2].module_.name == "std.stdio");
396 | assert(diff.keptEdges[2].import_.name == "object");
397 | assert(diff.addedEdges.length == 0);
398 | assert(diff.removedEdges.length == 0);
399 | }
400 |
401 | struct Node
402 | {
403 | string name;
404 |
405 | this(string name)
406 | {
407 | this.name = name;
408 | }
409 |
410 | int opCmp(ref const Node rhs)
411 | {
412 | return cmp(name, rhs.name);
413 | }
414 |
415 | int opCmp(ref const Node rhs) const
416 | {
417 | return cmp(name, rhs.name);
418 | }
419 | }
420 |
421 | struct Edge
422 | {
423 | string kind;
424 | Node module_;
425 | Node import_;
426 | string importType;
427 |
428 | this(string kind, Node module_, Node import_, string importType = "private")
429 | {
430 | this.kind = kind;
431 | this.module_ = module_;
432 | this.import_ = import_;
433 | this.importType = importType;
434 | }
435 |
436 | int opCmp(ref const Edge rhs) const
437 | {
438 | auto kind = cmp(this.kind, rhs.kind);
439 | if (kind != 0)
440 | return kind;
441 | auto module_ = cmp(this.module_.name, rhs.module_.name);
442 | if (module_ != 0)
443 | return module_;
444 | auto import_ = cmp(this.import_.name, rhs.import_.name);
445 | return import_;
446 | }
447 | }
448 |
449 | class DependenciesGraph
450 | {
451 | Node[] modules;
452 | Edge[] dependencies;
453 |
454 | this(Node[] modules, Edge[] dependencies)
455 | {
456 | this.modules = modules;
457 | this.dependencies = dependencies;
458 | }
459 | }
460 |
461 | auto pop(R)(auto ref R range)
462 | {
463 | scope (success)
464 | range.popFront();
465 |
466 | return range.front;
467 | }
468 |
469 | int[string] makeNodeCostDic(Edge[] edges, string filterName, int maxCost = int.max)
470 | {
471 | typeof(return) result;
472 |
473 | auto nodeSet = new RedBlackTree!string;
474 | foreach (edge; edges)
475 | {
476 | nodeSet.insert(edge.module_.name);
477 | nodeSet.insert(edge.import_.name);
478 | }
479 | foreach (name; nodeSet[])
480 | {
481 | result[name] = isFocusTarget(name, filterName) ? 0 : int.max;
482 | }
483 |
484 | bool changed;
485 | int currentCost = -1;
486 |
487 | do
488 | {
489 | changed = false;
490 | currentCost++;
491 |
492 | foreach (edge; edges)
493 | {
494 | if (result[edge.module_.name] == currentCost)
495 | {
496 | result[edge.import_.name] = min(result[edge.import_.name], currentCost + 1);
497 | changed = true;
498 | }
499 | }
500 | }
501 | while (changed && currentCost < maxCost);
502 |
503 | return result;
504 | }
505 |
506 | unittest
507 | {
508 | auto cost = makeNodeCostDic([Edge(null, Node("app"), Node("std.stdio"), null)], "app");
509 | assert(cost["app"] == 0);
510 | assert(cost["std.stdio"] == 1);
511 | }
512 |
513 | bool isFocusTarget(const scope string nodeName, const scope string name)
514 | {
515 | if (nodeName.length < name.length)
516 | return false;
517 | if (nodeName[0 .. name.length] != name)
518 | return false;
519 |
520 | if (nodeName.length == name.length)
521 | return true;
522 | if (nodeName.length >= name.length + 1 && nodeName[name.length] == '.')
523 | return true;
524 |
525 | return false;
526 | }
527 |
528 | unittest
529 | {
530 | assert(isFocusTarget("app", "app"));
531 | assert(isFocusTarget("app.common", "app"));
532 | assert(isFocusTarget("app.testing.utils", "app"));
533 | assert(!isFocusTarget("std.algorithm", "app"));
534 | assert(!isFocusTarget("core.atomic", "app"));
535 | assert(!isFocusTarget("util", "app"));
536 |
537 | assert(isFocusTarget("core.atomic", "core"));
538 |
539 | assert(!isFocusTarget("app", "util"));
540 | }
541 |
542 | int getCost(Node node, int[string] cost)
543 | {
544 | if (!(node.name in cost))
545 | return int.max;
546 | return cost[node.name];
547 | }
548 |
549 | bool isOutputTarget(Node node, int[string] cost, int filter, string[] excludeNames = null)
550 | {
551 | foreach (excludeName; excludeNames)
552 | {
553 | if (node.name == excludeName || node.name.startsWith(chain(excludeName, ".")))
554 | return false;
555 | }
556 |
557 | return node.getCost(cost) <= filter;
558 | }
559 |
560 | unittest
561 | {
562 | int[string] cost = ["app" : 0, "std.stdio" : 1, "core.atomic" : 2, "object" : 3];
563 | assert(isOutputTarget(Node("app"), cost, 1));
564 | assert(isOutputTarget(Node("std.stdio"), cost, 1));
565 | assert(!isOutputTarget(Node("core.atomic"), cost, 1));
566 | assert(!isOutputTarget(Node("object"), cost, 1));
567 |
568 | assert(isOutputTarget(Node("app"), cost, 2));
569 | assert(isOutputTarget(Node("std.stdio"), cost, 2));
570 | assert(isOutputTarget(Node("core.atomic"), cost, 2));
571 | assert(!isOutputTarget(Node("object"), cost, 2));
572 |
573 | assert(isOutputTarget(Node("app"), cost, 1, ["std.stdio"]));
574 | assert(!isOutputTarget(Node("std.stdio"), cost, 1, ["std.stdio"]));
575 | }
576 |
577 | bool isOutputTarget(Edge edge, int[string] cost, int filter, string[] excludeNames = null)
578 | {
579 | foreach (excludeName; excludeNames)
580 | {
581 | if (edge.module_.name == excludeName
582 | || edge.module_.name.startsWith(chain(excludeName, ".")))
583 | return false;
584 | if (edge.import_.name == excludeName
585 | || edge.import_.name.startsWith(chain(excludeName, ".")))
586 | return false;
587 | }
588 |
589 | if (edge.import_.getCost(cost) > filter)
590 | return false;
591 | if (edge.module_.getCost(cost) >= filter)
592 | return false;
593 |
594 | return true;
595 | }
596 |
597 | unittest
598 | {
599 | int[string] cost = ["app" : 0, "std.stdio" : 1, "core.atomic" : 2, "object" : 3];
600 | assert(isOutputTarget(Edge("module", Node("app"), Node("std.stdio")), cost, 1));
601 | assert(!isOutputTarget(Edge("module", Node("std.stdio"), Node("core.atomic")), cost, 1));
602 | assert(isOutputTarget(Edge("module", Node("std.stdio"), Node("core.atomic")), cost, 2));
603 |
604 | assert(isOutputTarget(Edge("module", Node("app"), Node("std.stdio")), cost, 1, ["std.conv"]));
605 | assert(!isOutputTarget(Edge("module", Node("app"), Node("std.stdio")), cost, 1, ["std.stdio"]));
606 | assert(!isOutputTarget(Edge("module", Node("std.stdio"),
607 | Node("core.atomic")), cost, 1, ["std.stdio"]));
608 | assert(!isOutputTarget(Edge("module", Node("std.stdio"),
609 | Node("core.atomic")), cost, 2, ["std.stdio"]));
610 | }
611 |
--------------------------------------------------------------------------------