Skip to content

Commit

Permalink
Closes #4932
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Mar 15, 2022
1 parent 4a11788 commit bf4dc84
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 67 deletions.
8 changes: 8 additions & 0 deletions .psalm/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,14 @@
<code>$client-&gt;__getFunctions()</code>
</PossiblyNullArgument>
</file>
<file src="src/Framework/MockObject/Invocation.php">
<ArgumentTypeCoercion occurrences="1">
<code>$types</code>
</ArgumentTypeCoercion>
<MissingThrowsDocblock occurrences="1">
<code>throw $t;</code>
</MissingThrowsDocblock>
</file>
<file src="src/Framework/MockObject/InvocationHandler.php">
<MissingReturnType occurrences="1">
<code>invoke</code>
Expand Down
1 change: 1 addition & 0 deletions ChangeLog-9.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes of the PHPUnit 9.5 release series are documented in this fil
### Fixed

* [#4929](https://github.com/sebastianbergmann/phpunit/issues/4929): Test Double code generator does not handle new expressions inside parameter default values
* [#4932](https://github.com/sebastianbergmann/phpunit/issues/4932): Backport support for intersection types from PHPUnit 10 to PHPUnit 9.5
* [#4933](https://github.com/sebastianbergmann/phpunit/issues/4933): Backport support for `never` type from PHPUnit 10 to PHPUnit 9.5

## [9.5.18] - 2022-03-08
Expand Down
61 changes: 61 additions & 0 deletions src/Framework/MockObject/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,67 @@ public function getMock(string $type, $methods = [], array $arguments = [], stri
);
}

/**
* @psalm-param list<class-string> $interfaces
*
* @throws RuntimeException
* @throws UnknownTypeException
*/
public function getMockForInterfaces(array $interfaces, bool $callAutoload = true): MockObject
{
if (count($interfaces) < 2) {
throw new RuntimeException('At least two interfaces must be specified');
}

foreach ($interfaces as $interface) {
if (!interface_exists($interface, $callAutoload)) {
throw new UnknownTypeException($interface);
}
}

sort($interfaces);

$methods = [];

foreach ($interfaces as $interface) {
$methods = array_merge($methods, $this->getClassMethods($interface));
}

if (count(array_unique($methods)) < count($methods)) {
throw new RuntimeException('Interfaces must not declare the same method');
}

$unqualifiedNames = [];

foreach ($interfaces as $interface) {
$parts = explode('\\', $interface);
$unqualifiedNames[] = array_pop($parts);
}

sort($unqualifiedNames);

do {
$intersectionName = sprintf(
'Intersection_%s_%s',
implode('_', $unqualifiedNames),
substr(md5((string) mt_rand()), 0, 8)
);
} while (interface_exists($intersectionName, false));

$template = $this->getTemplate('intersection.tpl');

$template->setVar(
[
'intersection' => $intersectionName,
'interfaces' => implode(', ', $interfaces),
]
);

eval($template->render());

return $this->getMock($intersectionName);
}

/**
* Returns a mock object for the specified abstract class with all abstract
* methods of the class mocked.
Expand Down
5 changes: 5 additions & 0 deletions src/Framework/MockObject/Generator/intersection.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare(strict_types=1);

interface {intersection} extends {interfaces}
{
}
171 changes: 106 additions & 65 deletions src/Framework/MockObject/Invocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,100 +121,141 @@ public function generateReturnValue()
return null;
}

$union = false;
$intersection = false;
$union = false;

if (strpos($this->returnType, '|') !== false) {
$types = explode('|', $this->returnType);
$union = true;
} elseif (strpos($this->returnType, '&') !== false) {
$types = explode('&', $this->returnType);
$intersection = true;
} else {
$types = [$this->returnType];
}

$types = array_map('strtolower', $types);

if (in_array('', $types, true) ||
in_array('null', $types, true) ||
in_array('mixed', $types, true) ||
in_array('void', $types, true)) {
return null;
}
if (!$intersection) {
if (in_array('', $types, true) ||
in_array('null', $types, true) ||
in_array('mixed', $types, true) ||
in_array('void', $types, true)) {
return null;
}

if (in_array('false', $types, true) ||
in_array('bool', $types, true)) {
return false;
}
if (in_array('false', $types, true) ||
in_array('bool', $types, true)) {
return false;
}

if (in_array('float', $types, true)) {
return 0.0;
}
if (in_array('float', $types, true)) {
return 0.0;
}

if (in_array('int', $types, true)) {
return 0;
}
if (in_array('int', $types, true)) {
return 0;
}

if (in_array('string', $types, true)) {
return '';
}
if (in_array('string', $types, true)) {
return '';
}

if (in_array('array', $types, true)) {
return [];
}
if (in_array('array', $types, true)) {
return [];
}

if (in_array('static', $types, true)) {
try {
return (new Instantiator)->instantiate(get_class($this->object));
} catch (Throwable $t) {
throw new RuntimeException(
$t->getMessage(),
(int) $t->getCode(),
$t
);
if (in_array('static', $types, true)) {
try {
return (new Instantiator)->instantiate(get_class($this->object));
} catch (Throwable $t) {
throw new RuntimeException(
$t->getMessage(),
(int) $t->getCode(),
$t
);
}
}
}

if (in_array('object', $types, true)) {
return new stdClass;
}
if (in_array('object', $types, true)) {
return new stdClass;
}

if (in_array('callable', $types, true) ||
in_array('closure', $types, true)) {
return static function (): void
{
};
}
if (in_array('callable', $types, true) ||
in_array('closure', $types, true)) {
return static function (): void
{
};
}

if (in_array('traversable', $types, true) ||
in_array('generator', $types, true) ||
in_array('iterable', $types, true)) {
$generator = static function (): \Generator
{
yield from [];
};
if (in_array('traversable', $types, true) ||
in_array('generator', $types, true) ||
in_array('iterable', $types, true)) {
$generator = static function (): \Generator
{
yield from [];
};

return $generator();
}
return $generator();
}

if (!$union) {
try {
return (new Generator)->getMock($this->returnType, [], [], '', false);
} catch (Throwable $t) {
if ($t instanceof Exception) {
throw $t;
}

if (!$union) {
try {
return (new Generator)->getMock($this->returnType, [], [], '', false);
} catch (Throwable $t) {
throw new RuntimeException(
sprintf(
'Return value for %s::%s() cannot be generated: %s',
$this->className,
$this->methodName,
throw new RuntimeException(
$t->getMessage(),
),
(int) $t->getCode(),
);
(int) $t->getCode(),
$t
);
}
}
}

$reason = '';

if ($union) {
$reason = ' because the declared return type is a union';
} elseif ($intersection) {
$reason = ' because the declared return type is an intersection';

$onlyInterfaces = true;

foreach ($types as $type) {
if (!interface_exists($type)) {
$onlyInterfaces = false;

break;
}
}

if ($onlyInterfaces) {
try {
return (new Generator)->getMockForInterfaces($types);
} catch (Throwable $t) {
throw new RuntimeException(
sprintf(
'Return value for %s::%s() cannot be generated: %s',
$this->className,
$this->methodName,
$t->getMessage(),
),
(int) $t->getCode(),
);
}
}
}

throw new RuntimeException(
sprintf(
'Return value for %s::%s() cannot be generated because the declared return type is a union, please configure a return value for this method',
'Return value for %s::%s() cannot be generated%s, please configure a return value for this method',
$this->className,
$this->methodName
$this->methodName,
$reason
)
);
}
Expand Down
17 changes: 15 additions & 2 deletions src/Framework/MockObject/MockMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use function substr_count;
use function trim;
use function var_export;
use ReflectionIntersectionType;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
Expand All @@ -32,7 +33,6 @@
use SebastianBergmann\Type\ReflectionMapper;
use SebastianBergmann\Type\Type;
use SebastianBergmann\Type\UnknownType;
use SebastianBergmann\Type\VoidType;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
Expand Down Expand Up @@ -309,7 +309,7 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
}

if ($type !== null) {
if ($typeName !== 'mixed' && $parameter->allowsNull() && !$type instanceof ReflectionUnionType) {
if ($typeName !== 'mixed' && $parameter->allowsNull() && !$type instanceof ReflectionIntersectionType && !$type instanceof ReflectionUnionType) {
$nullable = '?';
}

Expand All @@ -322,6 +322,8 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
$type,
$method->getDeclaringClass()->getName()
);
} elseif ($type instanceof ReflectionIntersectionType) {
$typeDeclaration = self::intersectionTypeAsString($type);
}
}

Expand Down Expand Up @@ -418,4 +420,15 @@ private static function unionTypeAsString(ReflectionUnionType $union, string $se

return implode('|', $types) . ' ';
}

private static function intersectionTypeAsString(ReflectionIntersectionType $intersection): string
{
$types = [];

foreach ($intersection->getTypes() as $type) {
$types[] = $type;
}

return implode('&', $types) . ' ';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture\MockObject;

interface InterfaceWithMethodReturningIntersection
{
public function method(): AnInterface & AnotherInterface;
}
Loading

0 comments on commit bf4dc84

Please sign in to comment.