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
46 changes: 46 additions & 0 deletions src/Rules/DeadCode/UnusedPrivateConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
use PHPStan\DependencyInjection\ExtensionsCollection;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\ClassConstantsNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use function array_key_exists;
use function in_array;
use function sprintf;

/**
Expand Down Expand Up @@ -45,6 +48,20 @@ public function processNode(Node $node, Scope $scope): array
$classReflection = $node->getClassReflection();
$classType = new ObjectType($classReflection->getName(), classReflection: $classReflection);

// A gathered ClassConst that is not one of the class' own statements comes from an
// inlined trait body, which NodeScopeResolver only traverses for analysed files.
// Its presence therefore proves the trait's own fetches are visible.
$constantNamesDeclaredInTraitBody = [];
foreach ($node->getConstants() as $constant) {
if (in_array($constant, $node->getClass()->stmts, true)) {
continue;
}

foreach ($constant->consts as $const) {
$constantNamesDeclaredInTraitBody[$const->name->toString()] = true;
}
}

$constants = [];
foreach ($node->getConstants() as $constant) {
if (!$constant->isPrivate()) {
Expand All @@ -54,6 +71,13 @@ public function processNode(Node $node, Scope $scope): array
foreach ($constant->consts as $const) {
$constantName = $const->name->toString();

if (
!array_key_exists($constantName, $constantNamesDeclaredInTraitBody)
&& $this->isRedeclaringPrivateTraitConstant($classReflection, $constantName)
) {
continue;
}

$constantReflection = $classReflection->getConstant($constantName);
foreach ($this->extensions->getAll() as $extension) {
if ($extension->isAlwaysUsed($constantReflection)) {
Expand Down Expand Up @@ -113,4 +137,26 @@ public function processNode(Node $node, Scope $scope): array
return $errors;
}

/**
* A private constant redeclared from a used trait is the very constant the trait's
* own methods fetch. Callers must only rely on this when the trait's body was not
* traversed, otherwise those fetches are visible and no guessing is needed.
*/
private function isRedeclaringPrivateTraitConstant(ClassReflection $classReflection, string $constantName): bool
{
foreach ($classReflection->getTraits() as $trait) {
if (!$trait->hasConstant($constantName)) {
continue;
}

if (!$trait->getConstant($constantName)->isPrivate()) {
continue;
}

return true;
}

return false;
}

}
27 changes: 27 additions & 0 deletions src/Rules/DeadCode/UnusedPrivateMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\DependencyInjection\ExtensionsCollection;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\ClassMethodsNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Rules\Methods\AlwaysUsedMethodExtension;
use PHPStan\Rules\Rule;
Expand Down Expand Up @@ -70,6 +71,10 @@ public function processNode(Node $node, Scope $scope): array
continue;
}

if ($this->isOverridingPrivateTraitMethod($classReflection, $methodName)) {
Comment thread
staabm marked this conversation as resolved.
continue;
}

$methodReflection = $classReflection->getNativeMethod($methodName);
foreach ($this->extensions->getAll() as $extension) {
if ($extension->isAlwaysUsed($methodReflection)) {
Expand Down Expand Up @@ -203,4 +208,26 @@ public function processNode(Node $node, Scope $scope): array
return $errors;
}

/**
* A private method overriding a private method of a used trait is called from
* the trait's own methods. Those call sites are invisible when the trait is not
* part of the analysed files.
*/
private function isOverridingPrivateTraitMethod(ClassReflection $classReflection, string $methodName): bool
{
foreach ($classReflection->getTraits() as $trait) {
if (!$trait->hasNativeMethod($methodName)) {
continue;
}

if (!$trait->getNativeMethod($methodName)->isPrivate()) {
continue;
}

return true;
}

return false;
}

}
41 changes: 41 additions & 0 deletions src/Rules/DeadCode/UnusedPrivatePropertyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Node\ClassPropertiesNode;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Node\Property\PropertyRead;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
Expand Down Expand Up @@ -64,6 +65,18 @@ public function processNode(Node $node, Scope $scope): array
}
$classReflection = $node->getClassReflection();
$classType = new ObjectType($classReflection->getName(), classReflection: $classReflection);
// A property node declared in a trait only reaches us when NodeScopeResolver traversed
// that trait's body, which it does for analysed files only. Its presence therefore
// proves the trait's own usages are visible.
$propertyNamesDeclaredInTraitBody = [];
foreach ($node->getProperties() as $property) {
if (!$property->isDeclaredInTrait()) {
continue;
}

$propertyNamesDeclaredInTraitBody[$property->getName()] = true;
}

