Skip to content

Commit

Permalink
Symfony: detect methods called by DIC
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Dec 23, 2024
1 parent c9602b5 commit e642b86
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 38 deletions.
65 changes: 53 additions & 12 deletions src/Provider/SymfonyUsageProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
namespace ShipMonk\PHPStan\DeadCode\Provider;

use Composer\InstalledVersions;
use LogicException;
use PhpParser\Node;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\Scope;
use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
use PHPStan\Node\InClassNode;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Symfony\ServiceMapFactory;
use PHPStan\Symfony\Configuration as PHPStanSymfonyConfiguration;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use Reflector;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use SimpleXMLElement;
use function file_get_contents;
use function simplexml_load_string;
use function sprintf;
use const PHP_VERSION_ID;

class SymfonyUsageProvider implements MemberUsageProvider
Expand All @@ -25,19 +30,21 @@ class SymfonyUsageProvider implements MemberUsageProvider
private bool $enabled;

/**
* @var array<string, true>
* class => methods[]
*
* @var array<string, array<string, true>>
*/
private array $dicClasses = [];

public function __construct(
?ServiceMapFactory $serviceMapFactory,
?PHPStanSymfonyConfiguration $symfonyConfiguration,
?bool $enabled
)
{
$this->enabled = $enabled ?? $this->isSymfonyInstalled();

if ($serviceMapFactory !== null) {
$this->fillDicClasses($serviceMapFactory);
if ($symfonyConfiguration !== null && $symfonyConfiguration->getContainerXmlPath() !== null) { // @phpstan-ignore phpstanApi.method
$this->fillDicClasses($symfonyConfiguration->getContainerXmlPath()); // @phpstan-ignore phpstanApi.method
}
}

Expand Down Expand Up @@ -142,7 +149,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
}
}

// phpcs:enable // phpcs:disable Squiz.PHP.CommentedOutCode.Found
// phpcs:disable Squiz.PHP.CommentedOutCode.Found

return $usages;
}
Expand All @@ -159,7 +166,7 @@ private function getUsagesFromReflection(InClassNode $node): array
$usages = [];

foreach ($nativeReflection->getMethods() as $method) {
if ($method->isConstructor() && isset($this->dicClasses[$className])) {
if (isset($this->dicClasses[$className][$method->getName()])) {
$usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()));
}

Expand All @@ -186,16 +193,50 @@ protected function shouldMarkAsUsed(ReflectionMethod $method): bool
|| $this->isProbablySymfonyListener($method);
}

protected function fillDicClasses(ServiceMapFactory $serviceMapFactory): void
protected function fillDicClasses(string $containerXmlPath): void
{
foreach ($serviceMapFactory->create()->getServices() as $service) { // @phpstan-ignore phpstanApi.method
$dicClass = $service->getClass();
$fileContents = file_get_contents($containerXmlPath);

if ($fileContents === false) {
throw new LogicException(sprintf('Container %s does not exist', $containerXmlPath));
}

$xml = @simplexml_load_string($fileContents);

if ($xml === false) {
throw new LogicException(sprintf('Container %s cannot be parsed', $containerXmlPath));
}

if (!isset($xml->services->service)) {
throw new LogicException(sprintf('XML %s does not contain container.services.service structure', $containerXmlPath));
}

foreach ($xml->services->service as $serviceDefinition) {
/** @var SimpleXMLElement $serviceAttributes */
$serviceAttributes = $serviceDefinition->attributes();
$class = isset($serviceAttributes->class) ? (string) $serviceAttributes->class : null;

if ($dicClass === null) {
if ($class === null) {
continue;
}

$this->dicClasses[$dicClass] = true;
$this->dicClasses[$class]['__construct'] = true;

if (!isset($serviceDefinition->call)) {
continue;
}

foreach ($serviceDefinition->call as $callDefinition) {
/** @var SimpleXMLElement $callAttributes */
$callAttributes = $callDefinition->attributes();
$method = $callAttributes->method !== null ? (string) $callAttributes->method : null;

if ($method === null) {
continue;
}

$this->dicClasses[$class][$method] = true;
}
}
}

Expand Down
27 changes: 2 additions & 25 deletions tests/Rule/DeadCodeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Symfony\ServiceDefinition;
use PHPStan\Symfony\ServiceMap;
use PHPStan\Symfony\ServiceMapFactory;
use PHPStan\Symfony\Configuration;
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector;
use ShipMonk\PHPStan\DeadCode\Collector\ConstantFetchCollector;
Expand Down Expand Up @@ -410,7 +408,7 @@ private function createSymfonyUsageProvider(): SymfonyUsageProvider

if ($cache === null) {
$cache = new SymfonyUsageProvider(
$this->createServiceMapFactoryMock(),
new Configuration(['containerXmlPath' => __DIR__ . '/data/providers/symfony/services.xml']), // @phpstan-ignore phpstanApi.constructor
true,
);
}
Expand All @@ -434,27 +432,6 @@ static function (string $type): array {
return $mock;
}

private function createServiceMapFactoryMock(): ServiceMapFactory
{
$service1Mock = $this->createMock(ServiceDefinition::class);
$service1Mock->method('getClass')
->willReturn('Symfony\DicClass1');

$service2Mock = $this->createMock(ServiceDefinition::class);
$service2Mock->method('getClass')
->willReturn('Symfony\DicClass2');

$serviceMapMock = $this->createMock(ServiceMap::class);
$serviceMapMock->method('getServices')
->willReturn([$service1Mock, $service2Mock]);

$factoryMock = $this->createMock(ServiceMapFactory::class); // @phpstan-ignore phpstanApi.classConstant
$factoryMock->method('create')
->willReturn($serviceMapMock);

return $factoryMock;
}

public function gatherAnalyserErrors(array $files): array
{
if (!$this->unwrapGroupedErrors) {
Expand Down
2 changes: 1 addition & 1 deletion tests/Rule/data/providers/symfony.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function __construct() {}
}

class DicClass1 extends DicClassParent {

public function calledViaDic(): void {}
}

class DicClass2 {
Expand Down
11 changes: 11 additions & 0 deletions tests/Rule/data/providers/symfony/services.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Symfony\DicClass1" class="Symfony\DicClass1" public="true" autowire="true" autoconfigure="true">
<call method="calledViaDic">
</call>
</service>
<service id="Symfony\DicClass2" class="Symfony\DicClass2" public="true" autowire="true" autoconfigure="true">
</service>
</services>
</container>

0 comments on commit e642b86

Please sign in to comment.