Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check FQN prefix for dependencies to recursive modules #7

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/deps.d
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,44 @@ import std.stdio;
import std.typecons;
version (unittest) import unit_threaded;

alias Dependency = Tuple!(string, "client", string, "supplier");
struct Element
{
string name;

Flag!"recursive" recursive;

int opCmp(const ref Element other) const
{
return tuple(this.name, this.recursive).opCmp(tuple(other.name, other.recursive));
}

string toLabel() const
{
return this.recursive ? (this.name ~ ".*") : this.name;
}

string toPackage() const
{
return this.recursive ? (this.name ~ ".all") : this.name;
}

string toString() const
{
return toPackage;
}
}

alias Dependency = Tuple!(Element, "client", Element, "supplier");

auto moduleDependencies(alias predicate)(File file)
{
import std.algorithm : filter, map;

return reader(file.byLine)
.filter!predicate
.map!(dependency => Dependency(dependency.client.name, dependency.supplier.name));
.map!(dependency => Dependency(
Element(dependency.client.name, No.recursive),
Element(dependency.supplier.name, No.recursive)));
}

auto reader(R)(R input)
Expand Down
30 changes: 16 additions & 14 deletions src/graph.d
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
module graph;

import deps : Dependency, Element;
import std.algorithm;
import std.range;
import std.typecons;
version (unittest) import unit_threaded;

alias Dependency = Tuple!(string, "client", string, "supplier");