$properties = [];
foreach ($node->getProperties() as $property) {
if (!$property->isPrivate()) {
Expand All @@ -72,6 +85,12 @@ public function processNode(Node $node, Scope $scope): array
if ($property->isDeclaredInTrait()) {
continue;
}
if (
!array_key_exists($property->getName(), $propertyNamesDeclaredInTraitBody)
&& $this->isRedeclaringPrivateTraitProperty($classReflection, $property->getName())
) {
continue;
}

$alwaysRead = !$property->isReadable();
$alwaysWritten = !$property->isWritable();
Expand Down Expand Up @@ -293,6 +312,28 @@ public function processNode(Node $node, Scope $scope): array
return $errors;
}

/**
* A private property redeclared from a used trait is the very property the trait's
* own methods read and write. Callers must only rely on this when the trait's body was
* not traversed, otherwise those usages are visible and no guessing is needed.
*/
private function isRedeclaringPrivateTraitProperty(ClassReflection $classReflection, string $propertyName): bool
{
foreach ($classReflection->getTraits() as $trait) {
if (!$trait->hasNativeProperty($propertyName)) {
continue;
}

if (!$trait->getNativeProperty($propertyName)->isPrivate()) {
continue;
}

return true;
}

return false;
}

private function isPropertySelfWrite(
Scope $usageScope,
string $propertyName,
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,31 @@ public function testBug14880(): void
$this->analyse([__DIR__ . '/data/bug-14880-constant.php'], []);
}

#[RequiresPhp('>= 8.2.0')]
public function testBug12201(): void
{
$this->analyse([__DIR__ . '/data/bug-12201-constant.php'], [
[
'Constant Bug12201Constant\\AnotherKernel::UNUSED is unused.',
23,
'See: https://phpstan.org/developing-extensions/always-used-class-constants',
],
[
'Constant Bug12201Constant\\UsesNeverFetchedTrait::NEVER_FETCHED is unused.',
30,
'See: https://phpstan.org/developing-extensions/always-used-class-constants',
],
[
'Constant Bug12201Constant\\ChildKernel::ALLOWED_ENVS is unused.',
52,
'See: https://phpstan.org/developing-extensions/always-used-class-constants',
],
[
'Constant Bug12201Constant\\RedeclaresAnalysedTrait::REDECLARED is unused.',
69,
'See: https://phpstan.org/developing-extensions/always-used-class-constants',
],
]);
}

}
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ public function testBug11802(): void
]);
}

public function testBug12201(): void
{
$this->analyse([__DIR__ . '/data/bug-12201.php'], [
[
'Method Bug12201\AnotherKernel::doNothing() is unused.',
24,
],
]);
}

public function testBug14880(): void
{
$this->analyse([__DIR__ . '/data/bug-14880.php'], []);
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,28 @@ public function testBug14880(): void
$this->analyse([__DIR__ . '/data/bug-14880-property.php'], []);
}

public function testBug12201(): void
{
$this->alwaysWrittenTags = [];
$this->alwaysReadTags = [];

$this->analyse([__DIR__ . '/data/bug-12201-property.php'], [
[
'Property Bug12201Property\\AnotherKernel::$unused is never read, only written.',
22,
'See: https://phpstan.org/developing-extensions/always-read-written-properties',
],
[
'Property Bug12201Property\\ChildKernel::$allowedEnvs is never read, only written.',
48,
'See: https://phpstan.org/developing-extensions/always-read-written-properties',
],
[
'Property Bug12201Property\\RedeclaresAnalysedTrait::$redeclared is never read, only written.',
82,
'See: https://phpstan.org/developing-extensions/always-read-written-properties',
],
]);
}

}
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-12201-constant-traits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug12201Constant;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a comment what this file is supposed to be and why itself is not analyzed


// Stands for a dependency shipped in vendor/: bug-12201-constant.php uses these traits, but
// this file is never passed to analyse(), so PHPStan does not traverse the trait bodies.

trait KernelTrait
{

private const ALLOWED_ENVS = ['prod', 'dev', 'test'];

/**
* @return list<string>
*/
protected function getAllowedEnvs(): array
{
return self::ALLOWED_ENVS;
}

}

trait MicroKernelTrait
{

use KernelTrait;

}
71 changes: 71 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-12201-constant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug12201Constant;

// The traits live in bug-12201-constant-traits.php which is not analysed on purpose:
// it stands for a dependency living outside of the analysed paths.
class AppKernel
{

use MicroKernelTrait;

private const ALLOWED_ENVS = ['prod', 'dev', 'test'];

}

class AnotherKernel
{

use MicroKernelTrait;

private const UNUSED = 'unused';

}

trait NeverFetchedTrait
{

private const NEVER_FETCHED = 'never fetched';

}

class UsesNeverFetchedTrait
{

use NeverFetchedTrait;

}

class ParentKernel
{

use MicroKernelTrait;

}

// The trait is used by the parent, so it cannot reach this separate private slot.
class ChildKernel extends ParentKernel
{

private const ALLOWED_ENVS = ['prod', 'dev', 'test'];

}

trait RedeclaredTrait
{

private const REDECLARED = 'redeclared';

}

// This trait is analysed, so its fetches are visible and nothing needs to be assumed.
class RedeclaresAnalysedTrait
{

use RedeclaredTrait;

private const REDECLARED = 'redeclared';

}
Loading
Loading