Skip to content

Commit de99d53

Browse files
committed
Extract Type::toObjectTypeForIsACheck() for is_a($x, $class, $allowString)
Replaces `IsAFunctionTypeSpecifyingHelper::determineType()`'s hand-rolled `TypeTraverser::map` callback with a polymorphic `Type` method. The helper shrinks to a few lines that: - call `$classType->toObjectTypeForIsACheck($objectOrClassType, ...)`, - OR-fold the polymorphic uncertainty with the original "no constant strings in input" initial state, - run the same false-positive suppression check. Per leaf: - `ConstantStringType`: the only branch with real logic — collapses to `NeverType` when the input is the same final class (`!$allowSameClass`), sets uncertainty when the same class name appears in the input's class names (or when `$allowString` matches the input's superclass), then projects to `ObjectType($value)` (or `ObjectType|class-string<X>` if `$allowString`). - `GenericClassStringType`: projects to its `getGenericType()` (or a union with the class-string itself if `$allowString`), no uncertainty. - All other leaves (default in `NonObjectTypeTrait`, `MaybeObjectTypeTrait`, `ObjectTypeTrait`, plus direct overrides on `ObjectType`, `StaticType`, `ClosureType`, `NonexistentParentClassType`, `MixedType`, `StrictMixedType`): `ObjectWithoutClassType` (or `ObjectWithoutClassType|class-string` if `$allowString`), no uncertainty. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, OR-folding uncertainty. - `LateResolvableTypeTrait`: delegate to `resolve()`. Reuses the `ClassNameToObjectTypeResult` value object introduced for `toObjectTypeForInstanceofCheck()`. Pure refactor: full test suite + phpstan + cs pass.
1 parent 2ffb575 commit de99d53

17 files changed

Lines changed: 244 additions & 85 deletions

src/Type/ClosureType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
531531
return new ClassNameToObjectTypeResult($this, true);
532532
}
533533

534+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
535+
{
536+
if ($allowString) {
537+
return new ClassNameToObjectTypeResult(
538+
new UnionType([new ObjectWithoutClassType(), new ClassStringType()]),
539+
false,
540+
);
541+
}
542+
543+
return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false);
544+
}
545+
534546
public function toAbsoluteNumber(): Type
535547
{
536548
return new ErrorType();

src/Type/Constant/ConstantStringType.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,19 @@
3737
use PHPStan\Type\IntersectionType;
3838
use PHPStan\Type\IsSuperTypeOfResult;
3939
use PHPStan\Type\MixedType;
40+
use PHPStan\Type\NeverType;
4041
use PHPStan\Type\NullType;
4142
use PHPStan\Type\ObjectType;
4243
use PHPStan\Type\StaticType;
4344
use PHPStan\Type\StringType;
4445
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
4546
use PHPStan\Type\Type;
4647
use PHPStan\Type\TypeCombinator;
48+
use PHPStan\Type\UnionType;
4749
use PHPStan\Type\VerbosityLevel;
4850
use function addcslashes;
51+
use function array_unique;
52+
use function array_values;
4953
use function in_array;
5054
use function is_float;
5155
use function is_int;
@@ -299,6 +303,56 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
299303
return new ClassNameToObjectTypeResult(new ObjectType($this->value), false);
300304
}
301305

