Skip to content
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
2 changes: 2 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
arguments:
tmpDir: %tmpDir%
offloadCollectorData: %shipmonkDeadCode.cache.offloadCollectorData%
-
class: ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector
-
class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy
-
Expand Down
78 changes: 78 additions & 0 deletions src/Composer/ComposerIntrospector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Composer;

use Composer\Autoload\ClassLoader;
use function array_filter;
use function array_keys;
use function count;
use function file_get_contents;
use function is_array;
use function is_file;
use function json_decode;
use function json_last_error;
use function reset;
use function str_starts_with;
use const JSON_ERROR_NONE;

final class ComposerIntrospector
{

/**
* Autodetection is possible only when exactly one non-phar autoloader is registered.
*/
public function autodetectVendorDir(): ?string
{
$vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool {
return !str_starts_with($vendorDir, 'phar://');
});

if (count($vendorDirs) !== 1) {
return null;
}

return reset($vendorDirs);
}

public function autodetectComposerJsonPath(): ?string
{
$vendorDir = $this->autodetectVendorDir();

if ($vendorDir === null) {
return null;
}

$composerJsonPath = $vendorDir . '/../composer.json';

if (!is_file($composerJsonPath)) {
return null;
}

return $composerJsonPath;
}

/**
* @return array<string, mixed>
*/
public function parseComposerJson(string $composerJsonPath): array
{
if (!is_file($composerJsonPath)) {
return [];
}

$composerJsonRawData = file_get_contents($composerJsonPath);

if ($composerJsonRawData === false) {
return [];
}

$composerJsonData = json_decode($composerJsonRawData, associative: true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($composerJsonData)) {
return [];
}

return $composerJsonData; // @phpstan-ignore return.type (composer.json keys are strings)
}

}
85 changes: 28 additions & 57 deletions src/Excluder/TestsUsageExcluder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,28 @@

namespace ShipMonk\PHPStan\DeadCode\Excluder;

use Composer\Autoload\ClassLoader;
use LogicException;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use function array_filter;
use function array_keys;
use function count;
use function dirname;
use function file_get_contents;
use function glob;
use function is_array;
use function is_file;
use function json_decode;
use function json_last_error;
use function is_string;
use function preg_match;
use function realpath;
use function reset;
use function str_contains;
use function str_starts_with;
use const JSON_ERROR_NONE;

final class TestsUsageExcluder implements MemberUsageExcluder
{

private readonly ReflectionProvider $reflectionProvider;

private readonly ComposerIntrospector $composerIntrospector;

private readonly bool $enabled;

/**
Expand All @@ -42,11 +36,13 @@ final class TestsUsageExcluder implements MemberUsageExcluder
*/
public function __construct(
ReflectionProvider $reflectionProvider,
ComposerIntrospector $composerIntrospector,
bool $enabled,
?array $devPaths,
)
{
$this->reflectionProvider = $reflectionProvider;
$this->composerIntrospector = $composerIntrospector;
$this->enabled = $enabled;

if ($devPaths !== null) {
Expand Down Expand Up @@ -120,78 +116,53 @@ private function getDeclarationFile(?string $className): ?string
*/
private function autodetectComposerDevPaths(): array
{
$vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool {
return !str_starts_with($vendorDir, 'phar://');
});

if (count($vendorDirs) !== 1) {
return [];
}

$vendorDir = reset($vendorDirs);
$composerJsonPath = $vendorDir . '/../composer.json';

$composerJsonData = $this->parseComposerJson($composerJsonPath);
$basePath = dirname($composerJsonPath);
$composerJsonPath = $this->composerIntrospector->autodetectComposerJsonPath();

return [
...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['psr-0'] ?? []),
...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['psr-4'] ?? []),
...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['files'] ?? []),
...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['classmap'] ?? []),
];
}

