From b2b3b0e4cad19bccae71382b0c0abd1f847258d7 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 7 Sep 2021 12:40:49 +0100 Subject: [PATCH] Fix auto-generated return values with union types --- library/Mockery/Mock.php | 9 +- library/Mockery/Reflector.php | 105 +++++++++++++++++++--- tests/PHP81/Php81LanguageFeaturesTest.php | 19 ++++ 3 files changed, 116 insertions(+), 17 deletions(-) diff --git a/library/Mockery/Mock.php b/library/Mockery/Mock.php index 8dcc4c405..e62116f98 100644 --- a/library/Mockery/Mock.php +++ b/library/Mockery/Mock.php @@ -705,18 +705,13 @@ public function mockery_getMethod($name) */ public function mockery_returnValueForMethod($name) { - if (\PHP_VERSION_ID < 70000) { - return null; - } - $rm = $this->mockery_getMethod($name); - // Default return value for methods with nullable type is null - if ($rm === null || $rm->getReturnType() === null || $rm->getReturnType()->allowsNull()) { + if ($rm === null) { return null; } - $returnType = Reflector::getReturnType($rm, true); + $returnType = Reflector::getSimplestReturnType($rm); switch ($returnType) { case null: return null; diff --git a/library/Mockery/Reflector.php b/library/Mockery/Reflector.php index 924bc89dc..3ec7cde12 100644 --- a/library/Mockery/Reflector.php +++ b/library/Mockery/Reflector.php @@ -98,13 +98,53 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable return null; } - $declaringClass = $method->getDeclaringClass(); - $typeHint = self::typeToString($type, $declaringClass); + $typeHint = self::typeToString($type, $method->getDeclaringClass()); // PHP 7.1+ supports nullable types via a leading question mark return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint; } + /** + * Compute the string representation for the simplest return type. + * + * @param \ReflectionParameter $param + * + * @return string|null + */ + public static function getSimplestReturnType(\ReflectionMethod $method) + { + // Strip all return types for HHVM and skip PHP 5. + if (method_exists($method, 'getReturnTypeText') || \PHP_VERSION_ID < 70000) { + return null; + } + + $type = $method->getReturnType(); + + if (is_null($type) && method_exists($method, 'getTentativeReturnType')) { + $type = $method->getTentativeReturnType(); + } + + if (is_null($type) || $type->allowsNull()) { + return null; + } + + $typeInformation = self::getTypeInformation($type, $method->getDeclaringClass()); + + // return the first primitive type hint + foreach ($typeInformation as $info) { + if ($info['isPrimitive']) { + return $info['typeHint']; + } + } + + // if no primitive type, return the first type + foreach ($typeInformation as $info) { + return $info['typeHint']; + } + + return null; + } + /** * Compute the legacy type hint. * @@ -198,22 +238,62 @@ private static function getLegacyClassName(\ReflectionParameter $param) * @return string|null */ private static function typeToString(\ReflectionType $type, \ReflectionClass $declaringClass) + { + return \implode('|', \array_map(function (array $typeInformation) { + return $typeInformation['typeHint']; + }, self::getTypeInformation($type, $declaringClass))); + } + + /** + * Get the string representation of the given type. + * + * This method MUST only be called on PHP 7+. + * + * @param \ReflectionType $type + * @param \ReflectionClass $declaringClass + * + * @return list + */ + private static function getTypeInformation(\ReflectionType $type, \ReflectionClass $declaringClass) { // PHP 8 union types can be recursively processed if ($type instanceof \ReflectionUnionType) { - return \implode('|', \array_filter(\array_map(function (\ReflectionType $type) use ($declaringClass) { - $typeHint = self::typeToString($type, $declaringClass); + $types = []; + + foreach ($type->getTypes() as $innterType) { + foreach (self::getTypeInformation($innterType, $declaringClass) as $info) { + if ($info['typeHint'] === 'null' && $info['isPrimitive']) { + continue; + } - return $typeHint === 'null' ? null : $typeHint; - }, $type->getTypes()))); + $types[] = $info; + } + } + + return $types; } // PHP 7.0 doesn't have named types, but 7.1+ does $typeHint = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; - // builtins and 'static' can be returned as is - if (($type->isBuiltin() || $typeHint === 'static')) { - return $typeHint; + // builtins can be returned as is + if ($type->isBuiltin()) { + return [ + [ + 'typeHint' => $typeHint, + 'isPrimitive' => in_array($typeHint, ['array', 'bool', 'int', 'float', 'null', 'object', 'string']), + ], + ]; + } + + // 'static' can be returned as is + if ($typeHint === 'static') { + return [ + [ + 'typeHint' => $typeHint, + 'isPrimitive' => false, + ], + ]; } // 'self' needs to be resolved to the name of the declaring class @@ -227,7 +307,12 @@ private static function typeToString(\ReflectionType $type, \ReflectionClass $de } // class names need prefixing with a slash - return sprintf('\\%s', $typeHint); + return [ + [ + 'typeHint' => sprintf('\\%s', $typeHint), + 'isPrimitive' => false, + ], + ]; } /** diff --git a/tests/PHP81/Php81LanguageFeaturesTest.php b/tests/PHP81/Php81LanguageFeaturesTest.php index 1bdfb39de..7f902b212 100644 --- a/tests/PHP81/Php81LanguageFeaturesTest.php +++ b/tests/PHP81/Php81LanguageFeaturesTest.php @@ -4,6 +4,7 @@ use DateTime; use Serializable; +use Mockery as m; use Mockery\Adapter\Phpunit\MockeryTestCase; use ReturnTypeWillChange; @@ -30,6 +31,24 @@ public function it_can_mock_an_internal_class_with_tentative_return_types() $this->assertSame(0, $mock->getTimestamp()); } + /** + * @test + */ + public function it_can_mock_an_internal_class_with_tentative_union_return_types() + { + $mock = m::mock('PDO'); + + $this->assertInstanceOf('PDO', $mock); + + $mock->shouldReceive('exec')->once(); + + try { + $this->assertSame(0, $mock->exec('select * from foo.bar')); + } finally { + m::close(); + } + } + /** @test */ public function it_can_mock_a_class_with_return_type_will_change_attribute_and_no_return_type() {