Skip to content

Commit

Permalink
Merge pull request #1174 from patrickkusebauch/feature/composer
Browse files Browse the repository at this point in the history
Composer Collector
  • Loading branch information
Denis Brumann authored May 11, 2023
2 parents 3087cf1 + 400e435 commit f73d480
Show file tree
Hide file tree
Showing 12 changed files with 7,506 additions and 25 deletions.
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use Qossmic\Deptrac\Core\Layer\Collector\CollectorProvider;
use Qossmic\Deptrac\Core\Layer\Collector\CollectorResolver;
use Qossmic\Deptrac\Core\Layer\Collector\CollectorResolverInterface;
use Qossmic\Deptrac\Core\Layer\Collector\ComposerCollector;
use Qossmic\Deptrac\Core\Layer\Collector\DirectoryCollector;
use Qossmic\Deptrac\Core\Layer\Collector\ExtendsCollector;
use Qossmic\Deptrac\Core\Layer\Collector\FunctionNameCollector;
Expand Down Expand Up @@ -303,6 +304,9 @@
$services
->set(PhpInternalCollector::class)
->tag('collector', ['type' => CollectorType::TYPE_PHP_INTERNAL->value]);
$services
->set(ComposerCollector::class)
->tag('collector', ['type' => CollectorType::TYPE_COMPOSER->value]);

