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
96 changes: 96 additions & 0 deletions src/Collector/ConstantFetchCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\Analyser\NameScope;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\PhpDocParser\Ast\NodeTraverser as PhpDocNodeTraverser;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeUtils;
use ShipMonk\PHPStan\DeadCode\Cache\UsageCacheStorage;
Expand All @@ -21,11 +27,13 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use ShipMonk\PHPStan\DeadCode\Visitor\PhpDocConstFetchCollectingVisitor;
use function array_map;
use function count;
use function current;
use function explode;
use function str_contains;
use function strtolower;

/**
* @implements Collector<Node, list<string>>
Expand All @@ -41,6 +49,7 @@ final class ConstantFetchCollector implements Collector
public function __construct(
UsageCacheStorage $usageCacheStorage,
private readonly ReflectionProvider $reflectionProvider,
private readonly FileTypeMapper $fileTypeMapper,
private readonly array $memberUsageExcluders,
)
{
Expand Down Expand Up @@ -68,6 +77,8 @@ public function processNode(
$this->registerFunctionCall($node, $scope);
}

$this->registerPhpDocConstantFetches($node, $scope);

return $this->tryFlushBuffer($node, $scope);
}

Expand Down Expand Up @@ -124,6 +135,91 @@ private function registerFunctionCall(
}
}

/**
* PHPStan eagerly resolves e.g. int<1, self::MAX> down to a literal int<1, 100>, so the reference
* to the constant survives only in the raw PhpDoc type AST. We walk it for ConstFetchNode occurrences.
*/
private function registerPhpDocConstantFetches(
Node $node,
Scope $scope,
): void
{
$docComment = $node->getDocComment();

if ($docComment === null || !str_contains($docComment->getText(), '::')) {
return;
}

if ($node instanceof ClassMethod || $node instanceof Function_) {
$functionName = $node->name->toString();
} else {
$function = $scope->getFunction();
$functionName = $function !== null ? $function->getName() : null;
}

$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$functionName,
$docComment->getText(),
);

$nameScope = $resolvedPhpDoc->getNullableNameScope();

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

$visitor = new PhpDocConstFetchCollectingVisitor();
(new PhpDocNodeTraverser([$visitor]))->traverse($resolvedPhpDoc->getPhpDocNodes());

foreach ($visitor->getConstFetchNodes() as $constFetchNode) {
if (str_contains($constFetchNode->name, '*')) {
continue; // constant mask (e.g. self::SIZE_*)
}

$ownerClassName = $this->resolvePhpDocConstFetchOwner($constFetchNode->className, $nameScope);

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

$possibleDescendant = strtolower($constFetchNode->className) === 'static';

foreach ($this->getDeclaringTypesWithConstant(new ObjectType($ownerClassName), $constFetchNode->name, $possibleDescendant) as $constantRef) {
$usage = new ClassConstantUsage(UsageOrigin::createRegular($node, $scope), $constantRef);
$this->registerUsage($usage, $node, $scope);
}
}
}

private function resolvePhpDocConstFetchOwner(
string $className,
NameScope $nameScope,
): ?string
{
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' || $lowerClassName === 'static') {
return $nameScope->getClassName();
}

if ($lowerClassName === 'parent') {
$currentClassName = $nameScope->getClassName();

if ($currentClassName === null || !$this->reflectionProvider->hasClass($currentClassName)) {
return null;
}

$parent = $this->reflectionProvider->getClass($currentClassName)->getParentClass();

return $parent !== null ? $parent->getName() : null;
}

return $nameScope->resolveStringName($className);
}

