Skip to content

Commit 493aba4

Browse files
committed
Dectect reading class constants in PHPDoc
1 parent ea5ce9c commit 493aba4

13 files changed

Lines changed: 270 additions & 1 deletion

‎conf/config.neon‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,9 @@ services:
592592
-
593593
class: PHPStan\PhpDoc\PhpDocStringResolver
594594

595+
-
596+
class: PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder
597+
595598
-
596599
class: PHPStan\PhpDoc\ConstExprNodeResolver
597600

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
use PHPStan\Parser\Parser;
106106
use PHPStan\Php\PhpVersion;
107107
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
108+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
108109
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
109110
use PHPStan\PhpDoc\StubPhpDocProvider;
110111
use PHPStan\PhpDoc\Tag\VarTag;
@@ -204,6 +205,7 @@ public function __construct(
204205
private readonly TypeSpecifier $typeSpecifier,
205206
private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
206207
private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
208+
private readonly PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
207209
private readonly bool $polluteScopeWithLoopInitialAssignments,
208210
private readonly bool $polluteScopeWithAlwaysIterableForeach,
209211
private readonly array $earlyTerminatingMethodCalls,
@@ -675,7 +677,7 @@ private function processStmtNode(
675677
throw new ShouldNotHappenException();
676678
}
677679

678-
$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
680+
$classStatementsGatherer = new ClassStatementsGatherer($this->phpDocPrivateClassConstFetchFinder, $classReflection, $nodeCallback);
679681
$this->processAttributeGroups($stmt->attrGroups, $classScope, $classStatementsGatherer);
680682

681683
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);

‎src/Node/ClassStatementsGatherer.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Node\Constant\ClassConstantFetch;
1616
use PHPStan\Node\Property\PropertyRead;
1717
use PHPStan\Node\Property\PropertyWrite;
18+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
1819
use PHPStan\Reflection\ClassReflection;
1920
use PHPStan\ShouldNotHappenException;
2021
use PHPStan\Type\ThisType;
@@ -54,6 +55,7 @@ class ClassStatementsGatherer
5455
* @param callable(Node $node, Scope $scope): void $nodeCallback
5556
*/
5657
public function __construct(
58+
private PhpDocPrivateClassConstFetchFinder $phpDocPrivateClassConstFetchFinder,
5759
private ClassReflection $classReflection,
5860
callable $nodeCallback,
5961
)
@@ -124,6 +126,11 @@ private function gatherNodes(Node $node, Scope $scope): void
124126
if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) {
125127
return;
126128
}
129+
130+
if ($node->getDocComment() !== null) {
131+
$this->gatherConstFetchesFromDocComment($node->getDocComment()->getText(), $scope);
132+
}
133+
127134
if ($node instanceof ClassPropertyNode) {
128135
$this->properties[] = $node;
129136
if ($node->isPromoted()) {
@@ -235,4 +242,11 @@ private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): voi
235242
}
236243
}
237244

245+
private function gatherConstFetchesFromDocComment(string $phpDoc, Scope $scope): void
246+
{
247+
foreach ($this->phpDocPrivateClassConstFetchFinder->findClassConstFetches($phpDoc, $scope) as $constFetch) {
248+
$this->constantFetches[] = $constFetch;
249+
}
250+
}
251+
238252
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Node\Constant\ClassConstantFetch;
7+
use PHPStan\PhpDocParser\Ast\NodeTraverser;
8+
use PHPStan\ShouldNotHappenException;
9+
use PHPStan\Type\FileTypeMapper;
10+
11+
class PhpDocPrivateClassConstFetchFinder
12+
{
13+
14+
public function __construct(
15+
private FileTypeMapper $fileTypeMapper,
16+
)
17+
{
18+
}
19+
20+
/**
21+
* @return list<ClassConstantFetch>
22+
*/
23+
public function findClassConstFetches(string $phpDoc, Scope $scope): array
24+
{
25+
if (!$scope->isInClass()) {
26+
throw new ShouldNotHappenException();
27+
}
28+
29+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
30+
$scope->getFile(),
31+
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
32+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
33+
null,
34+
$phpDoc,
35+
);
36+
37+
$visitor = new PhpDocPrivateClassConstFetchFinderVisitor($scope->getClassReflection(), $scope);
38+
39+
(new NodeTraverser([$visitor]))->traverse($resolvedPhpDoc->getPhpDocNodes());
40+
41+
return $visitor->classConstantFetches;
42+
}
43+
44+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc;
4+
5+
use Nette\Utils\Strings;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Node\Constant\ClassConstantFetch;
10+
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
11+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
12+
use PHPStan\Reflection\ClassReflection;
13+
use function preg_quote;
14+
use function str_replace;
15+
16+
class PhpDocPrivateClassConstFetchFinderVisitor extends AbstractNodeVisitor
17+
{
18+
19+
/** @var list<ClassConstantFetch> */
20+
public array $classConstantFetches = [];
21+
22+
public function __construct(private ClassReflection $classReflection, private Scope $scope)
23+
{
24+
}
25+
26+
public function enterNode(\PHPStan\PhpDocParser\Ast\Node $node)
27+
{
28+
if (!$node instanceof ConstFetchNode) {
29+
return null;
30+
}
31+
32+
if ($node->className !== 'self') {
33+
return null;
34+
}
35+
36+
$constantName = $node->name;
37+
if (Strings::contains($constantName, '*')) {
38+
// convert * into .*? and escape everything else so the constants can be matched against the pattern
39+
$pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D';
40+
foreach ($this->classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) {
41+
$classConstantName = $reflectionConstant->getName();
42+
if (Strings::match($classConstantName, $pattern) === null) {
43+
continue;
44+
}
45+
46+
$this->addClassConstantFetch($classConstantName);
47+
}
48+
} else {
49+
$this->addClassConstantFetch($constantName);
50+
}
51+
52+
return null;
53+
}
54+
55+
private function addClassConstantFetch(string $constantName): void
56+
{
57+
$this->classConstantFetches[] = new ClassConstantFetch(
58+
new Expr\ClassConstFetch(
59+
new Node\Name\FullyQualified($this->classReflection->getName()),
60+
$constantName,
61+
),
62+
$this->scope,
63+
);
64+
}
65+
66+
}