306+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
307+
{
308+
$objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames();
309+
if ($allowString) {
310+
foreach ($objectOrClassType->getConstantStrings() as $constantString) {
311+
$objectOrClassTypeClassNames[] = $constantString->getValue();
312+
}
313+
$objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames));
314+
}
315+
316+
$uncertainty = false;
317+
if (!$allowSameClass) {
318+
if ($objectOrClassTypeClassNames === [$this->value]) {
319+
$isSameClass = true;
320+
foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) {
321+
if (!$classReflection->isFinal()) {
322+
$isSameClass = false;
323+
break;
324+
}
325+
}
326+
327+
if ($isSameClass) {
328+
return new ClassNameToObjectTypeResult(new NeverType(), false);
329+
}
330+
}
331+
332+
if (
333+
// For object, as soon as the exact same type is provided
334+
// in the list we cannot be sure of the result
335+
in_array($this->value, $objectOrClassTypeClassNames, true)
336+
// This also occurs for generic class string
337+
|| ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($this)->yes())
338+
) {
339+
$uncertainty = true;
340+
}
341+
}
342+
343+
if ($allowString) {
344+
return new ClassNameToObjectTypeResult(
345+
new UnionType([
346+
new ObjectType($this->value),
347+
new GenericClassStringType(new ObjectType($this->value)),
348+
]),
349+
$uncertainty,
350+
);
351+
}
352+
353+
return new ClassNameToObjectTypeResult(new ObjectType($this->value), $uncertainty);
354+
}
355+
302356
public function toAbsoluteNumber(): Type
303357
{
304358
return $this->toNumber()->toAbsoluteNumber();

src/Type/Generic/GenericClassStringType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
5555
return new ClassNameToObjectTypeResult($this->getGenericType(), true);
5656
}
5757

58+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
59+
{
60+
if ($allowString) {
61+
return new ClassNameToObjectTypeResult(
62+
TypeCombinator::union($this->getGenericType(), $this),
63+
false,
64+
);
65+
}
66+
67+
return new ClassNameToObjectTypeResult($this->getGenericType(), false);
68+
}
69+
5870
public function getClassStringObjectType(): Type
5971
{
6072
return $this->getGenericType();

src/Type/IntersectionType.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,23 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
14441444
return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
14451445
}
14461446

1447+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1448+
{
1449+
$types = [];
1450+
$uncertainty = false;
1451+
foreach ($this->getTypes() as $innerType) {
1452+
$result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1453+
$types[] = $result->type;
1454+
if (!$result->uncertainty) {
1455+
continue;
1456+
}
1457+
1458+
$uncertainty = true;
1459+
}
1460+
1461+
return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1462+
}
1463+
14471464
public function toAbsoluteNumber(): Type
14481465
{
14491466
$type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());

src/Type/MixedType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
647647
return new ClassNameToObjectTypeResult(new MixedType(), false);
648648
}
649649

650+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
651+
{
652+
if ($allowString) {
653+
return new ClassNameToObjectTypeResult(
654+
new UnionType([new ObjectWithoutClassType(), new ClassStringType()]),
655+
false,
656+
);
657+
}
658+
659+
return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false);
660+
}
661+
650662
public function toAbsoluteNumber(): Type
651663
{
652664
return $this->toNumber()->toAbsoluteNumber();

src/Type/NeverType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
455455
return new ClassNameToObjectTypeResult($this, false);
456456
}
457457

458+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
459+
{
460+
return new ClassNameToObjectTypeResult($this, false);
461+
}
462+
458463
public function toAbsoluteNumber(): Type
459464
{
460465
return $this;

src/Type/NonexistentParentClassType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
185185
return new ClassNameToObjectTypeResult($this, true);
186186
}
187187

188+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
189+
{
190+
if ($allowString) {
191+
return new ClassNameToObjectTypeResult(
192+
new UnionType([new ObjectWithoutClassType(), new ClassStringType()]),
193+
false,
194+
);
195+
}
196+
197+
return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false);
198+
}
199+
188200
public function toAbsoluteNumber(): Type
189201
{
190202
return new ErrorType();

src/Type/ObjectType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
792792
return new ClassNameToObjectTypeResult($this, true);
793793
}
794794

