diff --git a/.build/.gitignore b/.build/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/.build/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..760b8b8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset=utf-8
+end_of_line=lf
+trim_trailing_whitespace=true
+insert_final_newline=true
+indent_style=space
+indent_size=4
+
+[Makefile]
+indent_style=tab
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb25ee9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+composer.lock
+
+*.swp
+.idea/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3501102
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,16 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Gert de Pagter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..eed74e7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# PHP Depend resolver
+
+## What is this
+
+This library allows you to find the dependencies of a php file.
+
+It was created to help with another libary, that allows you to run tests for only changes files (and their dependencies)
+
+## Usage
+```php
+build();
+
+$deps = $finder->getAllFilesDependingOn('./tests/Fixtures/Circular/A.php');
+
+foreach ($deps as $dep) {
+ var_dump($dep);
+}
+
+$finder->reBuild(['./src/Domain/User.php', './tests/Domain/User.php', './src/functions.php']);
+```
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..e545fe7
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "backendtea/dependency-finder",
+ "description": "Find out exactly what classes depend on what other classes",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^7.2",
+ "nikic/php-parser": "^4.2",
+ "symfony/finder": "^4.3"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "infection/infection": "^0.13.3",
+ "phpstan/phpstan": "^0.11",
+ "phpunit/phpunit": "^8.2"
+ },
+ "config": {
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Depend\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Depend\\Test\\": "tests/"
+ }
+ }
+}
diff --git a/infection.json.dist b/infection.json.dist
new file mode 100644
index 0000000..fab3a37
--- /dev/null
+++ b/infection.json.dist
@@ -0,0 +1,14 @@
+{
+ "timeout": 5,
+ "source": {
+ "directories": [
+ "src"
+ ]
+ },
+ "logs": {
+ "text": ".build/infection.log"
+ },
+ "mutators": {
+ "@default": true
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..422dceb
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ src
+ tests
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..ff0cc44
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+includes:
+ - vendor/phpstan/phpstan/conf/bleedingEdge.neon
+ - vendor/phpstan/phpstan/conf/config.levelmax.neon
+
+parameters:
+ paths:
+ - src
+ - tests
+ tmpDir: %currentWorkingDirectory%/.build
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..b1dde1b
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,22 @@
+
+
+
+
+ ./tests/
+ ./tests/Fixtures
+
+
+
+
+
+ ./src/
+
+
+
diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php
new file mode 100644
index 0000000..26199f1
--- /dev/null
+++ b/src/Dependency/DependencyResolver.php
@@ -0,0 +1,67 @@
+ */
+ private $resolved = [];
+
+ /** @var array */
+ private $knownDependencies = [];
+
+ /**
+ * Key: Class/ function name
+ * Value: Files that have a dependency on the class.
+ *
+ * @var array> $dependantMap
+ */
+ private $dependantMap = [];
+
+ /**
+ * Key: File name
+ * Value: Classes it declares
+ *
+ * @var array> $declareMap
+ */
+ private $declareMap = [];
+
+ /**
+ * @param array> $declareMap
+ * @param array> $dependantMap
+ *
+ * @return array
+ */
+ public function resolve(string $fileName, array $declareMap, array $dependantMap) : array
+ {
+ $this->declareMap = $declareMap;
+ $this->dependantMap = $dependantMap;
+ foreach ($this->declareMap[$fileName]?? [] as $class) {
+ $this->resolveClass($class);
+ }
+
+ return $this->knownDependencies;
+ }
+
+ private function resolveClass(string $className) : void
+ {
+ if (array_key_exists($className, $this->resolved)) {
+ return;
+ }
+ $this->resolved[$className] = true;
+
+ $dependants = $this->dependantMap['\\' . $className] ?? [];
+
+ array_push($this->knownDependencies, ...$dependants);
+ foreach ($dependants as $dependant) {
+ foreach ($this->declareMap[$dependant]?? [] as $class) {
+ $this->resolveClass($class);
+ }
+ }
+ }
+}
diff --git a/src/DependencyFinder.php b/src/DependencyFinder.php
new file mode 100644
index 0000000..41b1221
--- /dev/null
+++ b/src/DependencyFinder.php
@@ -0,0 +1,137 @@
+> $dependencyMap
+ */
+ private $dependencyMap = [];
+
+ /**
+ * Key: Class/ function name
+ * Value: Files that have a dependency on the class.
+ *
+ * @var array> $dependantMap
+ */
+ private $dependantMap = [];
+
+ /**
+ * Key: File name
+ * Value: Classes it declares
+ *
+ * @var array> $declareMap
+ */
+ private $declareMap = [];
+
+ /** @var Parser */
+ private $parser;
+
+ /**
+ * List of directories to build dependencies from.
+ * for example: `new DependencyFinder(['./src', './tests', './lib']);`
+ *
+ * @param array|string[] $directories
+ * @param array|string[] $exclude
+ */
+ public function __construct(array $directories, array $exclude = [])
+ {
+ $this->directories = $directories;
+ $this->exclude = $exclude;
+ $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
+ }
+
+ public function build() : void
+ {
+ /** @var SplFileInfo $file */
+ foreach (Finder::create()
+ ->in($this->directories)
+ ->exclude($this->exclude)
+ ->files() as $file) {
+ $path = $file->getRealPath();
+ if ($path === false) {
+ $path = '';
+ }
+
+ $this->reBuildFor($path);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getAllFilesDependingOn(string $fileName) : array
+ {
+ $path = realpath($fileName);
+ if ($path === false) {
+ return [];
+ }
+
+ return(new DependencyResolver())->resolve($path, $this->declareMap, $this->dependantMap);
+ }
+
+ /**
+ * @param array|string[] $fileNames
+ */
+ public function reBuild(array $fileNames) : void
+ {
+ foreach ($fileNames as $fileName) {
+ $this->reBuildFor($fileName);
+ }
+ }
+
+ private function reBuildFor(string $fileName) : void
+ {
+ $info = $this->traversePath($fileName);
+ $this->dependencyMap[$fileName] = array_unique($info->dependencies);
+ $this->declareMap[$fileName] = $info->declares;
+ foreach ($info->dependencies as $dependency) {
+ $this->dependantMap[$dependency][] = $fileName;
+ }
+ }
+
+ private function traversePath(string $filePath) : File
+ {
+ $content = file_get_contents($filePath);
+ if ($content === false) {
+ return new File();
+ }
+
+ $nodes = $this->parser->parse($content);
+ $traverser = new NodeTraverser();
+ $traverser->addVisitor(new NameResolver());
+ $traverser->addVisitor(new ParentConnectorVisitor());
+ $nameCollector = new NameCollector();
+ $declareCollector = new DeclarationCollector();
+ $traverser->addVisitor($nameCollector);
+ $traverser->addVisitor($declareCollector);
+ $traverser->traverse($nodes);
+
+ return new File($declareCollector->declared, $nameCollector->resolvedNames);
+ }
+}
diff --git a/src/File.php b/src/File.php
new file mode 100644
index 0000000..f38bfa3
--- /dev/null
+++ b/src/File.php
@@ -0,0 +1,32 @@
+
+ */
+ public $declares = [];
+
+ /**
+ * Array of all the class/function dependencies of this file
+ *
+ * @var array
+ */
+ public $dependencies = [];
+
+ /**
+ * @param array $delcares
+ * @param array $dependencies
+ */
+ public function __construct(array $delcares = [], array $dependencies = [])
+ {
+ $this->declares = $delcares;
+ $this->dependencies = $dependencies;
+ }
+}
diff --git a/src/PHPParser/Visitor/DeclarationCollector.php b/src/PHPParser/Visitor/DeclarationCollector.php
new file mode 100644
index 0000000..96c1ba1
--- /dev/null
+++ b/src/PHPParser/Visitor/DeclarationCollector.php
@@ -0,0 +1,37 @@
+ */
+ public $declared = [];
+
+ /**
+ * @param array $nodes
+ *
+ * @return array
+ */
+ public function beforeTraverse(array $nodes) : ?array
+ {
+ $this->declared = [];
+
+ return null;
+ }
+
+ public function enterNode(Node $node) : ?Node
+ {
+ /** @var Node\Name|null $name */
+ $name = $node->namespacedName ?? null;
+ if ($name instanceof Node\Name) {
+ $this->declared[] = $name->toCodeString();
+ }
+
+ return null;
+ }
+}
diff --git a/src/PHPParser/Visitor/NameCollector.php b/src/PHPParser/Visitor/NameCollector.php
new file mode 100644
index 0000000..9b0761e
--- /dev/null
+++ b/src/PHPParser/Visitor/NameCollector.php
@@ -0,0 +1,50 @@
+ */
+ public $resolvedNames = [];
+
+ /**
+ * @param array|Node[] $nodes
+ *
+ * @return array|Node[]|null
+ */
+ public function beforeTraverse(array $nodes) : ?array
+ {
+ $this->resolvedNames = [];
+
+ return null;
+ }
+
+ /**
+ * @return int|Node|null
+ */
+ public function enterNode(Node $node)
+ {
+ if ($node instanceof Node\Stmt\Use_) {
+ return NodeTraverser::DONT_TRAVERSE_CHILDREN;
+ }
+ if ($node instanceof Node\Name) {
+ $parent = $node->getAttribute(ParentConnectorVisitor::PARENT_KEY);
+ if ($parent instanceof Node\Stmt\Namespace_
+ || $parent instanceof Node\Expr\ConstFetch
+ || $parent instanceof Node\Expr\ClassConstFetch
+ ) {
+ return null;
+ }
+
+ $this->resolvedNames[] = $node->toCodeString();
+ }
+
+ return null;
+ }
+}
diff --git a/src/PHPParser/Visitor/ParentConnectorVisitor.php b/src/PHPParser/Visitor/ParentConnectorVisitor.php
new file mode 100644
index 0000000..ca872d4
--- /dev/null
+++ b/src/PHPParser/Visitor/ParentConnectorVisitor.php
@@ -0,0 +1,82 @@
+ */
+ private $stack = [];
+
+ /**
+ * @param array $nodes
+ *
+ * @return array|null
+ */
+ public function beforeTraverse(array $nodes) : ?array
+ {
+ $this->stack = [];
+
+ return null;
+ }
+
+ public function enterNode(Node $node) : ?Node
+ {
+ if (! empty($this->stack)) {
+ $node->setAttribute(self::PARENT_KEY, $this->stack[count($this->stack) - 1]);
+ }
+
+ $this->stack[] = $node;
+
+ return null;
+ }
+
+ public function leaveNode(Node $node) : ?Node
+ {
+ array_pop($this->stack);
+
+ return null;
+ }
+}
diff --git a/tests/DependencyFinderTest.php b/tests/DependencyFinderTest.php
new file mode 100644
index 0000000..9571d53
--- /dev/null
+++ b/tests/DependencyFinderTest.php
@@ -0,0 +1,30 @@
+build();
+ $deps = $finder->getAllFilesDependingOn(__DIR__ . '/Fixtures/Circular/A.php');
+
+ $this->assertCount(2, $deps);
+ }
+
+ public function testAWrongFileReturnsNoDependencies() : void
+ {
+ $finder = new DependencyFinder([__DIR__ . '/Fixtures/Circular']);
+ $finder->build();
+
+ $deps = $finder->getAllFilesDependingOn(__FILE__ . 'asdf');
+ $this->assertCount(0, $deps);
+ }
+}
diff --git a/tests/FileTest.php b/tests/FileTest.php
new file mode 100644
index 0000000..52cf60f
--- /dev/null
+++ b/tests/FileTest.php
@@ -0,0 +1,18 @@
+assertSame([], $file->dependencies);
+ $this->assertSame([], $file->declares);
+ }
+}
diff --git a/tests/Fixtures/Circular/A.php b/tests/Fixtures/Circular/A.php
new file mode 100644
index 0000000..79ad573
--- /dev/null
+++ b/tests/Fixtures/Circular/A.php
@@ -0,0 +1,12 @@
+create(ParserFactory::PREFER_PHP7);
+ $nodes = $parser->parse(<<<'PHP'
+addVisitor(new NameResolver());
+ $traverser->addVisitor(new ParentConnectorVisitor());
+ $nameCollector = new NameCollector();
+ $traverser->addVisitor($nameCollector);
+ $traverser->traverse($nodes);
+
+ $this->assertCount(2, $nameCollector->resolvedNames);
+ $this->assertSame([
+ '\F\Bar\Baz',
+ '\Full\Name',
+ ], $nameCollector->resolvedNames);
+ }
+}