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

Report dead dependencies #7

Merged
merged 3 commits into from
Jan 4, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Composer dependency analyser

This package aims to detect composer dependency issues in your project, fast!
This package aims to detect composer dependency issues in your project.

For example, it detects shadowed depencencies similar to [maglnet/composer-require-checker](https://github.com/maglnet/ComposerRequireChecker), but **much faster**:
It detects **shadowed dependencies** and **dead dependencies** similar to other tools, but **MUCH faster**:

| Project | Analysis of 13k files |
|---------------------------------------|-----------------------|
| shipmonk/composer-dependency-analyser | 2 secs |
| maglnet/composer-require-checker | 124 secs |
| Project | Dead dependency | Shadow dependency | Time* |
|-------------------------------------------|------------------|-------------------|-------------|
| maglnet/composer-require-checker | ❌ | ✅ | 124 secs |
| icanhazstring/composer-unused | ✅ | ❌ | 72 secs |
| **shipmonk/composer-dependency-analyser** | ✅ | ✅ | **3 secs** |

<sup><sub>\*Time measured on codebase with ~13 000 files</sub></sup>


This means you can safely add this tool to CI without wasting resources.

## Installation:

Expand Down Expand Up @@ -38,17 +44,19 @@ Found shadow dependencies!

You can add `--verbose` flag to see first usage (file & line) of each class.

## What it does:
## Detected issues:
This tool reads your `composer.json` and scans all paths listed in both `autoload` sections while analysing:

- Shadowed dependencies
- **Shadowed dependencies**
- Those are dependencies of your dependencies, which are not listed in `composer.json`
- Your code can break when your direct dependency gets updated to newer version which does not require that shadowed dependency anymore
- You should list all those classes within your dependencies
- Dev dependencies used in production code
- **Unused dependencies**
- Any non-dev dependency is expected to have at least single usage within the scanned paths
- **Dev dependencies in production code**
- Your code can break once you run your application with `composer install --no-dev`
- You should move those to `require` from `require-dev`
- Unknown classes
- **Unknown classes**
- Any class missing in composer classmap gets reported as we cannot say if that one is shadowed or not
- This might be expected in some cases, so you can disable this behaviour by `--ignore-unknown-classes`

Expand All @@ -57,9 +65,6 @@ If you want to run it elsewhere, you can use `--composer-json=path/to/composer.j

Currently, it only supports those autoload sections: `psr-4`, `psr-0`, `files`.

## Future scope:
- Detecting dead dependencies

## Limitations:
- Files without namespace has limited support
- Only classes with use statements and FQNs are detected
Expand Down
45 changes: 45 additions & 0 deletions src/ClassUsage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace ShipMonk\Composer;

class ClassUsage
{

/**
* @var string
*/
private $classname;

/**
* @var string
*/
private $filepath;

/**
* @var int
*/
private $lineNumber;

public function __construct(string $classname, string $filepath, int $lineNumber)
{
$this->classname = $classname;
$this->filepath = $filepath;
$this->lineNumber = $lineNumber;
}

public function getClassname(): string
{
return $this->classname;
}

public function getFilepath(): string
{
return $this->filepath;
}

public function getLineNumber(): int
{
return $this->lineNumber;
}

}
42 changes: 35 additions & 7 deletions src/ComposerDependencyAnalyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@
use ShipMonk\Composer\Error\DevDependencyInProductionCodeError;
use ShipMonk\Composer\Error\ShadowDependencyError;
use ShipMonk\Composer\Error\SymbolError;
use ShipMonk\Composer\Error\UnusedDependencyError;
use UnexpectedValueException;
use function array_diff;
use function array_filter;
use function array_keys;
use function array_values;
use function class_exists;
use function defined;
use function explode;
use function file_get_contents;
use function function_exists;
use function get_class;
use function interface_exists;
use function is_file;
use function ksort;
use function realpath;
use function str_replace;
use function strlen;
use function substr;
use function trim;
use function usort;
use const DIRECTORY_SEPARATOR;

class ComposerDependencyAnalyser
Expand Down Expand Up @@ -80,11 +86,12 @@ public function __construct(

/**
* @param array<string, bool> $scanPaths path => is dev path
* @return array<string, SymbolError>
* @return list<SymbolError>
*/
public function scan(array $scanPaths): array
{
$errors = [];
$usedPackages = [];

foreach ($scanPaths as $scanPath => $isDevPath) {
foreach ($this->listPhpFilesIn($scanPath) as $filePath) {
Expand All @@ -99,7 +106,7 @@ public function scan(array $scanPaths): array

if (!isset($this->optimizedClassmap[$usedSymbol])) {
if (!$this->isConstOrFunction($usedSymbol)) {
$errors[$usedSymbol] = new ClassmapEntryMissingError($usedSymbol, $filePath, $lineNumber);
$errors[$usedSymbol] = new ClassmapEntryMissingError(new ClassUsage($usedSymbol, $filePath, $lineNumber));
}

continue;
Expand All @@ -114,19 +121,40 @@ public function scan(array $scanPaths): array
$packageName = $this->getPackageNameFromVendorPath($classmapPath);

if ($this->isShadowDependency($packageName)) {
$errors[$usedSymbol] = new ShadowDependencyError($usedSymbol, $packageName, $filePath, $lineNumber);
$errors[$packageName] = new ShadowDependencyError($packageName, new ClassUsage($usedSymbol, $filePath, $lineNumber));
}

if (!$isDevPath && $this->isDevDependency($packageName)) {
$errors[$usedSymbol] = new DevDependencyInProductionCodeError($usedSymbol, $packageName, $filePath, $lineNumber);
$errors[$packageName] = new DevDependencyInProductionCodeError($packageName, new ClassUsage($usedSymbol, $filePath, $lineNumber));
}

$usedPackages[$packageName] = true;
}
}
}

ksort($errors);
$unusedDependencies = array_diff(
array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) {
return !$devDependency; // dev deps are typically used only in CI
})),
array_keys($usedPackages)
);

foreach ($unusedDependencies as $unusedDependency) {
$errors[] = new UnusedDependencyError($unusedDependency);
}

return $errors;
usort($errors, static function (SymbolError $a, SymbolError $b): int {
return [
get_class($a),
$a->getPackageName() ?? '',
] <=> [
get_class($b),
$b->getPackageName() ?? '',
];
});

return array_values($errors);
}

private function isShadowDependency(string $packageName): bool
Expand Down
38 changes: 8 additions & 30 deletions src/Error/ClassmapEntryMissingError.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,31 @@

namespace ShipMonk\Composer\Error;

use ShipMonk\Composer\ClassUsage;

class ClassmapEntryMissingError implements SymbolError
{

/**
* @var string
*/
private $className;

/**
* @var string
*/
private $exampleUsageFilepath;

/**
* @var int
* @var ClassUsage
*/
private $exampleUsageLine;
private $exampleUsage;

public function __construct(
string $className,
string $exampleUsageFilepath,
int $exampleUsageLine
ClassUsage $exampleUsage
)
{
$this->className = $className;
$this->exampleUsageFilepath = $exampleUsageFilepath;
$this->exampleUsageLine = $exampleUsageLine;
}

public function getSymbolName(): string
{
return $this->className;
}

public function getExampleUsageFilepath(): string
{
return $this->exampleUsageFilepath;
$this->exampleUsage = $exampleUsage;
}

public function getPackageName(): ?string
{
return null;
}

public function getExampleUsageLine(): int
public function getExampleUsage(): ClassUsage
{
return $this->exampleUsageLine;
return $this->exampleUsage;
}

}
38 changes: 8 additions & 30 deletions src/Error/DevDependencyInProductionCodeError.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,38 @@

namespace ShipMonk\Composer\Error;

use ShipMonk\Composer\ClassUsage;

class DevDependencyInProductionCodeError implements SymbolError
{

/**
* @var string
*/
private $className;

/**
* @var string
*/
private $packageName;

/**
* @var string
* @var ClassUsage
*/
private $exampleUsageFilepath;

/**
* @var int
*/
private $exampleUsageLine;
private $exampleUsage;

public function __construct(
string $className,
string $packageName,
string $exampleUsageFilepath,
int $exampleUsageLine
ClassUsage $exampleUsage
)
{
$this->className = $className;
$this->packageName = $packageName;
$this->exampleUsageFilepath = $exampleUsageFilepath;
$this->exampleUsageLine = $exampleUsageLine;
$this->exampleUsage = $exampleUsage;
}

public function getPackageName(): string
{
return $this->packageName;
}

public function getSymbolName(): string
{
return $this->className;
}

public function getExampleUsageFilepath(): string
{
return $this->exampleUsageFilepath;
}

public function getExampleUsageLine(): int
public function getExampleUsage(): ClassUsage
{
return $this->exampleUsageLine;
return $this->exampleUsage;
}

}
Loading