Skip to content
Closed
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 conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,11 @@ services:
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\ClassImplementsFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension
tags:
Expand Down
67 changes: 67 additions & 0 deletions src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeWithClassName;
use function count;

class ClassImplementsFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'class_implements';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
if (count($functionCall->getArgs()) < 1) {
return null;
}

$objectOrClassType = $scope->getType($functionCall->getArgs()[0]->value);
$autoload = !isset($functionCall->getArgs()[1]) || $scope->getType($functionCall->getArgs()[1]->value)->equals(new ConstantBooleanType(true));

if ($objectOrClassType instanceof TypeWithClassName) {
$className = $objectOrClassType->getClassName();
} elseif ($objectOrClassType instanceof ConstantStringType && $autoload) {
$className = $objectOrClassType->getValue();
} elseif ($objectOrClassType instanceof ClassStringType && $autoload) {
return TypeCombinator::remove(
ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(),
new ConstantBooleanType(false),
);
} else {
return null;
}

if (!$this->reflectionProvider->hasClass($className)) {
return new ConstantBooleanType(false);
}

$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($this->reflectionProvider->getClass($className)->getInterfaces() as $interface) {
$interfaceNameType = new ConstantStringType($interface->getName());
$builder->setOffsetValueType($interfaceNameType, $interfaceNameType);
}

return $builder->getArray();
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ public function dataFileAsserts(): iterable
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/class-implements.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php');

Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/data/class-implements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace ClassImplements;

use DateTime;
use Generator;
use stdClass;
use function PHPStan\Testing\assertType;

class ClassImplements
{

public function withObject(): void
{
assertType("array{DateTimeInterface: 'DateTimeInterface'}", class_implements(new DateTime()));
assertType("array{Iterator: 'Iterator', Traversable: 'Traversable'}", class_implements(new Generator()));
assertType('array{}', class_implements(new stdClass()));
assertType('false', class_implements(new invalidClass()));
}

public function withObjectAndAutoloadingDisabled(): void
{
assertType("array{DateTimeInterface: 'DateTimeInterface'}", class_implements(new DateTime(), false));
assertType("array{Iterator: 'Iterator', Traversable: 'Traversable'}", class_implements(new Generator(), false));
assertType('array{}', class_implements(new stdClass(), false));
assertType('false', class_implements(new invalidClass(), false));
}

/** @param class-string $classString */
public function withClassName(string $classString, string $className): void
{
assertType("array{DateTimeInterface: 'DateTimeInterface'}", class_implements(DateTime::class));
assertType("array{Iterator: 'Iterator', Traversable: 'Traversable'}", class_implements(Generator::class));
assertType('array{}', class_implements(stdClass::class));
assertType('false', class_implements(invalidClass::class));
assertType('array<string, string>', class_implements($classString));
assertType('array<string, string>|false', class_implements($className));
}

/** @param class-string $classString */
public function withClassNameAndAutoloadingDisabled(string $classString, string $className): void
{
assertType('array<string, string>|false', class_implements(DateTime::class, false));
assertType('array<string, string>|false', class_implements(stdClass::class, false));
assertType('array<string, string>|false', class_implements(invalidClass::class, false));
assertType('array<string, string>|false', class_implements($classString, false));
assertType('array<string, string>|false', class_implements($className, false));
}

}