Skip to content

Commit

Permalink
Merge pull request #1143 from GrahamCampbell/13-fix-union-return
Browse files Browse the repository at this point in the history
[1.3] Fix auto-generated return values with union types
  • Loading branch information
davedevelopment committed Sep 13, 2021
2 parents 6be9039 + b2b3b0e commit 553ad47
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 17 deletions.
9 changes: 2 additions & 7 deletions library/Mockery/Mock.php
Expand Up @@ -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;
Expand Down
105 changes: 95 additions & 10 deletions library/Mockery/Reflector.php
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<array{typeHint: string, isPrimitive: bool}>
*/
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
Expand All @@ -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,
],
];
}

/**
Expand Down
19 changes: 19 additions & 0 deletions tests/PHP81/Php81LanguageFeaturesTest.php
Expand Up @@ -4,6 +4,7 @@

use DateTime;
use Serializable;
use Mockery as m;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use ReturnTypeWillChange;

Expand All @@ -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()
{
Expand Down

0 comments on commit 553ad47

Please sign in to comment.