Skip to content

Commit

Permalink
Specify isA class-string types for falsey context
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm committed Feb 27, 2022
1 parent 73f14db commit 57b9406
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 47 deletions.
89 changes: 49 additions & 40 deletions src/Type/Php/IsAFunctionTypeSpecifyingExtension.php
Expand Up @@ -2,24 +2,26 @@

namespace PHPStan\Type\Php;

use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function count;
use function strtolower;

class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
Expand All @@ -30,53 +32,60 @@ class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtens
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return strtolower($functionReflection->getName()) === 'is_a'
&& isset($node->getArgs()[0])
&& isset($node->getArgs()[1])
&& !$context->null();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
if ($context->null()) {
throw new ShouldNotHappenException();
if (count($node->getArgs()) < 2) {
return new SpecifiedTypes();
}
$classType = $scope->getType($node->getArgs()[1]->value);
$allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false);
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));

$classNameArgExpr = $node->getArgs()[1]->value;
$classNameArgExprType = $scope->getType($classNameArgExpr);
if (
$classNameArgExpr instanceof ClassConstFetch
&& $classNameArgExpr->class instanceof Name
&& $classNameArgExpr->name instanceof Node\Identifier
&& strtolower($classNameArgExpr->name->name) === 'class'
) {
$objectType = $scope->resolveTypeByName($classNameArgExpr->class);
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($classNameArgExprType instanceof ConstantStringType) {
$objectType = new ObjectType($classNameArgExprType->getValue());
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($classNameArgExprType instanceof GenericClassStringType) {
$objectType = $classNameArgExprType->getGenericType();
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($context->true()) {
$objectType = new ObjectWithoutClassType();
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} else {
$types = new SpecifiedTypes();
if (!$classType instanceof ConstantStringType && !$context->truthy()) {
return new SpecifiedTypes([], []);
}

if (isset($node->getArgs()[2]) && $context->true()) {
if (!$scope->getType($node->getArgs()[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) {
$types = $types->intersectWith($this->typeSpecifier->create(
$node->getArgs()[0]->value,
isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(),
$context,
false,
$scope,
));
$type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use ($allowString): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
}
if ($type instanceof ConstantStringType) {
if ($allowString) {
return TypeCombinator::union(
new ObjectType($type->getValue()),
new GenericClassStringType(new ObjectType($type->getValue())),
);
}
return new ObjectType($type->getValue());
}
if ($type instanceof GenericClassStringType) {
if ($allowString) {
return TypeCombinator::union(
$type->getGenericType(),
$type,
);
}
return $type->getGenericType();
}
if ($allowString) {
return TypeCombinator::union(
new ObjectWithoutClassType(),
new ClassStringType(),
);
}
return new ObjectWithoutClassType();
});

return $types;
return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
$type,
$context,
false,
$scope,
);
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -726,6 +726,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6704.php');
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6698.php');
Expand Down
12 changes: 6 additions & 6 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Expand Up @@ -445,7 +445,7 @@ public function dataCondition(): array
)),
]),
['$foo' => 'static(DateTime)'],
['$foo' => '~static(DateTime)'],
[],
],
[
new FuncCall(new Name('is_a'), [
Expand All @@ -461,7 +461,7 @@ public function dataCondition(): array
new Arg(new Variable('genericClassString')),
]),
['$foo' => 'Bar'],
['$foo' => '~Bar'],
[],
],
[
new FuncCall(new Name('is_a'), [
Expand All @@ -470,15 +470,15 @@ public function dataCondition(): array
new Arg(new Expr\ConstFetch(new Name('true'))),
]),
['$foo' => 'class-string<Foo>|Foo'],
['$foo' => '~Foo'],
['$foo' => '~class-string<Foo>|Foo'],
],
[
new FuncCall(new Name('is_a'), [
new Arg(new Variable('foo')),
new Arg(new Variable('className')),
new Arg(new Expr\ConstFetch(new Name('true'))),
]),
['$foo' => 'class-string<object>|object'],
['$foo' => 'class-string|object'],
[],
],
[
Expand All @@ -488,15 +488,15 @@ public function dataCondition(): array
new Arg(new Variable('unknown')),
]),
['$foo' => 'class-string<Foo>|Foo'],
['$foo' => '~Foo'],
['$foo' => '~class-string<Foo>|Foo'],
],
[
new FuncCall(new Name('is_a'), [
new Arg(new Variable('foo')),
new Arg(new Variable('className')),
new Arg(new Variable('unknown')),
]),
['$foo' => 'class-string<object>|object'],
['$foo' => 'class-string|object'],
[],
],
[
Expand Down
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6704.php
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace Bug6704;

use DateTimeImmutable;
use stdClass;
use function PHPStan\Testing\assertType;

/**
* @param class-string<DateTimeImmutable>|class-string<stdClass> $a
* @param DateTimeImmutable|stdClass $b
*/
function foo(string $a, object $b): void
{
if (!is_a($a, stdClass::class, true)) {
assertType('class-string<DateTimeImmutable>', $a);
}

if (!is_a($b, stdClass::class)) {
assertType('DateTimeImmutable', $b);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/is-a.php
Expand Up @@ -23,6 +23,6 @@ function (string $foo) {

function (string $foo, string $someString) {
if (is_a($foo, $someString, true)) {
\PHPStan\Testing\assertType('class-string<object>', $foo);
\PHPStan\Testing\assertType('class-string', $foo);
}
};

0 comments on commit 57b9406

Please sign in to comment.