Skip to content

Commit a01670e

Browse files
committed
Extract Type::toObjectTypeForInstanceofCheck() for instanceof RHS
Replaces the duplicate `TypeTraverser::map` callback in `TypeSpecifier::specifyTypesInCondition()` (the `instanceof <expr>` branch) and the `InstanceOfClassTypeTraverser` helper used by `InstanceofHandler` with one polymorphic `Type` method. Both sites needed an out-of-band by-ref `$uncertainty` flag to carry "we kept this symbolically, don't decide yes/no definitively"; the new method returns a small `ClassNameToObjectTypeResult` value object (`Type $type, bool $uncertainty`) so each leaf can carry its own answer through composite-type distribution. Per leaf: - `ObjectType` and `ObjectTypeTrait` users (and `ClosureType`, `NonexistentParentClassType`, `StaticType`): keep `$this` as the comparison target and mark uncertain — the runtime class is only known when `instanceof` actually executes. - `GenericClassStringType`: project to `getGenericType()` and mark uncertain (the class name is symbolic). - `ConstantStringType`: collapse to `new ObjectType($value)` with no uncertainty (the class name is concrete). - All other non-objects (via `NonObjectTypeTrait`, `MaybeObjectTypeTrait`): `MixedType`, no uncertainty (matches the original `return new MixedType()` fallback). - `MixedType` / `StrictMixedType`: same `MixedType` fallback. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, OR-folding the uncertainty across members (matches the original closure-captured behavior). - `LateResolvableTypeTrait`: delegate to `resolve()`. `InstanceOfClassTypeTraverser` is removed (no longer used). Pure refactor: full test suite + phpstan + cs pass.
1 parent 8fafec7 commit a01670e

20 files changed

Lines changed: 155 additions & 70 deletions

src/Analyser/ExprHandler/InstanceofHandler.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\Analyser\ExprHandler;
1313
use PHPStan\Analyser\MutatingScope;
1414
use PHPStan\Analyser\NodeScopeResolver;
15-
use PHPStan\Analyser\Traverser\InstanceOfClassTypeTraverser;
1615
use PHPStan\DependencyInjection\AutowiredService;
1716
use PHPStan\Type\BooleanType;
1817
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -21,7 +20,6 @@
2120
use PHPStan\Type\ObjectType;
2221
use PHPStan\Type\StaticType;
2322
use PHPStan\Type\Type;
24-
use PHPStan\Type\TypeTraverser;
2523
use PHPStan\Type\TypeUtils;
2624
use function array_merge;
2725
use function strtolower;
@@ -94,9 +92,9 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
9492
}
9593
} else {
9694
$classType = $scope->getType($expr->class);
97-
$traverser = new InstanceOfClassTypeTraverser();
98-
$classType = TypeTraverser::map($classType, $traverser);
99-
$uncertainty = $traverser->getUncertainty();
95+
$result = $classType->toObjectTypeForInstanceofCheck();
96+
$classType = $result->type;
97+
$uncertainty = $result->uncertainty;
10098
}
10199

102100
if ($classType->isSuperTypeOf(new MixedType())->yes()) {

src/Analyser/Traverser/InstanceOfClassTypeTraverser.php

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/Analyser/TypeSpecifier.php

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -165,24 +165,9 @@ public function specifyTypesInCondition(
165165
}
166166

167167
$classType = $scope->getType($expr->class);
168-
$uncertainty = false;
169-
$type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use (&$uncertainty): Type {
170-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
171-
return $traverse($type);
172-
}
173-
if ($type->getObjectClassNames() !== []) {
174-
$uncertainty = true;
175-
return $type;
176-
}
177-
if ($type instanceof GenericClassStringType) {
178-
$uncertainty = true;
179-
return $type->getGenericType();
180-
}
181-
if ($type instanceof ConstantStringType) {
182-
return new ObjectType($type->getValue());
183-
}
184-
return new MixedType();
185-
});
168+
$result = $classType->toObjectTypeForInstanceofCheck();
169+
$type = $result->type;
170+
$uncertainty = $result->uncertainty;
186171

