diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 4c45b05744..812b2b706b 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -178,11 +178,12 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $otherProperty = $type->getProperty($propertyName, $scope); } catch (MissingPropertyFromReflectionException) { return new AcceptsResult( - TrinaryLogic::createNo(), + $result->result, [ sprintf( - '%s does not have property $%s.', + '%s %s not have property $%s.', $type->describe(VerbosityLevel::typeOnly()), + $result->no() ? 'does' : 'might', $propertyName, ), ], @@ -278,7 +279,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic try { $otherProperty = $type->getProperty($propertyName, $scope); } catch (MissingPropertyFromReflectionException) { - return TrinaryLogic::createNo(); + return $result; } if (!$otherProperty->isPublic()) { diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 95581d836a..b81d2bd964 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2919,7 +2919,7 @@ public function testObjectShapes(): void [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', 14, - 'Exception does not have property $foo.', + 'Exception might not have property $foo.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int} given.', @@ -2989,6 +2989,16 @@ public function testObjectShapes(): void 157, 'Property ($foo) type int does not accept type string.', ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, Traversable given.', + 209, + 'Traversable might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, ObjectShapesAcceptance\FinalClass given.', + 210, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass might not have property $foo.' : 'ObjectShapesAcceptance\FinalClass does not have property $foo.', + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/object-shapes.php b/tests/PHPStan/Rules/Methods/data/object-shapes.php index 551bd06f80..248eeecc30 100644 --- a/tests/PHPStan/Rules/Methods/data/object-shapes.php +++ b/tests/PHPStan/Rules/Methods/data/object-shapes.php @@ -174,3 +174,41 @@ public function doBaz(object $o): void } } + +final class FinalClass +{ + +} + +class ClassWithFooIntProperty +{ + + /** @var int */ + public $foo; + +} + +class TestAcceptance +{ + + /** + * @param object{foo: int} $o + * @return void + */ + public function doFoo(object $o): void + { + + } + + public function doBar( + \Traversable $traversable, + FinalClass $finalClass, + ClassWithFooIntProperty $classWithFooIntProperty + ) + { + $this->doFoo($traversable); + $this->doFoo($finalClass); + $this->doFoo($classWithFooIntProperty); + } + +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 58a248ec44..206c2aeb7c 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -13,6 +13,7 @@ use Exception; use InvalidArgumentException; use Iterator; +use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryLiteralStringType; @@ -2396,6 +2397,39 @@ public function dataUnion(): iterable UnionType::class, 'object{bar: string}|object{foo: int}', ]; + + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + UnionType::class, + 'object{foo: int}|Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShape\Foo::class), + ], + UnionType::class, + 'ObjectShape\Foo|object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + UnionType::class, + 'ObjectShapesAcceptance\FinalClass|object{foo: int}', + ]; } /** @@ -3936,6 +3970,38 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + IntersectionType::class, + 'object{foo: int}&Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\Foo::class), + ], + IntersectionType::class, + 'ObjectShapesAcceptance\Foo&object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectType::class, + 'ObjectShapesAcceptance\ClassWithFooIntProperty', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass&object{foo: int}' : '*NEVER*=implicit', + ]; } /**