Skip to content

Commit

Permalink
Resolve #189: Add feature for unused zombies
Browse files Browse the repository at this point in the history
Signed-off-by: Andreas Frömer <andreas.froemer@check24.de>
  • Loading branch information
Andreas Frömer authored and icanhazstring committed Jan 4, 2022
1 parent 7c63986 commit c1ee7e8
Show file tree
Hide file tree
Showing 19 changed files with 412 additions and 82 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@
- Added CLI argument `composer-json` which can be used to parse external projects.
This will default to the current working directory.
- Added error message when `composer.json` is not readable
- Added check for zombie exclusion. It will report if any excluded packages or pattern did match any package
### Changed
- Change `bin/composer-unused` to be used as standalone binary
- Package type is now `library` instead of `composer-plugin`
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -48,7 +48,8 @@
"autoload-dev": {
"psr-4": {
"ComposerUnused\\ComposerUnused\\Test\\Integration\\": "tests/Integration",
"ComposerUnused\\ComposerUnused\\Test\\Unit\\": "tests/Unit"
"ComposerUnused\\ComposerUnused\\Test\\Unit\\": "tests/Unit",
"ComposerUnused\\ComposerUnused\\Test\\Stubs\\": "tests/Stubs"
}
},
"bin": [
Expand Down
45 changes: 7 additions & 38 deletions src/Command/FilterDependencyCollectionCommand.php
Expand Up @@ -5,59 +5,28 @@
namespace ComposerUnused\ComposerUnused\Command;

use ComposerUnused\ComposerUnused\Dependency\DependencyCollection;

use function array_merge;
use ComposerUnused\ComposerUnused\Filter\FilterCollection;

final class FilterDependencyCollectionCommand
{
private const GLOBAL_NAMED_EXCLUSION = [
'composer-plugin-api'
];

private const GLOBAL_PATTERN_EXCLUSION = [
'/-implementation$/i'
];

/** @var DependencyCollection */
private $requiredDependencyCollection;
/** @var array<string> */
private $namedExclusion;
/** @var array<string> */
private $patternExclusion;
private DependencyCollection $requiredDependencyCollection;
private FilterCollection $filterCollection;

/**
* @param DependencyCollection $requiredDependencyCollection
* @param array<string> $namedExclusion
* @param array<string> $patternExclusion
*/
public function __construct(
DependencyCollection $requiredDependencyCollection,
array $namedExclusion = [],
array $patternExclusion = []
FilterCollection $filterCollection
) {
$this->requiredDependencyCollection = $requiredDependencyCollection;
$this->namedExclusion = array_merge(self::GLOBAL_NAMED_EXCLUSION, $namedExclusion);
$this->patternExclusion = array_merge(self::GLOBAL_PATTERN_EXCLUSION, $patternExclusion);
$this->filterCollection = $filterCollection;
}

public function getRequiredDependencyCollection(): DependencyCollection
{
return $this->requiredDependencyCollection;
}

/**
* @return array<string>
*/
public function getNamedExclusion(): array
{
return $this->namedExclusion;
}

/**
* @return array<string>
*/
public function getPatternExclusion(): array
public function getFilters(): FilterCollection
{
return $this->patternExclusion;
return $this->filterCollection;
}
}
26 changes: 10 additions & 16 deletions src/Command/Handler/CollectFilteredDependenciesCommandHandler.php
Expand Up @@ -12,24 +12,18 @@ final class CollectFilteredDependenciesCommandHandler
{
public function collect(FilterDependencyCollectionCommand $command): DependencyCollection
{
$namedExclusion = $command->getNamedExclusion();
$patternExclusion = $command->getPatternExclusion();
$filters = $command->getFilters();

return $command->getRequiredDependencyCollection()->filter(static function (DependencyInterface $dependency) use (
$namedExclusion,
$patternExclusion
) {
if (in_array($dependency->getName(), $namedExclusion, true)) {
return false;
}

foreach ($patternExclusion as $exclusion) {
if (preg_match($exclusion, $dependency->getName())) {
return false;
return $command->getRequiredDependencyCollection()->filter(
static function (DependencyInterface $dependency) use ($filters) {
foreach ($filters as $filter) {
if ($filter->applies($dependency)) {
return false;
}
}
}

return true;
});
return true;
}
);
}
}
10 changes: 10 additions & 0 deletions src/Composer/Config.php
Expand Up @@ -15,6 +15,8 @@ final class Config
private array $autoload = [];
/** @var array<string, string> */
private array $suggests = [];
/** @var array<string, mixed> */
private array $extra = [];

public function getName(): string
{
Expand Down Expand Up @@ -45,6 +47,14 @@ public function getSuggests(): array
return $this->suggests;
}

/**
* @return array<string, mixed>
*/
public function getExtra(): array
{
return $this->extra;
}

public function get(string $property): string
{
$value = $this->config[$property] ?? null;
Expand Down
32 changes: 27 additions & 5 deletions src/Console/Command/UnusedCommand.php
Expand Up @@ -17,6 +17,7 @@
use ComposerUnused\ComposerUnused\Dependency\DependencyInterface;
use ComposerUnused\ComposerUnused\Dependency\InvalidDependency;
use ComposerUnused\ComposerUnused\Dependency\RequiredDependency;
use ComposerUnused\ComposerUnused\Filter\FilterCollection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -141,11 +142,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
)
);

$filterCollection = new FilterCollection(
array_merge(
$config->getExtra()['unused'] ?? [],
$input->getOption('excludePackage')
),
[] // TODO pattern exclusion from CLI
);

$requiredDependencyCollection = $this->collectFilteredDependenciesCommandHandler->collect(
new FilterDependencyCollectionCommand(
$unfilteredRequiredDependencyCollection,
$input->getOption('excludePackage'),
[] // TODO use pattern exclude option from command line
$filterCollection
)
);

Expand Down Expand Up @@ -199,10 +207,11 @@ static function (DependencyInterface $dependency) {

$io->writeln(
sprintf(
'Found <fg=green>%d used</>, <fg=red>%d unused</> and <fg=yellow>%d ignored</> packages',
'Found <fg=green>%d used</>, <fg=red>%d unused</>, <fg=yellow>%d ignored</> and <fg=gray>%d zombie</> packages',
count($usedDependencyCollection),
count($unusedDependencyCollection),
count($invalidDependencyCollection)
count($invalidDependencyCollection),
count($filterCollection->getUnused())
)
);

Expand Down Expand Up @@ -271,7 +280,20 @@ static function (DependencyInterface $dependency) {
);
}

if ($unusedDependencyCollection->count() > 0) {
$io->newLine();
$io->text('<fg=gray>Zombies exclusions</> (<fg=cyan>did not match any package)</>)');

foreach ($filterCollection->getUnused() as $filter) {
$io->writeln(
sprintf(
' <fg=gray>%s</> %s',
"\u{1F480}",
$filter->toString()
)
);
}

if ($unusedDependencyCollection->count() > 0 || count($filterCollection->getUnused()) > 0) {
return 1;
}

Expand Down
85 changes: 85 additions & 0 deletions src/Filter/FilterCollection.php
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace ComposerUnused\ComposerUnused\Filter;

use ArrayIterator;
use Countable;
use IteratorAggregate;

/**
* @implements IteratorAggregate<FilterInterface>
*/
final class FilterCollection implements IteratorAggregate, Countable
{
/**
* List of global named packages which should be excluded from unused check
* Key => package name
* Value => always "used"
*/
private const GLOBAL_NAMED_EXCLUSION = [
'composer-plugin-api' => true
];

/**
* List of global pattern which should be excluded from unused check
* Key => pattern
* Value => always "used"
*/
private const GLOBAL_PATTERN_EXCLUSION = [
'/-implementation$/i' => true
];

/** @var array<FilterInterface> */
private array $items;

/**
* @param array<string> $namedFilter
* @param array<string> $patternFilter
*/
public function __construct(array $namedFilter, array $patternFilter)
{
$globalNamedFilter = array_map(
static fn(string $named, $used) => new NamedFilter($named, $used),
array_keys(self::GLOBAL_NAMED_EXCLUSION),
array_values(self::GLOBAL_NAMED_EXCLUSION)
);

$globalPatternFilter =
array_map(
static fn(string $pattern, $used) => new PatternFilter($pattern, $used),
array_keys(self::GLOBAL_PATTERN_EXCLUSION),
array_values(self::GLOBAL_PATTERN_EXCLUSION)
);

$named = array_map(static fn(string $named) => new NamedFilter($named), $namedFilter);
$pattern = array_map(static fn(string $pattern) => new PatternFilter($pattern), $patternFilter);

$this->items = array_merge($globalNamedFilter, $globalPatternFilter, $named, $pattern);
}

/**
* @return ArrayIterator<int, FilterInterface>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->items);
}

public function count(): int
{
return count($this->items);
}

/**
* @return array<FilterInterface>
*/
public function getUnused(): array
{
return array_filter(
$this->items,
static fn(FilterInterface $filter) => $filter->used() === false
);
}
}
16 changes: 16 additions & 0 deletions src/Filter/FilterInterface.php
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace ComposerUnused\ComposerUnused\Filter;

use ComposerUnused\ComposerUnused\Dependency\DependencyInterface;

interface FilterInterface
{
public function applies(DependencyInterface $dependency): bool;

public function used(): bool;

public function toString(): string;
}
35 changes: 35 additions & 0 deletions src/Filter/NamedFilter.php
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace ComposerUnused\ComposerUnused\Filter;

use ComposerUnused\ComposerUnused\Dependency\DependencyInterface;

final class NamedFilter implements FilterInterface
{
private string $filterString;
private bool $used = false;
private bool $alwaysUsed;

public function __construct(string $filterString, bool $alwaysUsed = false)
{
$this->filterString = $filterString;
$this->alwaysUsed = $alwaysUsed;
}

public function applies(DependencyInterface $dependency): bool
{
return $this->used = $dependency->getName() === $this->filterString;
}

public function used(): bool
{
return $this->alwaysUsed || $this->used;
}

public function toString(): string
{
return $this->filterString;
}
}
35 changes: 35 additions & 0 deletions src/Filter/PatternFilter.php
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace ComposerUnused\ComposerUnused\Filter;

use ComposerUnused\ComposerUnused\Dependency\DependencyInterface;

final class PatternFilter implements FilterInterface
{
private string $pattern;
private bool $used = false;
private bool $alwaysUsed;

public function __construct(string $pattern, bool $alwaysUsed = false)
{
$this->pattern = $pattern;
$this->alwaysUsed = $alwaysUsed;
}

public function applies(DependencyInterface $dependency): bool
{
return $this->used = (bool)preg_match($this->pattern, $dependency->getName());
}

public function used(): bool
{
return $this->alwaysUsed || $this->used;
}

public function toString(): string
{
return $this->pattern;
}
}

0 comments on commit c1ee7e8

Please sign in to comment.