795+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
796+
{
797+
if ($allowString) {
798+
return new ClassNameToObjectTypeResult(
799+
new UnionType([new ObjectWithoutClassType(), new ClassStringType()]),
800+
false,
801+
);
802+
}
803+
804+
return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false);
805+
}
806+
795807
public function toAbsoluteNumber(): Type
796808
{
797809
return $this->toNumber()->toAbsoluteNumber();

src/Type/Php/IsAFunctionTypeSpecifyingHelper.php

Lines changed: 8 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,7 @@
33
namespace PHPStan\Type\Php;
44

55
use PHPStan\DependencyInjection\AutowiredService;
6-
use PHPStan\Type\ClassStringType;
7-
use PHPStan\Type\Constant\ConstantStringType;
8-
use PHPStan\Type\Generic\GenericClassStringType;
9-
use PHPStan\Type\IntersectionType;
10-
use PHPStan\Type\NeverType;
11-
use PHPStan\Type\ObjectType;
12-
use PHPStan\Type\ObjectWithoutClassType;
136
use PHPStan\Type\Type;
14-
use PHPStan\Type\TypeCombinator;
15-
use PHPStan\Type\TypeTraverser;
16-
use PHPStan\Type\UnionType;
17-
use function array_unique;
18-
use function array_values;
19-
use function in_array;
207

218
#[AutowiredService]
229
final class IsAFunctionTypeSpecifyingHelper
@@ -29,84 +16,20 @@ public function determineType(
2916
bool $allowSameClass,
3017
): ?Type
3118
{
32-
$objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames();
33-
if ($allowString) {
34-
foreach ($objectOrClassType->getConstantStrings() as $constantString) {
35-
$objectOrClassTypeClassNames[] = $constantString->getValue();
36-
}
37-
$objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames));
38-
}
39-
40-
$isUncertain = $classType->getConstantStrings() === [];
41-
42-
$resultType = TypeTraverser::map(
43-
$classType,
44-
static function (Type $type, callable $traverse) use ($objectOrClassType, $objectOrClassTypeClassNames, $allowString, $allowSameClass, &$isUncertain): Type {
45-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
46-
return $traverse($type);
47-
}
48-
if ($type instanceof ConstantStringType) {
49-
if (!$allowSameClass) {
50-
if ($objectOrClassTypeClassNames === [$type->getValue()]) {
51-
$isSameClass = true;
52-
foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) {
53-
if (!$classReflection->isFinal()) {
54-
$isSameClass = false;
55-
break;
56-
}
57-
}
58-
59-
if ($isSameClass) {
60-
return new NeverType();
61-
}
62-
}
63-
64-
if (
65-
// For object, as soon as the exact same type is provided
66-
// in the list we cannot be sure of the result
67-
in_array($type->getValue(), $objectOrClassTypeClassNames, true)
68-
// This also occurs for generic class string
69-
|| ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($type)->yes())
70-
) {
71-
$isUncertain = true;
72-
}
73-
}
74-
if ($allowString) {
75-
return new UnionType([
76-
new ObjectType($type->getValue()),
77-
new GenericClassStringType(new ObjectType($type->getValue())),
78-
]);
79-
}
80-
81-
return new ObjectType($type->getValue());
82-
}
83-
if ($type instanceof GenericClassStringType) {
84-
if ($allowString) {
85-
return TypeCombinator::union(
86-
$type->getGenericType(),
87-
$type,
88-
);
89-
}
90-
91-
return $type->getGenericType();
92-
}
93-
if ($allowString) {
94-
return new UnionType([
95-
new ObjectWithoutClassType(),
96-
new ClassStringType(),
97-
]);
98-
}
19+
$result = $classType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
9920

100-
return new ObjectWithoutClassType();
101-
},
102-
);
21+
// `getConstantStrings() === []` propagates uncertainty from
22+
// the input as a whole — preserved from the original
23+
// `$isUncertain` initial state to keep the false-positive
24+
// suppression below identical.
25+
$isUncertain = $result->uncertainty || $classType->getConstantStrings() === [];
10326

10427
// prevent false-positives
105-
if ($isUncertain && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
28+
if ($isUncertain && $result->type->isSuperTypeOf($objectOrClassType)->yes()) {
10629
return null;
10730
}
10831

109-
return $resultType;
32+
return $result->type;
11033
}
11134

11235
}

src/Type/StaticType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,18 @@ public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
782782
return new ClassNameToObjectTypeResult($this, true);
783783
}
784784

785+
public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
786+
{
787+
if ($allowString) {
788+
return new ClassNameToObjectTypeResult(
789+
new UnionType([new ObjectWithoutClassType(), new ClassStringType()]),
790+
false,
791+
);
792+
}
793+
794+
return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false);
795+
}
796+
785797
public function toAbsoluteNumber(): Type
786798
{
787799
return new ErrorType();

0 commit comments

Comments
 (0)