Skip to content

Commit 27d2535

Browse files
rvanvelzenondrejmirtes
authored andcommitted
Dectect reading class constants in PHPDoc
Amp-Thread-ID: https://ampcode.com/threads/T-01a19219-874b-7614-bea5-a5f416095107
1 parent 091c5a7 commit 27d2535

17 files changed

Lines changed: 440 additions & 138 deletions

‎src/Analyser/StmtHandler/ClassLikeHandler.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use PHPStan\Node\ClassPropertiesNode;
3333
use PHPStan\Node\ClassStatementsGatherer;
3434
use PHPStan\Node\InClassNode;
35+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
3536
use PHPStan\Reflection\ClassReflection;
3637
use PHPStan\Reflection\ClassReflectionFactory;
3738
use PHPStan\Reflection\ReflectionProvider;
@@ -64,6 +65,7 @@ public function __construct(
6465
private AttributesHandler $attributesHandler,
6566
#[AutowiredExtensions(of: ReadWritePropertiesExtension::class)]
6667
private ExtensionsCollection $readWritePropertiesExtensions,
68+
private PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
6769
)
6870
{
6971
}
@@ -116,7 +118,7 @@ public function processStmt(
116118
throw new ShouldNotHappenException();
117119
}
118120

119-
$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
121+
$classStatementsGatherer = new ClassStatementsGatherer($this->phpDocPrivateClassConstFetchFinder, $classReflection, $nodeCallback);
120122
// the class attributes are processed before the InClassNode emission, so
121123
// rules firing on it (ClassAttributesRule) read the attribute arguments
122124
// from the storage

‎src/Node/ClassStatementsGatherer.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Node\Property\PropertyAssign;
2020
use PHPStan\Node\Property\PropertyRead;
2121
use PHPStan\Node\Property\PropertyWrite;
22+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
2223
use PHPStan\Reflection\ClassReflection;
2324
use PHPStan\Reflection\MethodReflection;
2425
use PHPStan\ShouldNotHappenException;
@@ -70,6 +71,7 @@ final class ClassStatementsGatherer
7071
* @param callable(Node $node, Scope $scope): void $nodeCallback
7172
*/
7273
public function __construct(
74+
private PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
7375
private ClassReflection $classReflection,
7476
callable $nodeCallback,
7577
)
@@ -156,6 +158,14 @@ private function gatherNodes(Node $node, Scope $scope): void
156158
if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) {
157159
return;
158160
}
161+
162+
$docComment = $node->getDocComment();
163+
if ($docComment !== null) {
164+
foreach ($this->phpDocPrivateClassConstFetchFinder->findClassConstFetches($docComment->getText(), $scope) as $constFetch) {
165+
$this->constantFetches[] = $constFetch;
166+
}
167+
}
168+
159169
if ($node instanceof ClassPropertyNode) {
160170
$this->properties[] = $node;
161171
if ($node->isPromoted()) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\DependencyInjection\AutowiredService;
7+
use PHPStan\Node\Constant\ClassConstantFetch;
8+
use PHPStan\PhpDocParser\Ast\NodeTraverser;
9+
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Type\FileTypeMapper;
11+
12+
#[AutowiredService]
13+
final class PhpDocPrivateClassConstFetchFinder
14+
{
15+
16+
public function __construct(
17+
private FileTypeMapper $fileTypeMapper,
18+
)
19+
{
20+
}
21+
22+
/**
23+
* @return list<ClassConstantFetch>
24+
*/
25+
public function findClassConstFetches(string $phpDoc, Scope $scope): array
26+
{
27+
if (!$scope->isInClass()) {
28+
throw new ShouldNotHappenException();
29+
}
30+
31+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
32+
$scope->getFile(),
33+
$scope->getClassReflection()->getName(),
34+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
35+
null,
36+
$phpDoc,
37+
);
38+
39+
$visitor = new PhpDocPrivateClassConstFetchFinderVisitor($scope->getClassReflection(), $scope);
40+
41+
(new NodeTraverser([$visitor]))->traverse($resolvedPhpDoc->getPhpDocNodes());
42+
43+
return $visitor->classConstantFetches;
44+
}
45+
46+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc;
4+
5+
use Nette\Utils\Strings;
6+
use Override;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\Constant\ClassConstantFetch;
11+
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
12+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
13+
use PHPStan\Reflection\ClassReflection;
14+
use function preg_quote;
15+
use function str_replace;
16+
17+
final class PhpDocPrivateClassConstFetchFinderVisitor extends AbstractNodeVisitor
18+
{
19+
20+
/** @var list<ClassConstantFetch> */
21+
public array $classConstantFetches = [];
22+
23+
public function __construct(private ClassReflection $classReflection, private Scope $scope)
24+
{
25+
}
26+
27+
#[Override]
28+
public function enterNode(\PHPStan\PhpDocParser\Ast\Node $node)
29+
{
30+
if (!$node instanceof ConstFetchNode) {
31+
return null;
32+
}
33+
34+
if ($node->className !== 'self') {
35+
return null;
36+
}
37+
38+
$constantName = $node->name;
39+
if (Strings::contains($constantName, '*')) {
40+
// convert * into .*? and escape everything else so the constants can be matched against the pattern
41+
$pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D';
42+
foreach ($this->classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) {
43+
$classConstantName = $reflectionConstant->getName();
44+
if (Strings::match($classConstantName, $pattern) === null) {
45+
continue;
46+
}
47+
48+
$this->addClassConstantFetch($classConstantName);
49+
}
50+
} else {
51+
$this->addClassConstantFetch($constantName);
52+
}
53+
54+
return null;
55+
}
56+
57+
private function addClassConstantFetch(string $constantName): void
58+
{
59+
$this->classConstantFetches[] = new ClassConstantFetch(
60+
new Expr\ClassConstFetch(
61+
new Node\Name\FullyQualified($this->classReflection->getName()),
62+
$constantName,
63+
),
64+
$this->scope,
65+
);
66+
}
67+
68+
}

‎tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,24 @@ public function testBug12201(): void
132132
]);
133133
}
134134

