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.3] Fix auto-generated return values with union types #1143

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
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