Skip to content

Commit

Permalink
Support literals and class constants as PHPDoc types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 30, 2020
1 parent 90b1186 commit 730a902
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"nikic/php-parser": "^4.4.0",
"ondram/ci-detector": "^3.1",
"ondrejmirtes/better-reflection": "^4.0.1",
"phpstan/phpdoc-parser": "^0.4.4",
"phpstan/phpdoc-parser": "^0.4.5",
"react/child-process": "^0.6.1",
"react/event-loop": "^1.1",
"react/socket": "^1.3",
Expand Down
102 changes: 102 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

namespace PHPStan\PhpDoc;

use Nette\Utils\Strings;
use PHPStan\Analyser\NameScope;
use PHPStan\DependencyInjection\Container;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
Expand All @@ -30,6 +38,7 @@
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FloatType;
use PHPStan\Type\Generic\GenericClassStringType;
Expand Down Expand Up @@ -105,6 +114,8 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type

} elseif ($typeNode instanceof ArrayShapeNode) {
return $this->resolveArrayShapeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof ConstTypeNode) {
return $this->resolveConstTypeNode($typeNode, $nameScope);
}

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

private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameScope): Type
{
$constExpr = $typeNode->constExpr;
if ($constExpr instanceof ConstExprArrayNode) {
throw new \PHPStan\ShouldNotHappenException(); // we prefer array shapes
}

if (
$constExpr instanceof ConstExprFalseNode
|| $constExpr instanceof ConstExprTrueNode
|| $constExpr instanceof ConstExprNullNode
) {
throw new \PHPStan\ShouldNotHappenException(); // we prefer IdentifierTypeNode
}

if ($constExpr instanceof ConstFetchNode) {
if ($constExpr->className === '') {
throw new \PHPStan\ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
}

if ($nameScope->getClassName() !== null) {
switch (strtolower($constExpr->className)) {
case 'static':
case 'self':
$className = $nameScope->getClassName();
break;

case 'parent':
if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) {
$classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName());
if ($classReflection->getParentClass() === false) {
return new ErrorType();

}

$className = $classReflection->getParentClass()->getName();
}
}
}

if (!isset($className)) {
$className = $nameScope->resolveStringName($constExpr->className);
}

if (!$this->getReflectionProvider()->hasClass($className)) {
return new ErrorType();
}

$classReflection = $this->getReflectionProvider()->getClass($className);

$constantName = $constExpr->name;
if (Strings::endsWith($constantName, '*')) {
$constantNameStartsWith = Strings::substring($constantName, 0, Strings::length($constantName) - 1);
$constantTypes = [];
foreach ($classReflection->getNativeReflection()->getConstants() as $classConstantName => $constantValue) {
if (!Strings::startsWith($classConstantName, $constantNameStartsWith)) {
continue;
}

$constantTypes[] = ConstantTypeHelper::getTypeFromValue($constantValue);
}

if (count($constantTypes) === 0) {
return new ErrorType();
}

return TypeCombinator::union(...$constantTypes);
}

if (!$classReflection->hasConstant($constantName)) {
return new ErrorType();
}

return $classReflection->getConstant($constantName)->getValueType();
}

if ($constExpr instanceof ConstExprFloatNode) {
return ConstantTypeHelper::getTypeFromValue((float) $constExpr->value);
}

if ($constExpr instanceof ConstExprIntegerNode) {
return ConstantTypeHelper::getTypeFromValue((int) $constExpr->value);
}

if ($constExpr instanceof ConstExprStringNode) {
return ConstantTypeHelper::getTypeFromValue($constExpr->value);
}

return new ErrorType();
}

/**
* @param TypeNode[] $typeNodes
* @param NameScope $nameScope
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9843,6 +9843,11 @@ public function dataBug1216(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php');
}

public function dataConstExprPhpDocType(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php');
}

/**
* @dataProvider dataBug2574
* @dataProvider dataBug2577
Expand Down Expand Up @@ -9881,6 +9886,7 @@ public function dataBug1216(): array
* @dataProvider dataBug3142
* @dataProvider dataArrayShapeKeysStrings
* @dataProvider dataBug1216
* @dataProvider dataConstExprPhpDocType
* @param ConstantStringType $expectedType
* @param Type $actualType
*/
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace ConstExprPhpDocType;

use RecursiveIteratorIterator as Rec;
use function PHPStan\Analyser\assertType;

class Foo
{

public const SOME_CONSTANT = 1;
public const SOME_OTHER_CONSTANT = 2;

/**
* @param 'foo'|'bar' $one
* @param self::SOME_* $two
* @param self::SOME_OTHER_CONSTANT $three
* @param \ConstExprPhpDocType\Foo::SOME_CONSTANT $four
* @param Rec::LEAVES_ONLY $five
* @param 1.0 $six
* @param 234 $seven
* @param self::SOME_OTHER_* $eight
*/
public function doFoo(
$one,
$two,
$three,
$four,
$five,
$six,
$seven,
$eight
)
{
assertType("'bar'|'foo'", $one);
assertType('1|2', $two);
assertType('2', $three);
assertType('1', $four);
assertType('0', $five);
assertType('1.0', $six);
assertType('234', $seven);
assertType('2', $eight);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ public function testRule(): void
'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.',
39,
],
[
'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant contains unresolvable type.',
42,
],
[
'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant2 contains unresolvable type.',
45,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ class FooWithProperty
/** @var \InvalidPhpDocDefinitions\FooGeneric<int, \stdClass> */
private $anotherInvalidTypeGenericfoo;

/** @var UnknownClass::BLABLA */
private $unknownClassConstant;

/** @var self::BLABLA */
private $unknownClassConstant2;

}

0 comments on commit 730a902

Please sign in to comment.