135+
public function testBug6415(): void
136+
{
137+
$this->analyse([__DIR__ . '/data/bug-6415.php'], []);
138+
}
139+
140+
public function testBug7184(): void
141+
{
142+
$this->analyse([__DIR__ . '/data/bug-7184.php'], []);
143+
}
144+
145+
public function testBug9385(): void
146+
{
147+
$this->analyse([__DIR__ . '/data/bug-9385.php'], []);
148+
}
149+
150+
public function testPhpdocConstWildcard(): void
151+
{
152+
$this->analyse([__DIR__ . '/data/phpdoc-const-wildcard.php'], []);
153+
}
154+
135155
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6415;
4+
5+
class Foo
6+
{
7+
private const VALUES = [1,2,3];
8+
9+
/** @param value-of<self::VALUES> $value */
10+
public function bar(int $value): void {
11+
}
12+
13+
/** @param key-of<self::VALUES> $value */
14+
public function baz(int $value): void {
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace Bug7184;
4+
5+
class Character
6+
{
7+
8+
private const MAX_CHARACTER = 4; // ISSUE, this is being used on line 10.
9+
10+
/** @param int<1,self::MAX_CHARACTER> $int */
11+
public function __construct(
12+
public int $int,
13+
) {}
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1); // lint >= 7.4
2+
3+
namespace Bug9385;
4+
5+
final class WorkDayCalculator
6+
{
7+
private const DATE_FORMAT = 'Y-m-d';
8+
private const WORKDAY = 1;
9+
private const HOLIDAY = 0;
10+
11+
/** @var non-empty-array<non-empty-string, int<0, 1>> */
12+
private array $days = ['2023-06-01' => 1, '2023-06-02' => 1];
13+
14+
public function calculate(DateTimeInterface $start, DateTimeInterface $end): int
15+
{
16+
if (self::HOLIDAY === $this->getDay($start)) {
17+
throw new RuntimeException(
18+
sprintf('Start day should be a work one: %s', $start->format(self::DATE_FORMAT))
19+
);
20+
}
21+
22+
$days = 0;
23+
foreach (new DatePeriod($start, new DateInterval('P1D'), $end) as $day) {
24+
$days += $this->getDay($day);
25+
}
26+
return $days;
27+
}
28+
29+
/**
30+
* @return self::WORKDAY|self::HOLIDAY
31+
*/
32+
private function getDay(DateTimeInterface $day): int
33+
{
34+
return $this->days[$day->format(self::DATE_FORMAT)];
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1); // lint >= 7.4
2+
3+
namespace PhpdocConstWildcard;
4+
5+
final class WorkDayCalculator
6+
{
7+
private const DATE_FORMAT = 'Y-m-d';
8+
private const WORKDAY = 1;
9+
private const HOLIDAY = 0;
10+
11+
/** @var non-empty-array<non-empty-string, int<0, 1>> */
12+
private array $days = ['2023-06-01' => 1, '2023-06-02' => 1];
13+
14+
public function calculate(DateTimeInterface $start, DateTimeInterface $end): int
15+
{
16+
if (self::HOLIDAY === $this->getDay($start)) {
17+
throw new RuntimeException(
18+
sprintf('Start day should be a work one: %s', $start->format(self::DATE_FORMAT))
19+
);
20+
}
21+
22+
$days = 0;
23+
foreach (new DatePeriod($start, new DateInterval('P1D'), $end) as $day) {
24+
$days += $this->getDay($day);
25+
}
26+
return $days;
27+
}
28+
29+
/**
30+
* @return self::*DAY
31+
*/
32+
private function getDay(DateTimeInterface $day): int
33+
{
34+
return $this->days[$day->format(self::DATE_FORMAT)];
35+
}
36+
}

‎turbo-ext/src/ClassLikeHandler.cpp‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class ClassLikeHandler
214214
explicit ClassLikeHandler(zend_object *self) : self(self) {}
215215

216216
/* the constructor body: the promoted properties */
217-
void construct(zval *reflector, zval *classReflectionFactory, zval *calledMethodProcessor, zval *reflectionProvider, zval *attributesHandler, zval *readWritePropertiesExtensions)
217+
void construct(zval *reflector, zval *classReflectionFactory, zval *calledMethodProcessor, zval *reflectionProvider, zval *attributesHandler, zval *readWritePropertiesExtensions, zval *phpDocPrivateClassConstFetchFinder)
218218
{
219219
zv::ObjRef object(self);
220220
object.propAtWrite(slots::reflector, zv::Val::copyOf(zv::Ref(reflector)));
@@ -223,6 +223,7 @@ class ClassLikeHandler
223223
object.propAtWrite(slots::reflectionProvider, zv::Val::copyOf(zv::Ref(reflectionProvider)));
224224
object.propAtWrite(slots::attributesHandler, zv::Val::copyOf(zv::Ref(attributesHandler)));
225225
object.propAtWrite(slots::readWritePropertiesExtensions, zv::Val::copyOf(zv::Ref(readWritePropertiesExtensions)));
226+
object.propAtWrite(slots::phpDocPrivateClassConstFetchFinder, zv::Val::copyOf(zv::Ref(phpDocPrivateClassConstFetchFinder)));
226227
}
227228

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

324-
zv::Val classStatementsGatherer = pt_class_statements_gatherer_new(classReflection.raw(), nodeCallback);
325+
zv::Val classStatementsGatherer = pt_class_statements_gatherer_new(OBJ_PROP_NUM(self, slots::phpDocPrivateClassConstFetchFinder), classReflection.raw(), nodeCallback);
325326
if (UNEXPECTED(classStatementsGatherer.isUndef())) return zv::Val();
326327
// the class attributes are processed before the InClassNode emission, so
327328
// rules firing on it (ClassAttributesRule) read the attribute arguments
@@ -629,16 +630,17 @@ PT_MINIT_REGISTRATION(pt_register_class_like_handler)
629630
/* the real parameter class names: the DI container autowires the
630631
* service by reflecting the constructor */
631632
cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) {
632-
zval *reflector, *classReflectionFactory, *calledMethodProcessor, *reflectionProvider, *attributesHandler, *readWritePropertiesExtensions;
633-
ZEND_PARSE_PARAMETERS_START(6, 6)
633+
zval *reflector, *classReflectionFactory, *calledMethodProcessor, *reflectionProvider, *attributesHandler, *readWritePropertiesExtensions, *phpDocPrivateClassConstFetchFinder;
634+
ZEND_PARSE_PARAMETERS_START(7, 7)
634635
Z_PARAM_OBJECT(reflector)
635636
Z_PARAM_OBJECT(classReflectionFactory)
636637
Z_PARAM_OBJECT(calledMethodProcessor)
637638
Z_PARAM_OBJECT(reflectionProvider)
638639
Z_PARAM_OBJECT(attributesHandler)
639640
Z_PARAM_OBJECT(readWritePropertiesExtensions)
641+
Z_PARAM_OBJECT(phpDocPrivateClassConstFetchFinder)
640642
ZEND_PARSE_PARAMETERS_END();
641-
ClassLikeHandler(Z_OBJ_P(ZEND_THIS)).construct(reflector, classReflectionFactory, calledMethodProcessor, reflectionProvider, attributesHandler, readWritePropertiesExtensions);
643+
ClassLikeHandler(Z_OBJ_P(ZEND_THIS)).construct(reflector, classReflectionFactory, calledMethodProcessor, reflectionProvider, attributesHandler, readWritePropertiesExtensions, phpDocPrivateClassConstFetchFinder);
642644
});
643645

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

0 commit comments

Comments
 (0)