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

Resolve #189: Add feature for unused zombies #265

Merged
merged 1 commit into from Jan 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}