private function registerFetch(
ClassConstFetch $node,
Scope $scope,
Expand Down
34 changes: 34 additions & 0 deletions src/Visitor/PhpDocConstFetchCollectingVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Visitor;

use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;

final class PhpDocConstFetchCollectingVisitor extends AbstractNodeVisitor
{

/**
* @var list<ConstFetchNode>
*/
private array $constFetchNodes = [];

public function enterNode(Node $node): ?Node
{
if ($node instanceof ConstFetchNode && $node->className !== '') {
$this->constFetchNodes[] = $node;
}

return null;
}

/**
* @return list<ConstFetchNode>
*/
public function getConstFetchNodes(): array
{
return $this->constFetchNodes;
}

}
2 changes: 2 additions & 0 deletions tests/AllServicesInConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveClassMemberVisitor;
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveDeadCodeTransformer;
use ShipMonk\PHPStan\DeadCode\Visitor\PhpDocConstFetchCollectingVisitor;
use ShipMonk\PHPStan\DeadCode\Visitor\PropertyHookBackingValueVisitor;
use function array_merge;
use function class_exists;
Expand Down Expand Up @@ -63,6 +64,7 @@ public function test(): void
RemoveDeadCodeTransformer::class,
RemoveClassMemberVisitor::class,
PropertyHookBackingValueVisitor::class,
PhpDocConstFetchCollectingVisitor::class,
];

/** @var DirectoryIterator $file */
Expand Down
4 changes: 3 additions & 1 deletion tests/Rule/DeadCodeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Testing\RuleTestCase as OriginalRuleTestCase;
use PHPStan\Type\FileTypeMapper;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -172,7 +173,7 @@ protected function getCollectors(): array
),
new ClassDefinitionCollector($reflectionProvider),
new MethodCallCollector($usageCacheStorage, $this->getMemberUsageExcluders()),
new ConstantFetchCollector($usageCacheStorage, $reflectionProvider, $this->getMemberUsageExcluders()),
new ConstantFetchCollector($usageCacheStorage, $reflectionProvider, self::getContainer()->getByType(FileTypeMapper::class), $this->getMemberUsageExcluders()),
new PropertyAccessCollector($usageCacheStorage, $reflectionProvider, [__DIR__ . '/data/'], $this->getMemberUsageExcluders()),
];
}
Expand Down Expand Up @@ -1043,6 +1044,7 @@ public static function provideFiles(): Traversable
yield 'const-descendant-4' => [__DIR__ . '/data/constants/descendant-4.php'];
yield 'const-dynamic' => [__DIR__ . '/data/constants/dynamic.php'];
yield 'const-expr' => [__DIR__ . '/data/constants/expr.php'];
yield 'const-phpdoc' => [__DIR__ . '/data/constants/phpdoc.php'];
yield 'const-magic' => [__DIR__ . '/data/constants/magic.php'];
yield 'const-mixed' => [__DIR__ . '/data/constants/mixed/tracked.php'];
yield 'const-soft-final' => [__DIR__ . '/data/constants/soft-final.php'];
Expand Down
89 changes: 89 additions & 0 deletions tests/Rule/data/constants/phpdoc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php declare(strict_types = 1);

namespace DeadConstPhpDoc;

use DeadConstPhpDoc\Limits as AliasedLimits;

final class PaginationInput
{

public const MAX_PAGE_SIZE = 100;
public const UNUSED = 1; // error: Unused DeadConstPhpDoc\PaginationInput::UNUSED

/**
* @param int<1, self::MAX_PAGE_SIZE> $pageSize
*/
public static function create(int $pageSize): void
{
echo $pageSize;
}

}

final class Limits
{

public const MAX_ITEMS = 1000;
public const MIN_ITEMS = 1;
public const VIA_ALIAS = 5;

}

final class BulkInput
{

/**
* @param int<0, Limits::MAX_ITEMS> $count
* @param array<Limits::MIN_ITEMS, string> $names
* @return AliasedLimits::VIA_ALIAS|null
*/
public static function process(int $count, array $names): ?int
{
echo $count;
echo count($names);

return null;
}

}

abstract class Base
{

public const BASE_MAX = 50;

}

final class Child extends Base
{

/**
* @param int<1, parent::BASE_MAX> $value
*/
public static function handle(int $value): void
{
echo $value;
}

}

final class Sizes
{

public const SIZE_SMALL = 1; // error: Unused DeadConstPhpDoc\Sizes::SIZE_SMALL
public const SIZE_LARGE = 2; // error: Unused DeadConstPhpDoc\Sizes::SIZE_LARGE

/**
* @param self::SIZE_* $size
*/
public static function pick(int $size): void
{
echo $size;
}

}

PaginationInput::create(5);
BulkInput::process(1, []);
Child::handle(1);
Sizes::pick(1);
Loading