From 923bd58cc1c8d2bf8e78a8fbca4b224805237dc2 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:50:36 +0200 Subject: [PATCH] Allow to `Assert::isInstanceOf()` to work with generic and anonymous class strings --- .../AssertTypeSpecifyingExtension.php | 41 ++++++------ .../AssertTypeSpecifyingExtensionTest.php | 1 + .../ImpossibleCheckTypeMethodCallRuleTest.php | 4 ++ tests/Type/WebMozartAssert/data/bug-183.php | 62 +++++++++++++++++++ .../Type/WebMozartAssert/data/collection.php | 2 +- tests/Type/WebMozartAssert/data/type.php | 6 +- 6 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 tests/Type/WebMozartAssert/data/bug-183.php diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index de018d8..9b35420 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -49,6 +49,7 @@ use ReflectionObject; use Traversable; use function array_key_exists; +use function array_map; use function array_reduce; use function array_shift; use function count; @@ -407,33 +408,37 @@ private function getExpressionResolvers(): array }, 'isInstanceOf' => static function (Scope $scope, Arg $expr, Arg $class): ?Expr { $classType = $scope->getType($class->value); - $classNameStrings = $classType->getConstantStrings(); - if (count($classNameStrings) !== 1) { - $classNames = $classType->getObjectClassNames(); - if (count($classNames) === 1) { - return new Instanceof_( - $expr->value, - new Name($classNames[0]) - ); - } - return null; + $classNames = $classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + + if (count($classNames) !== 0) { + return self::implodeExpr(array_map(static function (string $className) use ($expr): Expr { + return new Instanceof_($expr->value, new Name($className)); + }, $classNames), BooleanOr::class); } - return new Instanceof_( - $expr->value, - new Name($classNameStrings[0]->getValue()) + return new FuncCall( + new Name('is_object'), + [$expr] ); }, 'isInstanceOfAny' => function (Scope $scope, Arg $expr, Arg $classes): ?Expr { return self::buildAnyOfExpr($scope, $expr, $classes, $this->resolvers['isInstanceOf']); }, - 'notInstanceOf' => function (Scope $scope, Arg $expr, Arg $class): ?Expr { - $expr = $this->resolvers['isInstanceOf']($scope, $expr, $class); - if ($expr === null) { - return null; + 'notInstanceOf' => static function (Scope $scope, Arg $expr, Arg $class): ?Expr { + $classType = $scope->getType($class->value); + $classNames = $classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + + if (count($classNames) !== 0) { + $result = self::implodeExpr(array_map(static function (string $className) use ($expr): Expr { + return new Instanceof_($expr->value, new Name($className)); + }, $classNames), BooleanOr::class); + + if ($result !== null) { + return new BooleanNot($result); + } } - return new BooleanNot($expr); + return null; }, 'isAOf' => static function (Scope $scope, Arg $expr, Arg $class): Expr { $exprType = $scope->getType($expr->value); diff --git a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php index 2ab2c96..a0b14dd 100644 --- a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php +++ b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php @@ -22,6 +22,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-18.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-117.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-150.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-183.php'); } /** diff --git a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php index 190e60d..95b1dfc 100644 --- a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php @@ -101,6 +101,10 @@ public function testExtension(): void 'Call to static method Webmozart\Assert\Assert::implementsInterface() with mixed and \'WebmozartAssertImpossibleCheck\\\Foo\' will always evaluate to false.', 111, ], + [ + 'Call to static method Webmozart\Assert\Assert::isInstanceOf() with Exception and class-string will always evaluate to true.', + 119, + ], ]); } diff --git a/tests/Type/WebMozartAssert/data/bug-183.php b/tests/Type/WebMozartAssert/data/bug-183.php new file mode 100644 index 0000000..decd9f7 --- /dev/null +++ b/tests/Type/WebMozartAssert/data/bug-183.php @@ -0,0 +1,62 @@ + $className + */ +function assertInstanceOfWithGenericClassString($value, string $className): void +{ + Assert::isInstanceOf($value, $className); + assertType('Bug183', $value); +} + +/** + * @param class-string<\Bug183Bar<\Bug183>> $className + */ +function assertInstanceOfWithGenericClassStringReferencingGenericClass($value, string $className): void +{ + Assert::isInstanceOf($value, $className); + assertType('Bug183Bar', $value); +} + +/** + * @param class-string<\Bug183|\Bug183Foo> $className + */ +function assertInstanceOfWithGenericUnionClassString($value, string $className): void +{ + Assert::isInstanceOf($value, $className); + assertType('Bug183|Bug183Foo', $value); +} + + +interface Bug183 +{ +} + +interface Bug183Foo +{ +} + +/** + * @template T of object + */ +interface Bug183Bar +{ +} diff --git a/tests/Type/WebMozartAssert/data/collection.php b/tests/Type/WebMozartAssert/data/collection.php index 836b8cd..a853731 100644 --- a/tests/Type/WebMozartAssert/data/collection.php +++ b/tests/Type/WebMozartAssert/data/collection.php @@ -75,7 +75,7 @@ public function allInstanceOf(array $a, array $b, array $c, $d): void assertType('array', $b); Assert::allIsInstanceOf($c, 17); - assertType('array', $c); + assertType('array', $c); Assert::allIsInstanceOf($d, new stdClass()); assertType('iterable', $d); diff --git a/tests/Type/WebMozartAssert/data/type.php b/tests/Type/WebMozartAssert/data/type.php index f920254..9216966 100644 --- a/tests/Type/WebMozartAssert/data/type.php +++ b/tests/Type/WebMozartAssert/data/type.php @@ -197,7 +197,7 @@ public function isInstanceOf($a, $b, $c, $d): void assertType('stdClass', $c); Assert::isInstanceOf($d, 17); - assertType('mixed', $d); + assertType('object', $d); } public function isInstanceOfAny($a, $b, $c, $d, $e, $f, $g): void @@ -215,10 +215,10 @@ public function isInstanceOfAny($a, $b, $c, $d, $e, $f, $g): void assertType('mixed', $d); Assert::isInstanceOfAny($e, [17]); - assertType('mixed', $e); + assertType('object', $e); Assert::isInstanceOfAny($f, [17, self::class]); - assertType('PHPStan\Type\WebMozartAssert\TypeTest', $f); + assertType('object', $f); Assert::nullOrIsInstanceOfAny($g, [self::class, new stdClass()]); assertType('PHPStan\Type\WebMozartAssert\TypeTest|stdClass|null', $g);