Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/Analyser/StmtHandler/ClassLikeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use PHPStan\Node\ClassPropertiesNode;
use PHPStan\Node\ClassStatementsGatherer;
use PHPStan\Node\InClassNode;
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ClassReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
Expand Down Expand Up @@ -64,6 +65,7 @@ public function __construct(
private AttributesHandler $attributesHandler,
#[AutowiredExtensions(of: ReadWritePropertiesExtension::class)]
private ExtensionsCollection $readWritePropertiesExtensions,
private PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
)
{
}
Expand Down Expand Up @@ -116,7 +118,7 @@ public function processStmt(
throw new ShouldNotHappenException();
}

$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
$classStatementsGatherer = new ClassStatementsGatherer($this->phpDocPrivateClassConstFetchFinder, $classReflection, $nodeCallback);
// the class attributes are processed before the InClassNode emission, so
// rules firing on it (ClassAttributesRule) read the attribute arguments
// from the storage
Expand Down
10 changes: 10 additions & 0 deletions src/Node/ClassStatementsGatherer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\Node\Property\PropertyAssign;
use PHPStan\Node\Property\PropertyRead;
use PHPStan\Node\Property\PropertyWrite;
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\ShouldNotHappenException;
Expand Down Expand Up @@ -70,6 +71,7 @@ final class ClassStatementsGatherer
* @param callable(Node $node, Scope $scope): void $nodeCallback
*/
public function __construct(
private PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
private ClassReflection $classReflection,
callable $nodeCallback,
)
Expand Down Expand Up @@ -156,6 +158,14 @@ private function gatherNodes(Node $node, Scope $scope): void
if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) {
return;
}

$docComment = $node->getDocComment();
if ($docComment !== null) {
foreach ($this->phpDocPrivateClassConstFetchFinder->findClassConstFetches($docComment->getText(), $scope) as $constFetch) {
$this->constantFetches[] = $constFetch;
}
}

if ($node instanceof ClassPropertyNode) {
$this->properties[] = $node;
if ($node->isPromoted()) {
Expand Down
46 changes: 46 additions & 0 deletions src/PhpDoc/PhpDocPrivateClassConstFetchFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDoc;

use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Constant\ClassConstantFetch;
use PHPStan\PhpDocParser\Ast\NodeTraverser;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\FileTypeMapper;

#[AutowiredService]
final class PhpDocPrivateClassConstFetchFinder
{

public function __construct(
private FileTypeMapper $fileTypeMapper,
)
{
}

/**
* @return list<ClassConstantFetch>
*/
public function findClassConstFetches(string $phpDoc, Scope $scope): array
{
if (!$scope->isInClass()) {
throw new ShouldNotHappenException();
}

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

$visitor = new PhpDocPrivateClassConstFetchFinderVisitor($scope->getClassReflection(), $scope);

(new NodeTraverser([$visitor]))->traverse($resolvedPhpDoc->getPhpDocNodes());

return $visitor->classConstantFetches;
}

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

namespace PHPStan\PhpDoc;

use Nette\Utils\Strings;
use Override;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Constant\ClassConstantFetch;
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\Reflection\ClassReflection;
use function preg_quote;
use function str_replace;

final class PhpDocPrivateClassConstFetchFinderVisitor extends AbstractNodeVisitor
{

/** @var list<ClassConstantFetch> */
public array $classConstantFetches = [];

public function __construct(private ClassReflection $classReflection, private Scope $scope)
{
}

#[Override]
public function enterNode(\PHPStan\PhpDocParser\Ast\Node $node)
{
if (!$node instanceof ConstFetchNode) {
return null;
}

if ($node->className !== 'self') {
return null;
}

$constantName = $node->name;
if (Strings::contains($constantName, '*')) {
// convert * into .*? and escape everything else so the constants can be matched against the pattern
$pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D';
foreach ($this->classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) {
$classConstantName = $reflectionConstant->getName();
if (Strings::match($classConstantName, $pattern) === null) {
continue;
}

$this->addClassConstantFetch($classConstantName);
}
} else {
$this->addClassConstantFetch($constantName);
}

return null;
}

private function addClassConstantFetch(string $constantName): void
{
$this->classConstantFetches[] = new ClassConstantFetch(
new Expr\ClassConstFetch(
new Node\Name\FullyQualified($this->classReflection->getName()),
$constantName,
),
$this->scope,
);
}

}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,24 @@ public function testBug12201(): void
]);
}

public function testBug6415(): void
{
$this->analyse([__DIR__ . '/data/bug-6415.php'], []);
}

public function testBug7184(): void
{
$this->analyse([__DIR__ . '/data/bug-7184.php'], []);
}

public function testBug9385(): void
{
$this->analyse([__DIR__ . '/data/bug-9385.php'], []);
}

public function testPhpdocConstWildcard(): void
{
$this->analyse([__DIR__ . '/data/phpdoc-const-wildcard.php'], []);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-6415.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace Bug6415;

class Foo
{
private const VALUES = [1,2,3];

/** @param value-of<self::VALUES> $value */
public function bar(int $value): void {
}

/** @param key-of<self::VALUES> $value */
public function baz(int $value): void {
}
}
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-7184.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug7184;

class Character
{

private const MAX_CHARACTER = 4; // ISSUE, this is being used on line 10.

/** @param int<1,self::MAX_CHARACTER> $int */
public function __construct(
public int $int,
) {}
}
36 changes: 36 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-9385.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1); // lint >= 7.4

namespace Bug9385;

