Skip to content

Commit 70ac622

Browse files
herndlmondrejmirtes
authored andcommitted
Duplicate and adapt solution from is_subclass_of for is_a
1 parent baf9e84 commit 70ac622

File tree

5 files changed

+79
-47
lines changed

5 files changed

+79
-47
lines changed

src/Type/Php/IsAFunctionTypeSpecifyingExtension.php

+49-40
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,26 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node;
6-
use PhpParser\Node\Expr\ClassConstFetch;
75
use PhpParser\Node\Expr\FuncCall;
8-
use PhpParser\Node\Name;
96
use PHPStan\Analyser\Scope;
107
use PHPStan\Analyser\SpecifiedTypes;
118
use PHPStan\Analyser\TypeSpecifier;
129
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1310
use PHPStan\Analyser\TypeSpecifierContext;
1411
use PHPStan\Reflection\FunctionReflection;
15-
use PHPStan\ShouldNotHappenException;
1612
use PHPStan\Type\ClassStringType;
1713
use PHPStan\Type\Constant\ConstantBooleanType;
1814
use PHPStan\Type\Constant\ConstantStringType;
1915
use PHPStan\Type\FunctionTypeSpecifyingExtension;
2016
use PHPStan\Type\Generic\GenericClassStringType;
17+
use PHPStan\Type\IntersectionType;
2118
use PHPStan\Type\ObjectType;
2219
use PHPStan\Type\ObjectWithoutClassType;
20+
use PHPStan\Type\Type;
21+
use PHPStan\Type\TypeCombinator;
22+
use PHPStan\Type\TypeTraverser;
23+
use PHPStan\Type\UnionType;
24+
use function count;
2325
use function strtolower;
2426

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

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

44-
$classNameArgExpr = $node->getArgs()[1]->value;
45-
$classNameArgExprType = $scope->getType($classNameArgExpr);
46-
if (
47-
$classNameArgExpr instanceof ClassConstFetch
48-
&& $classNameArgExpr->class instanceof Name
49-
&& $classNameArgExpr->name instanceof Node\Identifier
50-
&& strtolower($classNameArgExpr->name->name) === 'class'
51-
) {
52-
$objectType = $scope->resolveTypeByName($classNameArgExpr->class);
53-
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
54-
} elseif ($classNameArgExprType instanceof ConstantStringType) {
55-
$objectType = new ObjectType($classNameArgExprType->getValue());
56-
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
57-
} elseif ($classNameArgExprType instanceof GenericClassStringType) {
58-
$objectType = $classNameArgExprType->getGenericType();
59-
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
60-
} elseif ($context->true()) {
61-
$objectType = new ObjectWithoutClassType();
62-
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
63-
} else {
64-
$types = new SpecifiedTypes();
47+
if (!$classType instanceof ConstantStringType && !$context->truthy()) {
48+
return new SpecifiedTypes([], []);
6549
}
6650

67-
if (isset($node->getArgs()[2]) && $context->true()) {
68-
if (!$scope->getType($node->getArgs()[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) {
69-
$types = $types->intersectWith($this->typeSpecifier->create(
70-
$node->getArgs()[0]->value,
71-
isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(),
72-
$context,
73-
false,
74-
$scope,
75-
));
51+
$type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use ($allowString): Type {
52+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
53+
return $traverse($type);
7654
}
77-
}
55+
if ($type instanceof ConstantStringType) {
56+
if ($allowString) {
57+
return TypeCombinator::union(
58+
new ObjectType($type->getValue()),
59+
new GenericClassStringType(new ObjectType($type->getValue())),
60+
);
61+
}
62+
return new ObjectType($type->getValue());
63+
}
64+
if ($type instanceof GenericClassStringType) {
65+
if ($allowString) {
66+
return TypeCombinator::union(
67+
$type->getGenericType(),
68+
$type,
69+
);
70+
}
71+
return $type->getGenericType();
72+
}
73+
if ($allowString) {
74+
return TypeCombinator::union(
75+
new ObjectWithoutClassType(),
76+
new ClassStringType(),
77+
);
78+
}
79+
return new ObjectWithoutClassType();
80+
});
7881

79-
return $types;
82+
return $this->typeSpecifier->create(
83+
$node->getArgs()[0]->value,
84+
$type,
85+
$context,
86+
false,
87+
$scope,
88+
);
8089
}
8190

8291
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ public function dataFileAsserts(): iterable
726726
yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php');
727727

728728
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php');
729+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6704.php');
729730
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php');
730731
yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php');
731732

tests/PHPStan/Analyser/TypeSpecifierTest.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ public function dataCondition(): array
445445
)),
446446
]),
447447
['$foo' => 'static(DateTime)'],
448-
['$foo' => '~static(DateTime)'],
448+
[],
449449
],
450450
[
451451
new FuncCall(new Name('is_a'), [
@@ -461,7 +461,7 @@ public function dataCondition(): array
461461
new Arg(new Variable('genericClassString')),
462462
]),
463463
['$foo' => 'Bar'],
464-
['$foo' => '~Bar'],
464+
[],
465465
],
466466
[
467467
new FuncCall(new Name('is_a'), [
@@ -470,15 +470,15 @@ public function dataCondition(): array
470470
new Arg(new Expr\ConstFetch(new Name('true'))),
471471
]),
472472
['$foo' => 'class-string<Foo>|Foo'],
473-
['$foo' => '~Foo'],
473+
['$foo' => '~class-string<Foo>|Foo'],
474474
],
475475
[
476476
new FuncCall(new Name('is_a'), [
477477
new Arg(new Variable('foo')),
478478
new Arg(new Variable('className')),
479479
new Arg(new Expr\ConstFetch(new Name('true'))),
480480
]),
481-
['$foo' => 'class-string<object>|object'],
481+
['$foo' => 'class-string|object'],
482482
[],
483483
],
484484
[
@@ -488,15 +488,15 @@ public function dataCondition(): array
488488
new Arg(new Variable('unknown')),
489489
]),
490490
['$foo' => 'class-string<Foo>|Foo'],
491-
['$foo' => '~Foo'],
491+
['$foo' => '~class-string<Foo>|Foo'],
492492
],
493493
[
494494
new FuncCall(new Name('is_a'), [
495495
new Arg(new Variable('foo')),
496496
new Arg(new Variable('className')),
497497
new Arg(new Variable('unknown')),
498498
]),
499-
['$foo' => 'class-string<object>|object'],
499+
['$foo' => 'class-string|object'],
500500
[],
501501
],
502502
[
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6704;
4+
5+
use DateTimeImmutable;
6+
use stdClass;
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @param class-string<DateTimeImmutable>|class-string<stdClass> $a
11+
* @param DateTimeImmutable|stdClass $b
12+
*/
13+
function foo(string $a, object $b): void
14+
{
15+
if (!is_a($a, stdClass::class, true)) {
16+
assertType('class-string<DateTimeImmutable>', $a);
17+
}
18+
19+
if (!is_a($b, stdClass::class)) {
20+
assertType('DateTimeImmutable', $b);
21+
}
22+
}

tests/PHPStan/Analyser/data/is-a.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ function (string $foo) {
2323

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

0 commit comments

Comments
 (0)