Skip to content

Commit 730a902

Browse files
committed
Support literals and class constants as PHPDoc types
1 parent 90b1186 commit 730a902

6 files changed

+168
-1
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"nikic/php-parser": "^4.4.0",
2323
"ondram/ci-detector": "^3.1",
2424
"ondrejmirtes/better-reflection": "^4.0.1",
25-
"phpstan/phpdoc-parser": "^0.4.4",
25+
"phpstan/phpdoc-parser": "^0.4.5",
2626
"react/child-process": "^0.6.1",
2727
"react/event-loop": "^1.1",
2828
"react/socket": "^1.3",

src/PhpDoc/TypeNodeResolver.php

+102
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22

33
namespace PHPStan\PhpDoc;
44

5+
use Nette\Utils\Strings;
56
use PHPStan\Analyser\NameScope;
67
use PHPStan\DependencyInjection\Container;
8+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
9+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
10+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
711
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
12+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
813
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
14+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
15+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
916
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
1017
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
1118
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
1219
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
20+
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
1321
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1422
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1523
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@@ -30,6 +38,7 @@
3038
use PHPStan\Type\Constant\ConstantBooleanType;
3139
use PHPStan\Type\Constant\ConstantIntegerType;
3240
use PHPStan\Type\Constant\ConstantStringType;
41+
use PHPStan\Type\ConstantTypeHelper;
3342
use PHPStan\Type\ErrorType;
3443
use PHPStan\Type\FloatType;
3544
use PHPStan\Type\Generic\GenericClassStringType;
@@ -105,6 +114,8 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
105114

106115
} elseif ($typeNode instanceof ArrayShapeNode) {
107116
return $this->resolveArrayShapeNode($typeNode, $nameScope);
117+
} elseif ($typeNode instanceof ConstTypeNode) {
118+
return $this->resolveConstTypeNode($typeNode, $nameScope);
108119
}
109120

110121
return new ErrorType();
@@ -481,6 +492,97 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name
481492
return TypeCombinator::union(...$arrays);
482493
}
483494

