Skip to content

Commit

Permalink
Simplify scan for imports
Browse files Browse the repository at this point in the history
  • Loading branch information
linkrope committed Jan 5, 2021
1 parent 3974945 commit e8179e2
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 116 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Example
-------

Using the `depend` tool on the code of the popular [vibe.d][] repository gives an overview of the package dependencies:
[package diagram](https://raw.githubusercontent.com/wiki/funkwerk/depend/images/vibe.png)
[package diagram](../media/vibe.png?raw=true)

Usage
-----
Expand All @@ -23,7 +23,7 @@ Run the `depend` tool like you run `dmd` (most arguments are just passed to `dmd

For example, run the `depend` tool on its own code:

depend src/*.d
depend --compiler dmd src/*.d

Consider switching to module dependencies instead of package dependencies:

Expand All @@ -32,19 +32,32 @@ Consider switching to module dependencies instead of package dependencies:
The output is a package diagram in the [PlantUML][] language:

@startuml
package check {}
package deps {}
package graph {}
package imports {}
package main {}
package model {}
package settings {}
package uml {}

check ..> model
deps ..> model
graph ..> model
imports ..> model
main ..> check
main ..> deps
main ..> graph
main ..> imports
main ..> settings
main ..> uml
uml ..> graph
settings ..> model
uml ..> model
@enduml

Copy and paste the output to the [PlantUML Server][] for visualization:

![package diagram](https://raw.githubusercontent.com/wiki/funkwerk/depend/images/self.png)
![package diagram](../media/self.png?raw=true)

For checking, specify the target dependencies in the [PlantUML][] language.
For example, create a text file `target.uml`.
Expand Down
162 changes: 79 additions & 83 deletions src/imports.d
Original file line number Diff line number Diff line change
@@ -1,123 +1,119 @@
module imports;

import model;
import std.algorithm;
import std.array;
import std.path;
import std.range;
import std.regex;
import std.typecons;

version (unittest) import unit_threaded;

public auto scanImports(const string[] args)
auto mutualDependencies(const string[] args)
{
const lookup = Lookup(args);

return lookup.sourceFiles
.map!(sourceFile => extractImports(sourceFile, lookup))
.joiner;
string[][string] importedModules;

foreach (arg; args)
with (readImports(arg))
importedModules[client] ~= suppliers;
return importedModules.byKeyValue
.map!(pair => pair.value.map!(supplier => Dependency(pair.key, supplier)))
.joiner
.filter!(dependency => dependency.supplier.toString in importedModules);
}

public auto extractImports(const string file, Lookup lookup)
auto readImports(string file)
{
import std.file : readText;
import std.path : baseName, stripExtension;

const input = file.readText;
auto captures = moduleDeclaration(input);
const client = captures
? captures["fullyQualifiedName"].toFullyQualifiedName
: file.baseName.stripExtension;
const suppliers = importDeclarations(input)
.map!(captures => captures["fullyQualifiedName"].toFullyQualifiedName)
.array;

alias Module = Tuple!(string, "name", string, "path");
alias Dependency = Tuple!(Module, "client", Module, "supplier");
return tuple!("client", "suppliers")(client, suppliers);
}

auto toModule(string name)
{
return Module(name, lookup.path(name));
}
auto moduleDeclaration(R)(R input)
{
import std.regex : matchFirst, regex;

const source = file.readText;
const moduleName = declaredModule(source);
// TODO: skip comments, string literals
enum pattern = regex(`\bmodule\s+` ~ fullyQualifiedName ~ `\s*;`);

return importedModules(source)
.map!(name => Dependency(Module(moduleName, file), toModule(name)))
.array;
return input.matchFirst(pattern);
}

string declaredModule(R)(R input)
@("match module declaration")
unittest
{
import std.string : join, strip;
auto captures = moduleDeclaration("module bar.baz;");

captures.shouldBeTrue;
captures["fullyQualifiedName"].should.be == "bar.baz";
}

@("match module declaration with white space")
unittest
{
auto captures = moduleDeclaration("module bar . baz\n;");

captures.shouldBeTrue;
captures["fullyQualifiedName"].should.be == "bar . baz";
}

enum fullyQualifiedName = `(?P<fullyQualifiedName>\w+(\s*\.\s*\w+)*)`;
enum pattern = regex(`\bmodule\s+` ~ fullyQualifiedName);
auto importDeclarations(R)(R input)
{
import std.regex : matchAll, regex;

// TODO: skip comments, string literals
if (auto captures = input.matchFirst(pattern))
{
return captures["fullyQualifiedName"].splitter('.').map!strip.join('.');
}
return null; // FIXME: fall back to basename?
enum pattern = regex(`\bimport\s+(\w+\s*=\s*)?` ~ fullyQualifiedName ~ `[^;]*;`);

return input.matchAll(pattern);
}

@("scan module declaration")
@("match import declaration")
unittest
{
declaredModule("module bar.baz;").should.be == "bar.baz";
declaredModule("module bar . baz;").should.be == "bar.baz";
auto match = importDeclarations("import bar.baz;");

match.shouldBeTrue;
match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar.baz"]);
}

auto importedModules(R)(R input)
@("match import declaration with white space")
unittest
{
import std.string : join, strip;

enum fullyQualifiedName = `(?P<fullyQualifiedName>\w+(\s*\.\s*\w+)*)`;
enum pattern = regex(`\bimport\s+(\w+\s*=\s*)?` ~ fullyQualifiedName);
auto match = importDeclarations("import bar . baz\n;");

// TODO: skip comments, string literals
return input.matchAll(pattern)
.map!(a => a["fullyQualifiedName"].splitter('.').map!strip.join('.'));
match.shouldBeTrue;
match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar . baz"]);
}

@("scan import declarations")
@("match renamed import")
unittest
{
importedModules("import bar.baz;").shouldEqual(["bar.baz"]);
importedModules("import foo = bar.baz;").shouldEqual(["bar.baz"]);
importedModules("import bar . baz;").shouldEqual(["bar.baz"]);
auto match = importDeclarations("import foo = bar.baz;");

match.shouldBeTrue;
match.map!`a["fullyQualifiedName"]`.shouldEqual(["bar.baz"]);
}

struct Lookup
enum fullyQualifiedName = `(?P<fullyQualifiedName>\w+(\s*\.\s*\w+)*)`;

string toFullyQualifiedName(string text)
{
import std.string : join, strip;

return text.splitter('.').map!strip.join('.');
}

@("convert text to fully-qualified name")
unittest
{
const string[] sourceFiles;

const string[] importPaths;

this(const string[] args)
{
import std.string : chompPrefix;

sourceFiles = args
.filter!(arg => arg.extension == ".d")
.array;
importPaths = args
.filter!(arg => arg.startsWith("-I"))
.map!(arg => arg.chompPrefix("-I"))
.array;
}

string path(string fullyQualifiedName)
{
const path = fullyQualifiedName.splitter(".").buildPath.setExtension(".d");
const packagePath = chain(fullyQualifiedName.splitter("."), only("package")).buildPath.setExtension(".d");

return chain(
match(path),
match(packagePath),
only(path),
).front;
}

auto match(string partialPath)
{
import std.file : exists;

return chain(
sourceFiles.filter!(path => path.pathSplitter.endsWith(partialPath.pathSplitter)),
importPaths.map!(path => buildPath(path, partialPath)).filter!(path => path.exists),
);
}
"bar . baz".toFullyQualifiedName.should.be == "bar.baz";
}
38 changes: 20 additions & 18 deletions src/main.d
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import std.algorithm;
import std.array;
import std.exception;
import std.range;
import std.regex;
import std.stdio;
import std.typecons;
import uml;
Expand All @@ -25,36 +24,39 @@ void main(string[] args)

with (settings)
{
bool matches(T)(T dependency)
auto readDependencies(File file)
{
with (dependency)
import std.regex : matchFirst, Regex;

if (pattern != Regex!char())
{
if (pattern.empty)
bool matches(T)(T dependency)
{
return unrecognizedArgs.canFind(client.path)
&& unrecognizedArgs.canFind(supplier.path);
with (dependency)
return client.path.matchFirst(pattern)
&& supplier.path.matchFirst(pattern);
}
else

return moduleDependencies!(dependency => matches(dependency))(file).array;
}
else
{
bool matches(T)(T dependency)
{
return client.path.matchFirst(pattern)
&& supplier.path.matchFirst(pattern);
with (dependency)
return unrecognizedArgs.canFind(client.path)
&& unrecognizedArgs.canFind(supplier.path);
}
}
}

Dependency[] readDependencies(File file)
{
return moduleDependencies!(dependency => matches(dependency))(file).array;
return moduleDependencies!(dependency => matches(dependency))(file).array;
}
}

Dependency[] actualDependencies;

if (compiler.empty)
{
actualDependencies = scanImports(unrecognizedArgs)
.filter!(dependency => matches(dependency))
.map!(dependency => Dependency(dependency.client.name, dependency.supplier.name))
.array;
actualDependencies = mutualDependencies(unrecognizedArgs).array;
}
else
{
Expand Down
32 changes: 21 additions & 11 deletions src/settings.d
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@ module settings;
import core.stdc.stdlib;
import model;
import std.range;
import std.regex;
import std.stdio;
version (unittest) import unit_threaded;

struct Settings
{
string compiler = null;
string[] depsFiles = null;
bool scan = false;
string[] umlFiles = null;
string pattern = null;
bool detail = false;
bool transitive = false;
bool dot = false;
string[] targetFiles = null;
bool simplify = false;
string compiler;
string[] depsFiles;
bool scan;
string[] umlFiles;
Regex!char pattern;
bool detail;
bool transitive;
bool dot;
string[] targetFiles;
bool simplify;
string[] unrecognizedArgs;
}

Settings read(string[] args)
in (!args.empty)
{
import std.exception : enforce;
import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult;

Settings settings;

with (settings)
{
string filter;
GetoptResult result;

try
Expand All @@ -39,13 +42,20 @@ in (!args.empty)
"compiler|c", "Specify the compiler to use", &compiler,
"deps", "Read module dependencies from file", &depsFiles,
"uml", "Read dependencies from PlantUML file", &umlFiles,
"filter", "Filter source files matching the regular expression", &pattern,
"filter", "Filter source files matching the regular expression", &filter,
"detail", "Inspect dependencies between modules instead of packages", &detail,
"transitive|t", "Keep transitive dependencies", &transitive,
"dot", "Write dependency graph in the DOT language", &dot,
"check", "Check against the PlantUML target dependencies", &targetFiles,
"simplify", "Use simplifying assumptions for the check (experimental)", &simplify,
);
if (!filter.empty)
{
enforce(!compiler.empty || !depsFiles.empty,
"filter can only be applied to dependencies collected by a compiler");

pattern = regex(filter);
}
}
catch (Exception exception)
{
Expand Down

0 comments on commit e8179e2

Please sign in to comment.