Skip to content

Commit

Permalink
Detect dead class constants
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Nov 13, 2024
1 parent e3c3729 commit 8649a07
Show file tree
Hide file tree
Showing 17 changed files with 427 additions and 84 deletions.
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.Commenting.Todo" />
<!-- rule ref="Generic.Commenting.Todo" -->
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.EndFileNewline"/>
<rule ref="Generic.Files.InlineHTML"/>
Expand Down
4 changes: 4 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ services:
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
tags:
- phpstan.collector
-
class: ShipMonk\PHPStan\DeadCode\Collector\ConstantFetchCollector
tags:
- phpstan.collector
-
class: ShipMonk\PHPStan\DeadCode\Collector\EntrypointCollector
tags:
Expand Down
13 changes: 13 additions & 0 deletions src/Collector/ClassDefinitionCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @implements Collector<ClassLike, array{
* kind: string,
* name: string,
* constants: array<string, array{line: int}>,
* methods: array<string, array{line: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
Expand All @@ -42,6 +43,7 @@ public function getNodeType(): string
* @return array{
* kind: string,
* name: string,
* constants: array<string, array{line: int}>,
* methods: array<string, array{line: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
Expand Down Expand Up @@ -70,10 +72,21 @@ public function processNode(
];
}

$constants = [];

foreach ($node->getConstants() as $constant) {
foreach ($constant->consts as $const) {
$constants[$const->name->toString()] = [
'line' => $const->getStartLine(),
];
}
}

return [
'kind' => $kind,
'name' => $typeName,
'methods' => $methods,
'constants' => $constants,
'parents' => $this->getParents($node),
'traits' => $this->getTraits($node),
'interfaces' => $this->getInterfaces($node),
Expand Down
103 changes: 103 additions & 0 deletions src/Collector/ConstantFetchCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Collector;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\ClassMethodsNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantStringType;
use ShipMonk\PHPStan\DeadCode\Crate\ClassConstantFetch;
use ShipMonk\PHPStan\DeadCode\Crate\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Crate\ClassMethodRef;
use function array_map;

/**
* @implements Collector<Node, list<string>>
*/
class ConstantFetchCollector implements Collector
{

/**
* @var list<ClassConstantFetch>
*/
private array $accessBuffer = [];

public function getNodeType(): string
{
return Node::class;
}

/**
* @return non-empty-list<string>|null
*/
public function processNode(
Node $node,
Scope $scope
): ?array
{
if ($node instanceof ClassConstFetch) {
$this->registerFetch($node, $scope);
}

if (!$scope->isInClass() || $node instanceof ClassMethodsNode) { // @phpstan-ignore-line ignore BC promise
$data = $this->accessBuffer;
$this->accessBuffer = [];

// collect data once per class to save memory & resultCache size
return $data === []
? null
: array_map(
static fn (ClassConstantFetch $fetch): string => $fetch->toString(),
$data,
);
}

return null;
}

private function registerFetch(ClassConstFetch $node, Scope $scope): void
{
$ownerType = $node->class instanceof Name
? $scope->resolveTypeByName($node->class)
: $scope->getType($node->class);

$constantNames = $node->name instanceof Expr
? array_map(static fn (ConstantStringType $string): string => $string->getValue(), $scope->getType($node->name)->getConstantStrings())
: [$node->name->toString()];

foreach ($ownerType->getObjectClassReflections() as $classReflection) {
foreach ($constantNames as $constantName) {
if ($classReflection->hasConstant($constantName)) {
$className = $classReflection->getConstant($constantName)->getDeclaringClass()->getName();

} else { // call of unknown const (might be present on children)
$className = $classReflection->getName(); // TODO untested yet
}

$this->accessBuffer[] = new ClassConstantFetch(
$this->getCaller($scope),
new ClassConstantRef($className, $constantName),
);
}
}
}

private function getCaller(Scope $scope): ?ClassMethodRef
{
if (!$scope->isInClass()) {
return null;
}

if (!$scope->getFunction() instanceof MethodReflection) {
return null;
}

return new ClassMethodRef($scope->getClassReflection()->getName(), $scope->getFunction()->getName());
}

}
8 changes: 4 additions & 4 deletions src/Collector/EntrypointCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\InClassNode;
use ShipMonk\PHPStan\DeadCode\Crate\Call;
use ShipMonk\PHPStan\DeadCode\Crate\Method;
use ShipMonk\PHPStan\DeadCode\Crate\ClassMethodCall;
use ShipMonk\PHPStan\DeadCode\Crate\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Provider\MethodEntrypointProvider;

/**
Expand Down Expand Up @@ -49,9 +49,9 @@ public function processNode(

foreach ($this->entrypointProviders as $entrypointProvider) {
foreach ($entrypointProvider->getEntrypoints($node->getClassReflection()) as $entrypointMethod) {
$call = new Call(
$call = new ClassMethodCall(
null,
new Method($entrypointMethod->getDeclaringClass()->getName(), $entrypointMethod->getName()),
new ClassMethodRef($entrypointMethod->getDeclaringClass()->getName(), $entrypointMethod->getName()),
false,
);
$entrypoints[] = $call->toString();
Expand Down
32 changes: 16 additions & 16 deletions src/Collector/MethodCallCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use ShipMonk\PHPStan\DeadCode\Crate\Call;
use ShipMonk\PHPStan\DeadCode\Crate\Method;
use ShipMonk\PHPStan\DeadCode\Crate\ClassMethodCall;
use ShipMonk\PHPStan\DeadCode\Crate\ClassMethodRef;
use function array_map;

/**
Expand All @@ -34,7 +34,7 @@ class MethodCallCollector implements Collector
{

/**
* @var list<Call>
* @var list<ClassMethodCall>
*/
private array $callsBuffer = [];

Expand Down Expand Up @@ -94,7 +94,7 @@ public function processNode(
return $data === []
? null
: array_map(
static fn (Call $call): string => $call->toString(),
static fn (ClassMethodCall $call): string => $call->toString(),
$data,
);
}
Expand Down Expand Up @@ -131,9 +131,9 @@ private function registerMethodCall(

foreach ($methodNames as $methodName) {
foreach ($this->getDeclaringTypesWithMethod($scope, $callerType, $methodName, TrinaryLogic::createNo()) as $className) {
$this->callsBuffer[] = new Call(
$this->callsBuffer[] = new ClassMethodCall(
$this->getCaller($scope),
new Method($className, $methodName),
new ClassMethodRef($className, $methodName),
$possibleDescendantCall,
);
}
Expand All @@ -158,9 +158,9 @@ private function registerStaticCall(

foreach ($methodNames as $methodName) {
foreach ($this->getDeclaringTypesWithMethod($scope, $callerType, $methodName, TrinaryLogic::createYes()) as $className) {
$this->callsBuffer[] = new Call(
$this->callsBuffer[] = new ClassMethodCall(
$this->getCaller($scope),
new Method($className, $methodName),
new ClassMethodRef($className, $methodName),
$possibleDescendantCall,
);
}
Expand All @@ -184,9 +184,9 @@ private function registerArrayCallable(
$possibleDescendantCall = !$caller->isClassString()->yes();

foreach ($this->getDeclaringTypesWithMethod($scope, $caller, $methodName, TrinaryLogic::createMaybe()) as $className) {
$this->callsBuffer[] = new Call(
$this->callsBuffer[] = new ClassMethodCall(
$this->getCaller($scope),
new Method($className, $methodName),
new ClassMethodRef($className, $methodName),
$possibleDescendantCall,
);
}
Expand All @@ -197,9 +197,9 @@ private function registerArrayCallable(

private function registerAttribute(Attribute $node, Scope $scope): void
{
$this->callsBuffer[] = new Call(
$this->callsBuffer[] = new ClassMethodCall(
null,
new Method($scope->resolveName($node->name), '__construct'),
new ClassMethodRef($scope->resolveName($node->name), '__construct'),
false,
);
}
Expand All @@ -210,9 +210,9 @@ private function registerClone(Clone_ $node, Scope $scope): void
$callerType = $scope->getType($node->expr);

foreach ($this->getDeclaringTypesWithMethod($scope, $callerType, $methodName, TrinaryLogic::createNo()) as $className) {
$this->callsBuffer[] = new Call(
$this->callsBuffer[] = new ClassMethodCall(
$this->getCaller($scope),
new Method($className, $methodName),
new ClassMethodRef($className, $methodName),
true,
);
}
Expand Down Expand Up @@ -278,7 +278,7 @@ private function getDeclaringTypesWithMethod(
return $result;
}

private function getCaller(Scope $scope): ?Method
private function getCaller(Scope $scope): ?ClassMethodRef
{
if (!$scope->isInClass()) {
return null;
Expand All @@ -288,7 +288,7 @@ private function getCaller(Scope $scope): ?Method
return null;
}

return new Method($scope->getClassReflection()->getName(), $scope->getFunction()->getName());
return new ClassMethodRef($scope->getClassReflection()->getName(), $scope->getFunction()->getName());
}

}
45 changes: 45 additions & 0 deletions src/Crate/ClassConstantFetch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Crate;

use LogicException;
use function serialize;
use function unserialize;

/**
* @immutable
*/
class ClassConstantFetch
{

public ?ClassMethodRef $origin; // TODO always known class, introduce new type?
public ClassConstantRef $fetch;

public function __construct(
?ClassMethodRef $origin,
ClassConstantRef $fetch
)
{
$this->origin = $origin;
$this->fetch = $fetch;
}

public function toString(): string
{
return serialize($this);
}

public static function fromString(string $callKey): self
{
$result = unserialize($callKey, ['allowed_classes' => [self::class, ClassMethodRef::class, ClassConstantRef::class]]);

if (!$result instanceof self) {
$self = self::class;
throw new LogicException("Invalid string for $self deserialization: $callKey");
}

return $result;
}

}
19 changes: 19 additions & 0 deletions src/Crate/ClassConstantRef.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Crate;

/**
* @immutable
*/
class ClassConstantRef extends ClassMemberRef
{

public function __construct(
?string $className,
string $constantName
)
{
parent::__construct($className, $constantName, ClassMemberRef::TYPE_CONSTANT);
}

}
Loading

0 comments on commit 8649a07

Please sign in to comment.