Skip to content

Fix support for classes named after pseudotypes in phpdoc #365

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 8, 2021
Merged
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
5 changes: 5 additions & 0 deletions src/Analyser/NameScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public function getUses(): array
return $this->uses;
}

public function hasUseAlias(string $name): bool
{
return isset($this->uses[strtolower($name)]);
}

public function getClassName(): ?string
{
return $this->className;
Expand Down
61 changes: 61 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,21 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]);

case 'number':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new UnionType([new IntegerType(), new FloatType()]);

case 'numeric':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new UnionType([
new IntegerType(),
new FloatType(),
Expand All @@ -171,7 +183,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
]);

case 'bool':
return new BooleanType();

case 'boolean':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new BooleanType();

case 'true':
Expand All @@ -184,7 +204,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new NullType();

case 'float':
return new FloatType();

case 'double':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new FloatType();

case 'array':
Expand All @@ -204,6 +232,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new CallableType();

case 'resource':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new ResourceType();

case 'mixed':
Expand All @@ -216,6 +250,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new ObjectWithoutClassType();

case 'never':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

if ($type !== null) {
return $type;
}

return new NeverType(true);

case 'never-return':
case 'never-returns':
case 'no-return':
Expand Down Expand Up @@ -263,6 +305,25 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new ObjectType($stringName);
}

private function tryResolvePseudoTypeClassType(IdentifierTypeNode $typeNode, NameScope $nameScope): ?Type
{
if ($nameScope->hasUseAlias($typeNode->name)) {
return new ObjectType($nameScope->resolveStringName($typeNode->name));
}

if ($nameScope->getNamespace() === null) {
return null;
}

$className = $nameScope->resolveStringName($typeNode->name);

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

return null;
}

private function resolveThisTypeNode(ThisTypeNode $typeNode, NameScope $nameScope): Type
{
$className = $nameScope->getClassName();
Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5619,6 +5619,23 @@ public function dataArrayFunctions(): array
];
}

public function dataPseudoTypeOverrides(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-override.php');
}

public function dataPseudoTypeNamespace(): array
{
require_once __DIR__ . '/data/phpdoc-pseudotype-namespace.php';

return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-namespace.php');
}

public function dataPseudoTypeGlobal(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php');
}

/**
* @dataProvider dataArrayFunctions
* @param string $description
Expand Down Expand Up @@ -11220,6 +11237,9 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataNestedGenericIncompleteConstructor
* @dataProvider dataIteratorIterator
* @dataProvider dataBug4642
* @dataProvider dataPseudoTypeGlobal
* @dataProvider dataPseudoTypeNamespace
* @dataProvider dataPseudoTypeOverrides
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use function PHPStan\Analyser\assertType;

function () {
/** @var Number $number */
$number = doFoo();

/** @var Boolean $boolean */
$boolean = doFoo();

/** @var Numeric $numeric */
$numeric = doFoo();

/** @var Never $never */
$never = doFoo();

/** @var Resource $resource */
$resource = doFoo();

/** @var Double $double */
$double = doFoo();

assertType('float|int', $number);
assertType('float|int|(string&numeric)', $numeric);
assertType('bool', $boolean);
assertType('resource', $resource);
assertType('*NEVER*', $never);
assertType('float', $double);
};
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace PhpdocPseudoTypesNamespace;

use function PHPStan\Analyser\assertType;

class Number {}
class Numeric {}
class Boolean {}
class Resource {}
class Never {}
class Double {}

function () {
/** @var Number $number */
$number = doFoo();

/** @var Boolean $boolean */
$boolean = doFoo();

/** @var Numeric $numeric */
$numeric = doFoo();

/** @var Never $never */
$never = doFoo();

/** @var Resource $resource */
$resource = doFoo();

/** @var Double $double */
$double = doFoo();

assertType('PhpdocPseudoTypesNamespace\Number', $number);
assertType('PhpdocPseudoTypesNamespace\Numeric', $numeric);
assertType('PhpdocPseudoTypesNamespace\Boolean', $boolean);
assertType('PhpdocPseudoTypesNamespace\Resource', $resource);
assertType('PhpdocPseudoTypesNamespace\Never', $never);
assertType('PhpdocPseudoTypesNamespace\Double', $double);
};
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/phpdoc-pseudotype-override.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use Foo\Number;
use Foo\Numeric;
use Foo\Boolean;
use Foo\Resource;
use Foo\Never;
use Foo\Double;

use function PHPStan\Analyser\assertType;

function () {
/** @var Number $number */
$number = doFoo();

/** @var Boolean $boolean */
$boolean = doFoo();

/** @var Numeric $numeric */
$numeric = doFoo();

/** @var Never $never */
$never = doFoo();

/** @var Resource $resource */
$resource = doFoo();

/** @var Double $double */
$double = doFoo();

assertType('Foo\Number', $number);
assertType('Foo\Numeric', $numeric);
assertType('Foo\Boolean', $boolean);
assertType('Foo\Resource', $resource);
assertType('Foo\Never', $never);
assertType('Foo\Double', $double);
};