void write(Output)(auto ref Output output, const Dependency[] dependencies)
{
import std.format : formattedWrite;
Expand All @@ -15,12 +14,12 @@ void write(Output)(auto ref Output output, const Dependency[] dependencies)
output.put("node [shape=box];\n");
foreach (element; dependencies.elements)
{
output.formattedWrite!(`"%s"`)(element);
output.formattedWrite!(`"%s"`)(element.name);
output.put('\n');
}
foreach (dependency; dependencies)
{
output.formattedWrite!(`"%s" -> "%s"`)(dependency.client, dependency.supplier);
output.formattedWrite!`"%s" -> "%s"`(dependency.client, dependency.supplier);
output.put('\n');
}
output.put("}\n");
Expand All @@ -33,7 +32,7 @@ unittest
import std.string : outdent, stripLeft;

auto output = appender!string;
const dependencies = [Dependency("a", "b")];
const dependencies = [Dependency(Element("a", No.recursive), Element("b", No.recursive))];

output.write(dependencies);

Expand All @@ -51,7 +50,7 @@ unittest

void transitiveClosure(ref Dependency[] dependencies)
{
string[] elements = dependencies.elements;
Element[] elements = dependencies.elements;

foreach (element; elements)
foreach (client; elements)
Expand All @@ -63,10 +62,10 @@ void transitiveClosure(ref Dependency[] dependencies)

Dependency[] transitiveReduction(ref Dependency[] dependencies)
{
bool[string] mark = null;
bool[Element] mark = null;
Dependency[] cyclicDependencies = null;

void traverse(string node)
void traverse(Element node)
{
import std.array : array;

Expand Down Expand Up @@ -101,26 +100,29 @@ Dependency[] transitiveReduction(ref Dependency[] dependencies)
@("apply transitive reduction")
unittest
{
auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("a", "c")];
auto dependencies = [dependency("a", "b"), dependency("b", "c"), dependency("a", "c")];
auto cyclicDependencies = transitiveReduction(dependencies);

dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c")];
dependencies.should.be == [dependency("a", "b"), dependency("b", "c")];
cyclicDependencies.shouldBeEmpty;
}

@("apply transitive reduction to cyclic dependencies")
unittest
{
auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")];
auto dependencies = [dependency("a", "b"), dependency("b", "c"), dependency("c", "a")];
auto cyclicDependencies = transitiveReduction(dependencies);

dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")];
dependencies.should.be == [dependency("a", "b"), dependency("b", "c"), dependency("c", "a")];
cyclicDependencies.sort.should.be == dependencies;
}

string[] elements(in Dependency[] dependencies)
private alias dependency = (client, supplier) =>
Dependency(Element(client, No.recursive), Element(supplier, No.recursive));

Element[] elements(in Dependency[] dependencies)
{
string[] elements = null;
Element[] elements = null;

foreach (dependency; dependencies)
{
Expand Down
35 changes: 29 additions & 6 deletions src/main.d
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import std.regex;
import std.stdio;
import std.typecons;
import uml;
import util;

void main(string[] args)
{
Expand Down Expand Up @@ -95,11 +96,33 @@ void main(string[] args)
dependency = Dependency(client, supplier);
else
{
dependency = Dependency(client.packages, supplier.packages);
if (dependency.client.empty || dependency.supplier.empty || dependency.client == dependency.supplier)
dependency = Dependency(
Element(client.name.packages, No.recursive),
Element(supplier.name.packages, No.recursive));
if (dependency.client.name.empty || dependency.supplier.name.empty ||
dependency.client == dependency.supplier)
continue;
}
if (!targetDependencies.canFind(dependency))
bool matchDependency(const Dependency targetDependency, const Dependency actualDependency)
{
bool matchPackage(bool recursive, string targetPackage, string actualPackage)
{
if (recursive)
{
return actualPackage.fqnStartsWith(targetPackage);
}
else
{
return actualPackage == targetPackage;
}
}
return matchPackage(targetDependency.client.recursive, targetDependency.client.name,
actualDependency.client.name) &&
matchPackage(targetDependency.supplier.recursive, targetDependency.supplier.name,
actualDependency.supplier.name);
}

if (!targetDependencies.any!(a => matchDependency(a, dependency)))
{
stderr.writefln("error: unintended dependency %s -> %s", client, supplier);
success = false;
Expand All @@ -118,11 +141,11 @@ void main(string[] args)
{
foreach (dependency; actualDependencies)
{
const client = dependency.client.packages;
const supplier = dependency.supplier.packages;
const client = dependency.client.name.packages;
const supplier = dependency.supplier.name.packages;

if (!client.empty && !supplier.empty && client != supplier)
dependencies_.add(Dependency(client, supplier));
dependencies_.add(Dependency(Element(client, No.recursive), Element(supplier, No.recursive)));
}
}
if (!transitive)
Expand Down
86 changes: 64 additions & 22 deletions src/uml.d
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module uml;

import deps : Dependency, Element;
import graph;
import std.algorithm;
import std.range;
Expand All @@ -18,37 +19,44 @@ Dependency[] read(R)(R input)

private void read(Input, Output)(Input input, auto ref Output output)
{
import std.conv : to;
import std.regex : matchFirst, regex;

enum arrow = `(?P<arrow><?\.+(left|right|up|down|le?|ri?|up?|do?|\[.*?\])*\.*>?)`;
enum pattern = regex(`(?P<lhs>\w+(.\w+)*)\s*` ~ arrow ~ `\s*(?P<rhs>\w+(.\w+)*)`);
enum pattern = regex(`(?P<lhs>\w+(\.\w+)*(\.\*)?)\s*` ~ arrow ~ `\s*(?P<rhs>\w+(\.\w+)*(\.\*)?)`);

foreach (line; input)
{
auto captures = line.matchFirst(pattern);

if (captures)
{
const lhs = captures["lhs"].to!string;
const rhs = captures["rhs"].to!string;
enum recursiveMarker = ".all";

const string lhs = captures["lhs"].idup;
const lhsRecursive = lhs.endsWith(recursiveMarker) ? Yes.recursive : No.recursive;
const lhsElement = Element(lhsRecursive ? lhs.dropBack(recursiveMarker.length) : lhs, lhsRecursive);

const string rhs = captures["rhs"].idup;
const rhsRecursive = rhs.endsWith(recursiveMarker) ? Yes.recursive : No.recursive;
const rhsElement = Element(rhsRecursive ? rhs.dropBack(recursiveMarker.length) : rhs, rhsRecursive);

if (captures["arrow"].endsWith(">"))
output.put(Dependency(lhs, rhs));
output.put(Dependency(lhsElement, rhsElement));
if (captures["arrow"].startsWith("<"))
output.put(Dependency(rhs, lhs));
output.put(Dependency(rhsElement, lhsElement));
}
}
}

@("read Plant-UML dependencies")
unittest
{
read(only("a .> b")).should.be == [Dependency("a", "b")];
read(only("a <. b")).should.be == [Dependency("b", "a")];
read(only("a <.> b")).should.be == [Dependency("a", "b"), Dependency("b", "a")];
read(only("a.[#red]>b")).should.be == [Dependency("a", "b")];
read(only("a.[#red]le>b")).should.be == [Dependency("a", "b")];
read(only("a .> b")).should.be == [dependency("a", "b")];
read(only("a <. b")).should.be == [dependency("b", "a")];
read(only("a <.> b")).should.be == [dependency("a", "b"), dependency("b", "a")];
read(only("a.all .> b.all")).should.be == [Dependency(Element("a", Yes.recursive), Element("b", Yes.recursive))];
read(only("a.[#red]>b")).should.be == [dependency("a", "b")];
read(only("a.[#red]le>b")).should.be == [dependency("a", "b")];
}

void write(Output)(auto ref Output output, const Dependency[] dependencies)
Expand All @@ -69,7 +77,7 @@ unittest
import std.string : outdent, stripLeft;

auto output = appender!string;
const dependencies = [Dependency("a", "b")];
const dependencies = [dependency("a", "b")];

output.write(dependencies);

Expand All @@ -92,7 +100,7 @@ unittest
import std.string : outdent, stripLeft;

auto output = appender!string;
const dependencies = [Dependency("a", "a.b"), Dependency("a.b", "a.c")];
const dependencies = [dependency("a", "a.b"), dependency("a.b", "a.c")];

output.write(dependencies);

Expand All @@ -112,31 +120,59 @@ unittest
output.data.should.be == outdent(expected).stripLeft;
}

@("use appropriate wildcard descriptions")
unittest
{
import std.array : appender;
import std.string : outdent, stripLeft;

auto output = appender!string;
const dependencies = [Dependency(Element("a", Yes.recursive), Element("b", Yes.recursive))];

output.write(dependencies);

const expected = `
@startuml
package a.* as a.all {}
package b.* as b.all {}

a.all ..> b.all
@enduml
`;

output.data.should.be == outdent(expected).stripLeft;
}

private alias dependency = (client, supplier) =>
Dependency(Element(client, No.recursive), Element(supplier, No.recursive));

private struct Package
{
string[] path;

Flag!"recursive" recursive;

Package[string] subpackages;

Dependency[] dependencies;

void add(Dependency dependency)
{
const clientPath = dependency.client.split('.');
const supplierPath = dependency.supplier.split('.');
const clientPath = dependency.client.name.split('.');
const supplierPath = dependency.supplier.name.split('.');
const path = commonPrefix(clientPath.dropBackOne, supplierPath.dropBackOne);

addPackage(clientPath);
addPackage(supplierPath);
addPackage(clientPath, dependency.client.recursive);
addPackage(supplierPath, dependency.client.recursive);
addDependency(path, dependency);
}

void addPackage(const string[] path, size_t index = 0)
void addPackage(const string[] path, Flag!"recursive" recursive, size_t index = 0)
{
if (path[index] !in subpackages)
subpackages[path[index]] = Package(path[0 .. index + 1].dup);
subpackages[path[index]] = Package(path[0 .. index + 1].dup, recursive);
if (index + 1 < path.length)
subpackages[path[index]].addPackage(path, index + 1);
subpackages[path[index]].addPackage(path, recursive, index + 1);
}

void addDependency(const string[] path, Dependency dependency)
Expand All @@ -160,7 +196,13 @@ private struct Package
foreach (subpackage; subpackages.keys.sort.map!(key => subpackages[key]))
{
indent;
if (subpackage.path.length == 1)
if (subpackage.recursive)
{
assert(subpackage.subpackages.empty && subpackage.dependencies.empty,
"recursive package must not contain subpackages");
output.formattedWrite!"package %s.* as %s.all {"(subpackage.path.back, subpackage.path.join('.'));
}
else if (subpackage.path.length == 1)
output.formattedWrite!"package %s {"(subpackage.path.join('.'));
else
output.formattedWrite!"package %s as %s {"(subpackage.path.back, subpackage.path.join('.'));
Expand All @@ -178,7 +220,7 @@ private struct Package
foreach (dependency; dependencies.sort)
{
indent;
output.formattedWrite!"%s ..> %s\n"(dependency.client, dependency.supplier);
output.formattedWrite!"%s ..> %s\n"(dependency.client.toPackage, dependency.supplier.toPackage);
}
}
}
10 changes: 10 additions & 0 deletions src/util.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module util;

import std.string;

bool fqnStartsWith(string haystack, string needle)
{
import std.algorithm : splitter;

return haystack.splitter(".").startsWith(needle.splitter("."));
}