495+
private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameScope): Type
496+
{
497+
$constExpr = $typeNode->constExpr;
498+
if ($constExpr instanceof ConstExprArrayNode) {
499+
throw new \PHPStan\ShouldNotHappenException(); // we prefer array shapes
500+
}
501+
502+
if (
503+
$constExpr instanceof ConstExprFalseNode
504+
|| $constExpr instanceof ConstExprTrueNode
505+
|| $constExpr instanceof ConstExprNullNode
506+
) {
507+
throw new \PHPStan\ShouldNotHappenException(); // we prefer IdentifierTypeNode
508+
}
509+
510+
if ($constExpr instanceof ConstFetchNode) {
511+
if ($constExpr->className === '') {
512+
throw new \PHPStan\ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
513+
}
514+
515+
if ($nameScope->getClassName() !== null) {
516+
switch (strtolower($constExpr->className)) {
517+
case 'static':
518+
case 'self':
519+
$className = $nameScope->getClassName();
520+
break;
521+
522+
case 'parent':
523+
if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) {
524+
$classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName());
525+
if ($classReflection->getParentClass() === false) {
526+
return new ErrorType();
527+
528+
}
529+
530+
$className = $classReflection->getParentClass()->getName();
531+
}
532+
}
533+
}
534+
535+
if (!isset($className)) {
536+
$className = $nameScope->resolveStringName($constExpr->className);
537+
}
538+
539+
if (!$this->getReflectionProvider()->hasClass($className)) {
540+
return new ErrorType();
541+
}
542+
543+
$classReflection = $this->getReflectionProvider()->getClass($className);
544+
545+
$constantName = $constExpr->name;
546+
if (Strings::endsWith($constantName, '*')) {
547+
$constantNameStartsWith = Strings::substring($constantName, 0, Strings::length($constantName) - 1);
548+
$constantTypes = [];
549+
foreach ($classReflection->getNativeReflection()->getConstants() as $classConstantName => $constantValue) {
550+
if (!Strings::startsWith($classConstantName, $constantNameStartsWith)) {
551+
continue;
552+
}
553+
554+
$constantTypes[] = ConstantTypeHelper::getTypeFromValue($constantValue);
555+
}
556+
557+
if (count($constantTypes) === 0) {
558+
return new ErrorType();
559+
}
560+
561+
return TypeCombinator::union(...$constantTypes);
562+
}
563+
564+
if (!$classReflection->hasConstant($constantName)) {
565+
return new ErrorType();
566+
}
567+
568+
return $classReflection->getConstant($constantName)->getValueType();
569+
}
570+
571+
if ($constExpr instanceof ConstExprFloatNode) {
572+
return ConstantTypeHelper::getTypeFromValue((float) $constExpr->value);
573+
}
574+
575+
if ($constExpr instanceof ConstExprIntegerNode) {
576+
return ConstantTypeHelper::getTypeFromValue((int) $constExpr->value);
577+
}
578+
579+
if ($constExpr instanceof ConstExprStringNode) {
580+
return ConstantTypeHelper::getTypeFromValue($constExpr->value);
581+
}
582+
583+
return new ErrorType();
584+
}
585+
484586
/**
485587
* @param TypeNode[] $typeNodes
486588
* @param NameScope $nameScope

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -9843,6 +9843,11 @@ public function dataBug1216(): array
98439843
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php');
98449844
}
98459845

9846+
public function dataConstExprPhpDocType(): array
9847+
{
9848+
return $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php');
9849+
}
9850+
98469851
/**
98479852
* @dataProvider dataBug2574
98489853
* @dataProvider dataBug2577
@@ -9881,6 +9886,7 @@ public function dataBug1216(): array
98819886
* @dataProvider dataBug3142
98829887
* @dataProvider dataArrayShapeKeysStrings
98839888
* @dataProvider dataBug1216
9889+
* @dataProvider dataConstExprPhpDocType
98849890
* @param ConstantStringType $expectedType
98859891
* @param Type $actualType
98869892
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace ConstExprPhpDocType;
4+
5+
use RecursiveIteratorIterator as Rec;
6+
use function PHPStan\Analyser\assertType;
7+
8+
class Foo
9+
{
10+
11+
public const SOME_CONSTANT = 1;
12+
public const SOME_OTHER_CONSTANT = 2;
13+
14+
/**
15+
* @param 'foo'|'bar' $one
16+
* @param self::SOME_* $two
17+
* @param self::SOME_OTHER_CONSTANT $three
18+
* @param \ConstExprPhpDocType\Foo::SOME_CONSTANT $four
19+
* @param Rec::LEAVES_ONLY $five
20+
* @param 1.0 $six
21+
* @param 234 $seven
22+
* @param self::SOME_OTHER_* $eight
23+
*/
24+
public function doFoo(
25+
$one,
26+
$two,
27+
$three,
28+
$four,
29+
$five,
30+
$six,
31+
$seven,
32+
$eight
33+
)
34+
{
35+
assertType("'bar'|'foo'", $one);
36+
assertType('1|2', $two);
37+
assertType('2', $three);
38+
assertType('1', $four);
39+
assertType('0', $five);
40+
assertType('1.0', $six);
41+
assertType('234', $seven);
42+
assertType('2', $eight);
43+
}
44+
45+
}

tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ public function testRule(): void
4747
'Type stdClass in generic type InvalidPhpDocDefinitions\FooGeneric<int, stdClass> in PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$anotherInvalidTypeGenericfoo is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.',
4848
39,
4949
],
50+
[
51+
'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant contains unresolvable type.',
52+
42,
53+
],
54+
[
55+
'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant2 contains unresolvable type.',
56+
45,
57+
],
5058
]);
5159
}
5260

tests/PHPStan/Rules/PhpDoc/data/incompatible-property-phpdoc.php

+6
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,10 @@ class FooWithProperty
3838
/** @var \InvalidPhpDocDefinitions\FooGeneric<int, \stdClass> */
3939
private $anotherInvalidTypeGenericfoo;
4040

41+
/** @var UnknownClass::BLABLA */
42+
private $unknownClassConstant;
43+
44+
/** @var self::BLABLA */
45+
private $unknownClassConstant2;
46+
4147
}

0 commit comments

Comments
 (0)