/*
* Analyser
Expand Down
41 changes: 28 additions & 13 deletions deptrac.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Qossmic\Deptrac\Contract\Analyser\ProcessEvent;
use Qossmic\Deptrac\Contract\Config\Collector\BoolConfig;
use Qossmic\Deptrac\Contract\Config\Collector\ClassNameConfig;
use Qossmic\Deptrac\Contract\Config\Collector\ComposerConfig;
use Qossmic\Deptrac\Contract\Config\Collector\DirectoryConfig;
use Qossmic\Deptrac\Contract\Config\DeptracConfig;
use Qossmic\Deptrac\Contract\Config\EmitterType;
Expand Down Expand Up @@ -32,9 +33,11 @@
),
$ast = Layer::withName('Ast')->collectors(
DirectoryConfig::create('src/Core/Ast/.*'),
ClassNameConfig::create('^PHPStan\\PhpDocParser\\.*')->private(),
ClassNameConfig::create('^PhpParser\\.*')->private(),
ClassNameConfig::create('^phpDocumentor\\Reflection\\.*')->private(),
ComposerConfig::create()
->addPackage('phpstan/phpdoc-parser')
->addPackage('nikic/php-parser')
->addPackage('phpdocumentor/type-resolver')
->private(),
),
$console = Layer::withName('Console')->collectors(
DirectoryConfig::create('src/Supportive/Console/.*')
Expand All @@ -56,7 +59,8 @@
),
$outputFormatter = Layer::withName('OutputFormatter')->collectors(
DirectoryConfig::create('src/Supportive/OutputFormatter/.*'),
ClassNameConfig::create('^phpDocumentor\\GraphViz\\.*')->private(),
ComposerConfig::create('composer.json', 'composer.lock')
->addPackage('phpdocumentor/graphviz')->private(),
),
$file = Layer::withName('File')->collectors(
DirectoryConfig::create('src/Supportive/File/.*')
Expand All @@ -69,24 +73,35 @@
->mustNot(DirectoryConfig::create('src/Supportive/.*/.*'))
->must(DirectoryConfig::create('src/Supportive/.*'))
),
$symfony = Layer::withName('Symfony')->collectors(
ComposerConfig::create()
->addPackage('symfony/config')
->addPackage('symfony/console')
->addPackage('symfony/dependency-injection')
->addPackage('symfony/event-dispatcher')
->addPackage('symfony/filesystem')
->addPackage('symfony/finder')
->addPackage('symfony/yaml'),
),
)
->rulesets(
Ruleset::forLayer($layer)->accesses($ast),
Ruleset::forLayer($console)->accesses($analyser, $outputFormatter, $dependencyInjection, $file, $time),
Ruleset::forLayer($layer)->accesses($ast, $symfony),
Ruleset::forLayer($console)->accesses($analyser, $outputFormatter, $dependencyInjection, $file, $time, $symfony),
Ruleset::forLayer($dependency)->accesses($ast),
Ruleset::forLayer($analyser)->accesses($layer, $dependency, $ast),
Ruleset::forLayer($outputFormatter)->accesses($dependencyInjection),
Ruleset::forLayer($ast)->accesses($file, $inputCollector),
Ruleset::forLayer($inputCollector)->accesses($file),
Ruleset::forLayer($analyser)->accesses($layer, $dependency, $ast, $symfony),
Ruleset::forLayer($outputFormatter)->accesses($dependencyInjection, $symfony),
Ruleset::forLayer($ast)->accesses($file, $inputCollector, $symfony),
Ruleset::forLayer($inputCollector)->accesses($file, $symfony),
Ruleset::forLayer($supportive)->accesses($file),
Ruleset::forLayer($contract),
Ruleset::forLayer($contract)->accesses($symfony),
Ruleset::forLayer($file)->accesses($symfony),
Ruleset::forLayer($dependencyInjection)->accesses($symfony),
)
->formatters(
GraphvizConfig::create()
->pointsToGroup(true)
->groups('Contract', $contract)
->groups('Supportive', $supportive, $file)
->groups('Symfony', $console, $dependencyInjection, $outputFormatter)
->groups('Supportive', $supportive, $file, $symfony, $console, $dependencyInjection, $outputFormatter, $time)
->groups('Core', $analyser, $ast, $dependency, $inputCollector, $layer)
);
};
51 changes: 39 additions & 12 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ deptrac:
Supportive:
- Supportive
- File
Symfony:
- Console
- Time
- DependencyInjection
- OutputFormatter
- Symfony
Core:
- Analyser
- Ast
Expand All @@ -46,15 +46,14 @@ deptrac:
collectors:
- type: directory
value: src/Core/Ast/.*
- type: className
value: ^PHPStan\\PhpDocParser\\.*
private: true
- type: className
value: ^PhpParser\\.*
private: true
- type: className
value: ^phpDocumentor\\Reflection\\.*
- type: composer
private: true
composerPath: composer.json
composerLockPath: composer.lock
packages:
- phpstan/phpdoc-parser
- nikic/php-parser
- phpdocumentor/type-resolver
- name: Console
collectors:
- type: directory
Expand Down Expand Up @@ -83,9 +82,12 @@ deptrac:
collectors:
- type: directory
value: src/Supportive/OutputFormatter/.*
- type: className
value: ^phpDocumentor\\GraphViz\\.*
- type: composer
private: true
composerPath: composer.json
composerLockPath: composer.lock
packages:
- phpdocumentor/graphviz
- name: File
collectors:
- type: directory
Expand All @@ -103,26 +105,51 @@ deptrac:
must:
- type: directory
value: src/Supportive/.*
- name: Symfony
collectors:
- type: composer
composerPath: composer.json
composerLockPath: composer.lock
packages:
- symfony/config
- symfony/console
- symfony/dependency-injection
- symfony/event-dispatcher
- symfony/filesystem
- symfony/finder
- symfony/yaml

ruleset:
Layer:
- Ast
- Symfony
Console:
- Analyser
- OutputFormatter
- DependencyInjection
- File
- Time
- Symfony
Dependency:
- Ast
Analyser:
- Layer
- Dependency
- Ast
- Symfony
OutputFormatter:
- DependencyInjection
- Symfony
Ast:
- File
- InputCollector
- Symfony
InputCollector:
- File
- Symfony
DependencyInjection:
- Symfony
Contract:
- Symfony
File:
- Symfony
20 changes: 20 additions & 0 deletions docs/collectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ deptrac:
Every class name that matches the regular expression becomes a part of the
*controller* layer.

## `composer` Collector