‎src/Testing/RuleTestCase.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPStan\Node\CollectedDataNode;
1919
use PHPStan\Php\PhpVersion;
2020
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
21+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
2122
use PHPStan\PhpDoc\StubPhpDocProvider;
2223
use PHPStan\Reflection\InitializerExprTypeResolver;
2324
use PHPStan\Rules\DirectRegistry as DirectRuleRegistry;
@@ -92,6 +93,7 @@ private function getAnalyser(): Analyser
9293
$typeSpecifier,
9394
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
9495
$readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
96+
self::getContainer()->getByType(PhpDocPrivateClassConstFetchFinder::class),
9597
$this->shouldPolluteScopeWithLoopInitialAssignments(),
9698
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
9799
[],

‎src/Testing/TypeInferenceTestCase.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\File\FileHelper;
1313
use PHPStan\Php\PhpVersion;
1414
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
15+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
1516
use PHPStan\PhpDoc\StubPhpDocProvider;
1617
use PHPStan\Reflection\InitializerExprTypeResolver;
1718
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
@@ -59,6 +60,7 @@ public static function processFile(
5960
$typeSpecifier,
6061
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
6162
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
63+
self::getContainer()->getByType(PhpDocPrivateClassConstFetchFinder::class),
6264
true,
6365
true,
6466
static::getEarlyTerminatingMethodCalls(),

‎tests/PHPStan/Analyser/AnalyserTest.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Parser\RichParser;
1515
use PHPStan\Php\PhpVersion;
1616
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
17+
use PHPStan\PhpDoc\PhpDocPrivateClassConstFetchFinder;
1718
use PHPStan\PhpDoc\StubPhpDocProvider;
1819
use PHPStan\Reflection\InitializerExprTypeResolver;
1920
use PHPStan\Rules\AlwaysFailRule;
@@ -636,6 +637,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser
636637
$typeSpecifier,
637638
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
638639
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
640+
self::getContainer()->getByType(PhpDocPrivateClassConstFetchFinder::class),
639641
false,
640642
true,
641643
[],

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,36 @@ public function testBug9005(): void
9292
$this->analyse([__DIR__ . '/data/bug-9005.php'], []);
9393
}
9494

95+
public function testBug6415(): void
96+
{
97+
$this->analyse([__DIR__ . '/data/bug-6415.php'], []);
98+
}
99+
100+
public function testBug7184(): void
101+
{
102+
if (PHP_VERSION_ID < 80000) {
103+
$this->markTestSkipped('Test requires PHP 8.0.');
104+
}
105+
106+
$this->analyse([__DIR__ . '/data/bug-7184.php'], []);
107+
}
108+
109+
public function testBug9385(): void
110+
{
111+
if (PHP_VERSION_ID < 70400) {
112+
$this->markTestSkipped('Test requires PHP 7.4.');
113+
}
114+
115+
$this->analyse([__DIR__ . '/data/bug-9385.php'], []);
116+
}
117+
118+
public function testPhpdocConstWildcard(): void
119+
{
120+
if (PHP_VERSION_ID < 70400) {
121+
$this->markTestSkipped('Test requires PHP 7.4.');
122+
}
123+
124+
$this->analyse([__DIR__ . '/data/phpdoc-const-wildcard.php'], []);
125+
}
126+
95127
}
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+
}

0 commit comments

Comments
 (0)