Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.4] Fix auto-generated return values with union types #1144

Merged
merged 1 commit into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions library/Mockery/Mock.php
Expand Up @@ -717,12 +717,11 @@ public function mockery_returnValueForMethod($name)
{
$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
100 changes: 88 additions & 12 deletions library/Mockery/Reflector.php
Expand Up @@ -81,12 +81,47 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable
return null;
}

$declaringClass = $method->getDeclaringClass();
$typeHint = self::typeToString($type, $declaringClass);
$typeHint = self::typeToString($type, $method->getDeclaringClass());

return (!$withoutNullable && $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)
{
$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;
}

/**
* Get the string representation of the given type.
*
Expand All @@ -96,22 +131,60 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable
* @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.
*
* @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;
}

// $type must be an instance of \ReflectionNamedType
$typeHint = $type->getName();

// 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 @@ -125,14 +198,17 @@ 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,
],
];
}

/**
* Format the given type as a nullable type.
*
* This method MUST only be called on PHP 7.1+.
*
* @param string $typeHint
*
* @return string
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