final class WorkDayCalculator
{
private const DATE_FORMAT = 'Y-m-d';
private const WORKDAY = 1;
private const HOLIDAY = 0;

/** @var non-empty-array<non-empty-string, int<0, 1>> */
private array $days = ['2023-06-01' => 1, '2023-06-02' => 1];

public function calculate(DateTimeInterface $start, DateTimeInterface $end): int
{
if (self::HOLIDAY === $this->getDay($start)) {
throw new RuntimeException(
sprintf('Start day should be a work one: %s', $start->format(self::DATE_FORMAT))
);
}

$days = 0;
foreach (new DatePeriod($start, new DateInterval('P1D'), $end) as $day) {
$days += $this->getDay($day);
}
return $days;
}

/**
* @return self::WORKDAY|self::HOLIDAY
*/
private function getDay(DateTimeInterface $day): int
{
return $this->days[$day->format(self::DATE_FORMAT)];
}
}
36 changes: 36 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/phpdoc-const-wildcard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1); // lint >= 7.4

namespace PhpdocConstWildcard;

final class WorkDayCalculator
{
private const DATE_FORMAT = 'Y-m-d';
private const WORKDAY = 1;
private const HOLIDAY = 0;

/** @var non-empty-array<non-empty-string, int<0, 1>> */
private array $days = ['2023-06-01' => 1, '2023-06-02' => 1];

public function calculate(DateTimeInterface $start, DateTimeInterface $end): int
{
if (self::HOLIDAY === $this->getDay($start)) {
throw new RuntimeException(
sprintf('Start day should be a work one: %s', $start->format(self::DATE_FORMAT))
);
}

$days = 0;
foreach (new DatePeriod($start, new DateInterval('P1D'), $end) as $day) {
$days += $this->getDay($day);
}
return $days;
}

/**
* @return self::*DAY
*/
private function getDay(DateTimeInterface $day): int
{
return $this->days[$day->format(self::DATE_FORMAT)];
}
}
12 changes: 7 additions & 5 deletions turbo-ext/src/ClassLikeHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ class ClassLikeHandler
explicit ClassLikeHandler(zend_object *self) : self(self) {}

/* the constructor body: the promoted properties */
void construct(zval *reflector, zval *classReflectionFactory, zval *calledMethodProcessor, zval *reflectionProvider, zval *attributesHandler, zval *readWritePropertiesExtensions)
void construct(zval *reflector, zval *classReflectionFactory, zval *calledMethodProcessor, zval *reflectionProvider, zval *attributesHandler, zval *readWritePropertiesExtensions, zval *phpDocPrivateClassConstFetchFinder)
{
zv::ObjRef object(self);
object.propAtWrite(slots::reflector, zv::Val::copyOf(zv::Ref(reflector)));
Expand All @@ -223,6 +223,7 @@ class ClassLikeHandler
object.propAtWrite(slots::reflectionProvider, zv::Val::copyOf(zv::Ref(reflectionProvider)));
object.propAtWrite(slots::attributesHandler, zv::Val::copyOf(zv::Ref(attributesHandler)));
object.propAtWrite(slots::readWritePropertiesExtensions, zv::Val::copyOf(zv::Ref(readWritePropertiesExtensions)));
object.propAtWrite(slots::phpDocPrivateClassConstFetchFinder, zv::Val::copyOf(zv::Ref(phpDocPrivateClassConstFetchFinder)));
}

/* Mirrors supports(); false = pending exception */
Expand Down Expand Up @@ -321,7 +322,7 @@ class ClassLikeHandler
zv::Val classScope = pt_mutating_scope_enter_class(Z_OBJ_P(resultScope), classReflection.raw());
if (UNEXPECTED(classScope.isUndef())) return zv::Val();

zv::Val classStatementsGatherer = pt_class_statements_gatherer_new(classReflection.raw(), nodeCallback);
zv::Val classStatementsGatherer = pt_class_statements_gatherer_new(OBJ_PROP_NUM(self, slots::phpDocPrivateClassConstFetchFinder), classReflection.raw(), nodeCallback);
if (UNEXPECTED(classStatementsGatherer.isUndef())) return zv::Val();
// the class attributes are processed before the InClassNode emission, so
// rules firing on it (ClassAttributesRule) read the attribute arguments
Expand Down Expand Up @@ -629,16 +630,17 @@ PT_MINIT_REGISTRATION(pt_register_class_like_handler)
/* the real parameter class names: the DI container autowires the
* service by reflecting the constructor */
cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) {
zval *reflector, *classReflectionFactory, *calledMethodProcessor, *reflectionProvider, *attributesHandler, *readWritePropertiesExtensions;
ZEND_PARSE_PARAMETERS_START(6, 6)
zval *reflector, *classReflectionFactory, *calledMethodProcessor, *reflectionProvider, *attributesHandler, *readWritePropertiesExtensions, *phpDocPrivateClassConstFetchFinder;
ZEND_PARSE_PARAMETERS_START(7, 7)
Z_PARAM_OBJECT(reflector)
Z_PARAM_OBJECT(classReflectionFactory)
Z_PARAM_OBJECT(calledMethodProcessor)
Z_PARAM_OBJECT(reflectionProvider)
Z_PARAM_OBJECT(attributesHandler)
Z_PARAM_OBJECT(readWritePropertiesExtensions)
Z_PARAM_OBJECT(phpDocPrivateClassConstFetchFinder)
ZEND_PARSE_PARAMETERS_END();
ClassLikeHandler(Z_OBJ_P(ZEND_THIS)).construct(reflector, classReflectionFactory, calledMethodProcessor, reflectionProvider, attributesHandler, readWritePropertiesExtensions);
ClassLikeHandler(Z_OBJ_P(ZEND_THIS)).construct(reflector, classReflectionFactory, calledMethodProcessor, reflectionProvider, attributesHandler, readWritePropertiesExtensions, phpDocPrivateClassConstFetchFinder);
});

cls.method<&ClassLikeHandler::supports, zp::Obj>(sigs::supports);
Expand Down
Loading
Loading