187172
if (!$type->isSuperTypeOf(new MixedType())->yes()) {
188173
if ($context->true()) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
/**
6+
* Result of projecting a "class-name-or-object" `Type` to its corresponding
7+
* `ObjectType` for an `instanceof` / `is_a` check.
8+
*
9+
* `$uncertainty` is `true` when the projection lost information that prevents
10+
* a definite yes/no decision later — e.g. a runtime class string was kept
11+
* symbolically instead of being resolved to a concrete object type. Composite
12+
* types OR-fold the flag across their members.
13+
*/
14+
final class ClassNameToObjectTypeResult
15+
{
16+
17+
public function __construct(
18+
public readonly Type $type,
19+
public readonly bool $uncertainty,
20+
)
21+
{
22+
}
23+
24+
}

src/Type/ClosureType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,11 @@ public function toClassConstantType(ReflectionProvider $reflectionProvider): Typ
526526
return $this->objectType->toClassConstantType($reflectionProvider);
527527
}
528528

529+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
530+
{
531+
return new ClassNameToObjectTypeResult($this, true);
532+
}
533+
529534
public function toAbsoluteNumber(): Type
530535
{
531536
return new ErrorType();

src/Type/Constant/ConstantStringType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
2626
use PHPStan\Type\Accessory\AccessoryNumericStringType;
2727
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
28+
use PHPStan\Type\ClassNameToObjectTypeResult;
2829
use PHPStan\Type\ClassStringType;
2930
use PHPStan\Type\CompoundType;
3031
use PHPStan\Type\ConstantScalarType;
@@ -293,6 +294,11 @@ public function toBitwiseNotType(): Type
293294
return new ConstantStringType(~$this->value);
294295
}
295296

297+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
298+
{
299+
return new ClassNameToObjectTypeResult(new ObjectType($this->value), false);
300+
}
301+
296302
public function toAbsoluteNumber(): Type
297303
{
298304
return $this->toNumber()->toAbsoluteNumber();

src/Type/Generic/GenericClassStringType.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
88
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
99
use PHPStan\Type\AcceptsResult;
10+
use PHPStan\Type\ClassNameToObjectTypeResult;
1011
use PHPStan\Type\ClassStringType;
1112
use PHPStan\Type\CompoundType;
1213
use PHPStan\Type\Constant\ConstantStringType;
@@ -45,6 +46,15 @@ public function getGenericType(): Type
4546
return $this->type;
4647
}
4748

49+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
50+
{
51+
// `class-string<X>` narrows to `X` for the comparison target, but
52+
// the actual runtime class can be any subclass of `X` — keep
53+
// uncertainty so the caller falls back to `BooleanType` instead
54+
// of a definite yes when `$x instanceof Y` and `Y === X`.
55+
return new ClassNameToObjectTypeResult($this->getGenericType(), true);
56+
}
57+
4858
public function getClassStringObjectType(): Type
4959
{
5060
return $this->getGenericType();

src/Type/IntersectionType.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,23 @@ public function toClassConstantType(ReflectionProvider $reflectionProvider): Typ
14271427
return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
14281428
}
14291429

1430+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1431+
{
1432+
$types = [];
1433+
$uncertainty = false;
1434+
foreach ($this->getTypes() as $innerType) {
1435+
$result = $innerType->toObjectTypeForInstanceofCheck();
1436+
$types[] = $result->type;
1437+
if (!$result->uncertainty) {
1438+
continue;
1439+
}
1440+
1441+
$uncertainty = true;
1442+
}
1443+
1444+
return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1445+
}
1446+
14301447
public function toAbsoluteNumber(): Type
14311448
{
14321449
$type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());

src/Type/MixedType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,11 @@ public function toClassConstantType(ReflectionProvider $reflectionProvider): Typ
642642
return new ErrorType();
643643
}
644644

645+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
646+
{
647+
return new ClassNameToObjectTypeResult(new MixedType(), false);
648+
}
649+
645650
public function toAbsoluteNumber(): Type
646651
{
647652
return $this->toNumber()->toAbsoluteNumber();

src/Type/NeverType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ public function toClassConstantType(ReflectionProvider $reflectionProvider): Typ
450450
return $this;
451451
}
452452

453+
public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
454+
{
455+
return new ClassNameToObjectTypeResult($this, false);
456+
}
457+
453458
public function toAbsoluteNumber(): Type
454459
{
455460
return $this;

0 commit comments

Comments
 (0)