The `composer` collector allows you to define dependencies on composer `require` or `require-dev` packages that follow PSR-0 or PSR-4 autoloading convention. With this collector you can for example enforce:
- That your `require-dev` dependencies are only used in you non-production code (like DB migrations or SA tools)
- That your code does not use any transitive dependencies (dependencies on packages installed only because your `composer.json` required packages depend on them themselves)
- That some packages are only used in particular layers

```yaml
deptrac:
layers:
- name: Symfony
collectors:
- type: composer
composerPath: composer.json
composerLockPath: composer.lock
packages:
- symfony/config
- symfony/console
```

## `directory` Collector

The `directory` collector allows collecting classes by matching their file path
Expand Down
58 changes: 58 additions & 0 deletions src/Contract/Config/Collector/ComposerConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Qossmic\Deptrac\Contract\Config\Collector;

use Qossmic\Deptrac\Contract\Config\CollectorConfig;
use Qossmic\Deptrac\Contract\Config\CollectorType;

final class ComposerConfig extends CollectorConfig
{
protected CollectorType $collectorType = CollectorType::TYPE_COMPOSER;

/** @var list<string> */
private array $packages = [];

private function __construct(
private readonly string $composerPath,
private readonly string $composerLockPath,
) {
}

/**
* @param list<string> $packages
*/
public static function create(string $composerPath = 'composer.json', string $composerLockPath = 'composer.lock', array $packages = []): self
{
$result = new self($composerPath, $composerLockPath);
foreach ($packages as $package) {
$result->addPackage($package);
}

return $result;
}

public function addPackage(string $package): self
{
$this->packages[] = $package;

return $this;
}

/** @return array{
* composerPath: string,
* composerLockPath: string,
* packages: list<string>,
* private: bool,
* type: string}
*/
public function toArray(): array
{
return [
'composerPath' => $this->composerPath,
'composerLockPath' => $this->composerLockPath,
'packages' => $this->packages,
'private' => $this->private,
'type' => $this->collectorType->value,
];
}
}
1 change: 1 addition & 0 deletions src/Contract/Config/CollectorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ enum CollectorType: string
case TYPE_TRAIT = 'trait';
case TYPE_USES = 'uses';
case TYPE_PHP_INTERNAL = 'php_internal';
case TYPE_COMPOSER = 'composer';
}
50 changes: 50 additions & 0 deletions src/Core/Layer/Collector/ComposerCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Core\Layer\Collector;

use Qossmic\Deptrac\Contract\Ast\CouldNotParseFileException;
use Qossmic\Deptrac\Contract\Ast\TokenReferenceInterface;
use Qossmic\Deptrac\Contract\Layer\CollectorInterface;
use Qossmic\Deptrac\Contract\Layer\InvalidCollectorDefinitionException;
use RuntimeException;

final class ComposerCollector implements CollectorInterface
{
public function __construct()
{
}

public function satisfy(array $config, TokenReferenceInterface $reference): bool
{
if (!isset($config['composerPath']) || !is_string($config['composerPath'])) {
throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('ComposerCollector needs the path to the composer.json file as string.');
}

if (!isset($config['composerLockPath']) || !is_string($config['composerLockPath'])) {
throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('ComposerCollector needs the path to the composer.lock file as string.');
}

if (!isset($config['packages']) || !is_array($config['packages'])) {
throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('ComposerCollector needs the list of packages as string.');
}

try {
$composerFilesParser = new ComposerFilesParser($config['composerLockPath']);
} catch (RuntimeException $exception) {
throw new CouldNotParseFileException('Could not parse composer files.', 0, $exception);
}

$namespaces = $composerFilesParser->autoloadableNamespacesForRequirements($config['packages'], true);
$token = $reference->getToken()->toString();

foreach ($namespaces as $namespace) {
if (str_starts_with($token, $namespace)) {
return true;
}
}

return false;
}
}
Loading

0 comments on commit f73d480

Please sign in to comment.