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

Symfony: autodetect constants used in yamls #131

Merged
merged 3 commits into from
Dec 27, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ includes:
- `#[Route]` attributes
- `EventSubscriberInterface::getSubscribedEvents`
- `onKernelResponse`, `onKernelRequest`, etc
- `!php const` references in `config` yamls

#### Doctrine:
- `#[AsEntityListener]` attribute
Expand Down
3 changes: 1 addition & 2 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@
intval=>null,
strval=>null,
settype=>null,
exit=>null,
reset=>array_key_first
exit=>null
"
/>
</properties>
Expand Down
2 changes: 2 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ parameters:
enabled: null
symfony:
enabled: null
configDir: null
doctrine:
enabled: null
nette:
Expand All @@ -136,6 +137,7 @@ parametersSchema:
])
symfony: structure([
enabled: schema(bool(), nullable())
configDir: schema(string(), nullable())
])
doctrine: structure([
enabled: schema(bool(), nullable())
Expand Down
129 changes: 125 additions & 4 deletions src/Provider/SymfonyUsageProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,44 @@

namespace ShipMonk\PHPStan\DeadCode\Provider;

use Composer\Autoload\ClassLoader;
use Composer\InstalledVersions;
use FilesystemIterator;
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\ClassReflection;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Symfony\Configuration as PHPStanSymfonyConfiguration;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use Reflector;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use SimpleXMLElement;
use SplFileInfo;
use UnexpectedValueException;
use function array_filter;
use function array_keys;
use function count;
use function explode;
use function file_get_contents;
use function in_array;
use function is_dir;
use function preg_match_all;
use function reset;
use function simplexml_load_string;
use function sprintf;
use function strpos;
use const PHP_VERSION_ID;

class SymfonyUsageProvider implements MemberUsageProvider
Expand All @@ -36,16 +54,29 @@ class SymfonyUsageProvider implements MemberUsageProvider
*/
private array $dicCalls = [];

/**
* class => [constant]
*
* @var array<string, list<string>>
*/
private array $dicConstants = [];

public function __construct(
?PHPStanSymfonyConfiguration $symfonyConfiguration,
?bool $enabled
?bool $enabled,
?string $configDir
)
{
$this->enabled = $enabled ?? $this->isSymfonyInstalled();
$resolvedConfigDir = $configDir ?? $this->autodetectConfigDir();

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

if ($this->enabled && $resolvedConfigDir !== null) {
$this->fillDicConstants($resolvedConfigDir);
}
}

public function getUsages(Node $node, Scope $scope): array
Expand All @@ -59,7 +90,8 @@ public function getUsages(Node $node, Scope $scope): array
if ($node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
$usages = [
...$usages,
...$this->getUsagesFromReflection($node),
...$this->getMethodUsagesFromReflection($node),
...$this->getConstantUsages($node->getClassReflection()),
];
}

Expand Down Expand Up @@ -157,7 +189,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
/**
* @return list<ClassMethodUsage>
*/
private function getUsagesFromReflection(InClassNode $node): array
private function getMethodUsagesFromReflection(InClassNode $node): array
{
$classReflection = $node->getClassReflection();
$nativeReflection = $classReflection->getNativeReflection();
Expand Down Expand Up @@ -374,4 +406,93 @@ private function createUsage(ExtendedMethodReflection $methodReflection): ClassM
);
}

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

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

$vendorDir = reset($vendorDirs);
$configDir = $vendorDir . '/../config';

if (is_dir($configDir)) {
return $configDir;
}

return null;
}

private function fillDicConstants(string $configDir): void
{
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($configDir, FilesystemIterator::SKIP_DOTS),
);
} catch (UnexpectedValueException $e) {
throw new LogicException("Provided config path '$configDir' is not a directory", 0, $e);
}

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
if (
$file->isFile()
&& in_array($file->getExtension(), ['yaml', 'yml'], true)
&& $file->getRealPath() !== false
) {
$this->extractYamlConstants($file->getRealPath());
}
}
}

private function extractYamlConstants(string $yamlFile): void
{
$dicFileContents = file_get_contents($yamlFile);

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

$nameRegex = '[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*'; // https://www.php.net/manual/en/language.oop5.basic.php

preg_match_all(
"~!php/const ($nameRegex(?:\\\\$nameRegex)+::$nameRegex)~",
$dicFileContents,
$matches,
);

foreach ($matches[1] as $usedConstants) {
[$className, $constantName] = explode('::', $usedConstants); // @phpstan-ignore offsetAccess.notFound
$this->dicConstants[$className][] = $constantName;
}
}

/**
* @return list<ClassConstantUsage>
*/
private function getConstantUsages(ClassReflection $classReflection): array
{
$usages = [];

foreach ($this->dicConstants[$classReflection->getName()] ?? [] as $constantName) {
if (!$classReflection->hasConstant($constantName)) {
continue;
}

$usages[] = new ClassConstantUsage(
null,
new ClassConstantRef(
$classReflection->getName(),
$constantName,
false,
),
);
}

return $usages;
}

}
19 changes: 4 additions & 15 deletions tests/Rule/DeadCodeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -397,23 +397,12 @@ public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
self::getContainer()->getByType(ReflectionProvider::class),
true,
),
$this->createSymfonyUsageProvider(),
];
}

private function createSymfonyUsageProvider(): SymfonyUsageProvider
{
/** @var SymfonyUsageProvider|null $cache */
static $cache = null;

if ($cache === null) {
$cache = new SymfonyUsageProvider(
new SymfonyUsageProvider(
new Configuration(['containerXmlPath' => __DIR__ . '/data/providers/symfony/services.xml']), // @phpstan-ignore phpstanApi.constructor
true,
);
}

return $cache;
__DIR__ . '/data/providers/symfony/',
),
];
}

private function createPhpStanContainerMock(): Container
Expand Down
4 changes: 4 additions & 0 deletions tests/Rule/data/providers/symfony.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,7 @@ public function create(): self {
return new self();
}
}

class Sftp {
const RETRY_LIMIT = 3; // used in yaml via !php/const
}
7 changes: 7 additions & 0 deletions tests/Rule/data/providers/symfony/config/sftp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
some_package:
sftp:
options:
host: "%foo_ftp_host%"
username: "%foo_ftp_username%"
password: "%foo_ftp_password%"
maxTries: !php/const Symfony\Sftp::RETRY_LIMIT
Loading