/**
* @return array{
* autoload-dev?: array{
* psr-0?: array<string, string|string[]>,
* psr-4?: array<string, string|string[]>,
* files?: string[],
* classmap?: string[],
* }
* }
*/
private function parseComposerJson(string $composerJsonPath): array
{
if (!is_file($composerJsonPath)) {
if ($composerJsonPath === null) {
return [];
}

$composerJsonRawData = file_get_contents($composerJsonPath);
$composerJsonData = $this->composerIntrospector->parseComposerJson($composerJsonPath);
$autoloadDev = $composerJsonData['autoload-dev'] ?? [];

if ($composerJsonRawData === false) {
if (!is_array($autoloadDev)) {
return [];
}

$composerJsonData = json_decode($composerJsonRawData, associative: true);

$jsonError = json_last_error();

if ($jsonError !== JSON_ERROR_NONE) {
return [];
}
$basePath = dirname($composerJsonPath);

return $composerJsonData; // @phpstan-ignore-line ignore mixed returned
return [
...$this->extractAutoloadPaths($basePath, $autoloadDev['psr-0'] ?? []),
...$this->extractAutoloadPaths($basePath, $autoloadDev['psr-4'] ?? []),
...$this->extractAutoloadPaths($basePath, $autoloadDev['files'] ?? []),
...$this->extractAutoloadPaths($basePath, $autoloadDev['classmap'] ?? []),
];
}

/**
* @param array<string|array<string>> $autoload
* @return list<string>
*/
private function extractAutoloadPaths(
string $basePath,
array $autoload,
mixed $autoload,
): array
{
if (!is_array($autoload)) {
return [];
}

$result = [];

foreach ($autoload as $paths) {
if (!is_array($paths)) {
$paths = [$paths]; // @phpstan-ignore shipmonk.variableTypeOverwritten
$paths = [$paths];
}

foreach ($paths as $path) {
if (!is_string($path)) {
continue;
}

$isAbsolute = preg_match('#([a-z]:)?[/\\\\]#Ai', $path);

if ($isAbsolute === 1) {
Expand Down
48 changes: 7 additions & 41 deletions src/Provider/ComposerUsageProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@

namespace ShipMonk\PHPStan\DeadCode\Provider;

use Composer\Autoload\ClassLoader;
use LogicException;
use PHPStan\Reflection\ReflectionProvider;
use ReflectionMethod;
use function array_filter;
use function array_keys;
use function count;
use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector;
use function explode;
use function file_get_contents;
use function is_array;
use function is_file;
use function is_string;
use function json_decode;
use function json_last_error;
use function ltrim;
use function reset;
use function sprintf;
use function str_contains;
use function str_starts_with;
use const JSON_ERROR_NONE;

/**
* Detects static methods referenced as PHP callbacks in the scripts section of composer.json,
Expand All @@ -34,6 +26,8 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider

private readonly ReflectionProvider $reflectionProvider;

private readonly ComposerIntrospector $composerIntrospector;

/**
* declaring class => [method => note]
*
Expand All @@ -43,11 +37,13 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider

public function __construct(
ReflectionProvider $reflectionProvider,
ComposerIntrospector $composerIntrospector,
bool $enabled,
?string $composerJsonPath,
)
{
$this->reflectionProvider = $reflectionProvider;
$this->composerIntrospector = $composerIntrospector;
$this->loadScriptCallbacks($enabled, $composerJsonPath);
}

Expand All @@ -61,7 +57,7 @@ private function loadScriptCallbacks(
}

if ($composerJsonPath === null) {
$autodetectedPath = $this->autodetectComposerJsonPath();
$autodetectedPath = $this->composerIntrospector->autodetectComposerJsonPath();

if ($autodetectedPath !== null) {
$this->extractScriptCallbacks($autodetectedPath);
Expand All @@ -88,18 +84,7 @@ protected function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsa

private function extractScriptCallbacks(string $composerJsonPath): void
{
$composerJsonRawData = file_get_contents($composerJsonPath);

if ($composerJsonRawData === false) {
return;
}

$composerJsonData = json_decode($composerJsonRawData, associative: true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($composerJsonData)) {
return;
}

$composerJsonData = $this->composerIntrospector->parseComposerJson($composerJsonPath);
$scripts = $composerJsonData['scripts'] ?? [];

if (!is_array($scripts)) {
Expand Down Expand Up @@ -155,23 +140,4 @@ private function isPhpScript(string $listener): bool
&& str_contains($listener, '::');
}

private function autodetectComposerJsonPath(): ?string
{
$vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool {
return !str_starts_with($vendorDir, 'phar://');
});

if (count($vendorDirs) !== 1) {
return null;
}

$composerJsonPath = reset($vendorDirs) . '/../composer.json';

if (!is_file($composerJsonPath)) {
return null;
}

return $composerJsonPath;
}

}
Loading
Loading